PageSourceSearch

https://4responsible.ai/site-index

html 4responsible.ai collected 2026-10-05 15:11:48 UTC 172,626 bytes, 2,973 lines download raw bytes

1<!DOCTYPE html><html style="" data-scribe-recorder-ready="true"><head><meta name="description" content="View this page published on 4responsible.ai">
2<meta property="og:title" content="Site Index — 4Responsible.AI">
3<meta property="og:description" content="View this page published on 4responsible.ai">
4<meta property="og:image" content="https://htmlpub.com/api/og/screenshot?id=rm90klif4ys3o04jw9zgjfpw&amp;v=1790529748919">
5<meta property="og:image:width" content="1200">
6<meta property="og:image:height" content="630">
7<meta property="og:url" content="https://4responsible.ai/site-index">
8<meta name="twitter:url" content="https://4responsible.ai/site-index">
9<meta property="og:type" content="website">
10<meta property="og:site_name" content="4responsible.ai">
11<meta property="og:locale" content="en_US">
12<meta property="fb:app_id" content="4198265810445529">
13<meta name="twitter:card" content="summary_large_image">
14<meta name="twitter:title" content="Site Index — 4Responsible.AI">
15<meta name="twitter:description" content="View this page published on 4responsible.ai">
16<meta name="twitter:image" content="https://htmlpub.com/api/og/screenshot?id=rm90klif4ys3o04jw9zgjfpw&amp;v=1790529748919">
17<meta name="twitter:image:width" content="1200">
18<meta name="twitter:image:height" content="630">
19<meta name="robots" content="index, follow">
20<link rel="canonical" href="https://4responsible.ai/site-index">
21<link rel="icon" href="https://4responsible.ai/api/library/at2io7gzpvjczuan26x0xyak">
22<title>Site Index — 4Responsible.AI</title>
23<meta charset="utf-8">
24<meta name="viewport" content="width=device-width, initial-scale=1">
25
26<script type="text/javascript" src="https://analytics.lead.eyes/analytics.js" id="leadpages-analytics" data-leadpages-analytics="true" defer="defer"></script>
26
27<style>
28:root{--panel:#07131e;--line:#17384b;--text:#eef7ff;--muted:#9bb1bd;--cyan:#23d9ff;--bg:#03070d}
29body{margin:0;background:var(--bg);color:var(--text);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;-webkit-font-smoothing:antialiased}
30.index{max-width:900px;margin:0 auto;padding:70px 24px 40px}
31.index h1{font-size:clamp(2rem,4.5vw,3.2rem);font-weight:800;letter-spacing:-.01em;margin:0 0 10px;color:var(--text)}
32.index h1 b{color:var(--cyan)}
33.index p.sub{font-size:.95rem;color:var(--muted);margin:0 0 42px;line-height:1.5;max-width:560px}
34.map{list-style:none;margin:0;padding:0;counter-reset:page}
35.map li{border-bottom:1px solid var(--line)}
36.map a{display:flex;align-items:baseline;gap:20px;padding:20px 8px;text-decoration:none;color:var(--text);transition:background .15s}
37.map a:hover{background:rgba(35,217,255,.05)}
38.map .no{font-size:.78rem;color:var(--muted);font-weight:700;letter-spacing:.08em;white-space:nowrap}
39.map .name{font-size:1.15rem;font-weight:800;color:var(--text);transition:color .15s;white-space:nowrap}
40.map a:hover .name{color:var(--cyan)}
41.map .desc{font-size:.82rem;color:var(--muted);font-weight:400}
42@media(max-width:640px){.map .desc{display:none}}
43</style>
44<style id="htmlpub-breakpoint-visibility">@media (max-width: 767px) { .hp-hide-mobile { display: none !important; } }
45@media (min-width: 768px) and (max-width: 1199px) { .hp-hide-tablet { display: none !important; } }
46@media (min-width: 1200px) { .hp-hide-desktop { display: none !important; } }</style></head>
47<body><a href="https://4responsible.ai/cdn-cgi/content?id=Qj.fA0HZKgYXjB6X6RubkUjXu5yHnSIqOrAEjq6nrfY-1791213108.6624029-1.2.1.1-FIBppySlutbPL.v13in4u9UuGUh_Dn4pzm_ZsbwgTG7W7lygedvDf0091.3LHQAi" aria-hidden="true" rel="nofollow noopener" style="display: none !important; visibility: hidden !important"></a>
48<header data-persist-part-id="4r-site-header" data-persist-part-kind="header" data-persist-nav-id="nav-cmbwk7s" style=""><style data-persist-part-style-for="4r-site-header">[data-persist-part-id="4r-site-header"]:is(*),[data-persist-part-id="4r-site-header"] *{box-sizing:border-box}[data-persist-part-id="4r-site-header"]:is(a),[data-persist-part-id="4r-site-header"] a{color:var(--cyan)}[data-persist-part-id="4r-site-header"]:is(header),[data-persist-part-id="4r-site-header"] header{display:grid;grid-template-columns:1fr auto;gap:24px;align-items:end;padding:18px 0 26px;border-bottom:1px solid var(--line)}@media (max-width:850px){[data-persist-part-id="4r-site-header"]:is(header),[data-persist-part-id="4r-site-header"] header{grid-template-columns:1fr}}[data-persist-part-id="4r-site-header"]:is(.sp-esvf8-site-nav),[data-persist-part-id="4r-site-header"] .sp-esvf8-site-nav{position:sticky;top:0;z-index:100;background:rgba(3,7,13,.88);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border-bottom:1px solid var(--line)}[data-persist-part-id="4r-site-header"]:is(.sp-esvf8-nav-bar),[data-persist-part-id="4r-site-header"] .sp-esvf8-nav-bar{max-width:1280px;margin:auto;padding:14px 24px;display:flex;align-items:center;justify-content:space-between;gap:24px}[data-persist-part-id="4r-site-header"]:is(.sp-esvf8-nav-logo),[data-persist-part-id="4r-site-header"] .sp-esvf8-nav-logo{font-family:ui-monospace,SFMono-Regular,monospace;font-weight:800;font-size:1.05rem;color:var(--text);text-decoration:none;letter-spacing:.02em;white-space:nowrap}[data-persist-part-id="4r-site-header"]:is(.sp-esvf8-nav-logo b),[data-persist-part-id="4r-site-header"] .sp-esvf8-nav-logo b{color:var(--cyan)}[data-persist-part-id="4r-site-header"]:is(.sp-esvf8-nav-links),[data-persist-part-id="4r-site-header"] .sp-esvf8-nav-links{display:flex;gap:22px;list-style:none;margin:0;padding:0}[data-persist-part-id="4r-site-header"]:is(.sp-esvf8-nav-links a),[data-persist-part-id="4r-site-header"] .sp-esvf8-nav-links a{color:var(--muted);text-decoration:none;font-size:.83rem;font-weight:600;letter-spacing:.01em;transition:color .15s;white-space:nowrap}[data-persist-part-id="4r-site-header"]:is(.sp-esvf8-nav-links a:hover),[data-persist-part-id="4r-site-header"] .sp-esvf8-nav-links a:hover{color:var(--cyan)}[data-persist-part-id="4r-site-header"]:is(.sp-esvf8-nav-toggle),[data-persist-part-id="4r-site-header"] .sp-esvf8-nav-toggle{display:none;background:none;border:1px solid var(--line);border-radius:8px;color:var(--text);padding:8px;
48cursor:pointer;align-items:center;justify-content:center}[data-persist-part-id="4r-site-header"]:is(.sp-esvf8-nav-toggle:hover),[data-persist-part-id="4r-site-header"] .sp-esvf8-nav-toggle:hover{border-color:#3289a4;color:var(--cyan)}[data-persist-part-id="4r-site-header"]:is(.sp-esvf8-nav-mobile-panel a),[data-persist-part-id="4r-site-header"] .sp-esvf8-nav-mobile-panel a{color:var(--text);text-decoration:none;font-size:.95rem;font-weight:700;padding:12px 24px}[data-persist-part-id="4r-site-header"]:is(.sp-esvf8-nav-mobile-panel a:hover),[data-persist-part-id="4r-site-header"] .sp-esvf8-nav-mobile-panel a:hover{color:var(--cyan)}@media (max-width:900px){[data-persist-part-id="4r-site-header"]:is(.sp-esvf8-nav-links),[data-persist-part-id="4r-site-header"] .sp-esvf8-nav-links{display:none}[data-persist-part-id="4r-site-header"]:is(.sp-esvf8-nav-toggle),[data-persist-part-id="4r-site-header"] .sp-esvf8-nav-toggle{display:flex}}[data-persist-part-id="4r-site-header"]:is([data-persist-nav-id] li),[data-persist-part-id="4r-site-header"] [data-persist-nav-id] li,[data-persist-part-id="4r-site-header"][data-persist-nav-id] li{list-style:none}[data-persist-part-id="4r-site-header"]:is([data-persist-nav-mobile-panel]),[data-persist-part-id="4r-site-header"] [data-persist-nav-mobile-panel],[data-persist-part-id="4r-site-header"]:is([data-persist-nav-tablet-panel]),[data-persist-part-id="4r-site-header"] [data-persist-nav-tablet-panel]{display:none}[data-persist-part-id="4r-site-header"]:is([data-persist-nav-mobile-panel].is-open),[data-persist-part-id="4r-site-header"] [data-persist-nav-mobile-panel].is-open,[data-persist-part-id="4r-site-header"]:is([data-persist-nav-tablet-panel].is-open),[data-persist-part-id="4r-site-header"] [data-persist-nav-tablet-panel].is-open{display:flex}[data-persist-part-id="4r-site-header"]:is([data-persist-nav-mobile-panel][data-persist-nav-mobile-panel-position="top"]),[data-persist-part-id="4r-site-header"] [data-persist-nav-mobile-panel][data-persist-nav-mobile-panel-position="top"],[data-persist-part-id="4r-site-header"]:is([data-persist-nav-tablet-panel][data-persist-nav-tablet-panel-position="top"]),[data-persist-part-id="4r-site-header"] [data-persist-nav-tablet-panel][data-persist-nav-tablet-panel-position="top"]{position:absolute;top:100%;left:0;right:0;flex-direction:column;gap:8px;padding:16px;background:#fff;box-shadow:0 8px 24px rgba(0,0,0,0.15);border-radius:0 0 8px 8px;z-index:1000;max-height:70vh;overflow-y:auto}[data-persist-part-id="4r-site-header"]:is([data-persist-nav-mobile-panel] a),[data-persist-part-id="4r-site-header"] [data-persist-nav-mobile-panel] a,[data-persist-part-id="4r-site-header"]:is([data-persist-nav-tablet-panel] a),[data-persist-part-id="4r-site-header"] [data-persist-nav-tablet-panel] a{display:block;padding:8px 4px;text-decoration:none;color:inherit}[data-persist-part-id="4r-site-header"]{--line:#17384b;--text:#eef7ff;--muted:#9bb1bd;--cyan:#23d9ff}[data-persist-part-id="4r-site-header"]:is(p),[data-persist-part-id="4r-site-header"] p,[data-persist-part-id="4r-site-header"]:is(div),[data-persist-part-id="4r-site-header"] div{color:rgba(238,247,255,.85)}</style>
49    <nav class="sp-esvf8-site-nav" data-persist-nav-id="4r-main-nav">
50      <div class="sp-esvf8-nav-bar" style="">
51        <a href="/4responsible-ai" class="sp-esvf8-nav-logo" data-persist-nav-logo="" style="color: rgb(3, 7, 13);"><br><br></a>
52        <a href="/site-index" class="sp-esvf8-nav-links" style="display: inline-flex; gap: 10px; align-items: center; padding: 9px 18px; border: 1px solid var(--cyan); border-radius: 99px; text-decoration: none; font-size: 0.83rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;" target="_blank" rel="noopener noreferrer"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg><span>Site Index</span></a>
53
54      </div>
55      
56    </nav>
57  </header>
58<section class="index" style="">
59<h1>Site <b>Index</b></h1>
60<p class="sub">A map of every node in the registry — the complete 4Responsible.AI standard, from first principles to sources.</p>
61<ul class="map">
62<li><a href="/4responsible-ai" style=""><span class="no">01</span><span class="name">Home</span><span class="desc" style="">Coevolutionary workforce system overview</span></a></li>
63<li><a href="/getting-started" style=""><span class="no">02</span><span class="name" style="">Getting Started</span><span class="desc" style="">
63How to begin applying the 4R framework</span></a></li>
64<li><a href="/ai-grounding" style=""><span class="no" style="">03</span><span class="name" style="">AI Grounding</span><span class="desc" style="">Human grounding before consequential AI implementation and delegation</span></a></li>
65<li><a href="/ai-skill-taxonomy" style=""><span class="no">04</span><span class="name" style="">AI Skills Taxonomy</span><span class="desc" style="">Skills for responsible AI work, mapped to the 4R principles</span></a></li>
66<li><a href="/four-governing-principles" style=""><span class="no">05</span><span class="name">Four Principles</span><span class="desc">The governing principles of responsible adoption</span></a></li>
67<li><a href="/the-standard" style=""><span class="no">06</span><span class="name">The Standard</span><span class="desc">The full normative specification</span></a></li>
68<li><a href="/maturity-model" style=""><span class="no">07</span><span class="name" style="">Maturity Model</span><span class="desc" style="">4R maturity — responsibility assessed in progression</span></a></li>
69<li><a href="/adoption-levels" style=""><span class="no">08</span><span class="name" style="">Adoption Levels</span><span class="desc" style="">Levels of AI adoption in the workforce</span></a></li>
70<li><a href="/human-coding" style=""><span class="no" style="">09</span><span class="name" style="">Human-Coding</span><span class="desc" style="">Coding human agency into AI systems</span></a></li>
71<li style=""><a href="/sources" style=""><span class="no">10</span><span class="name">Sources</span><span class="desc" style="">External human agency sources and references</span></a></li>
72<li><a href="/carlton-l-robinson" style=""><span class="no">11</span><span class="name">Architect</span><span class="desc" style="">DocCLR — Carlton L. Robinson, DBA</span></a></li>
73<li><a href="/agi" style=""><span class="no">12</span><span class="name">AGI</span><span class="desc" style="">AGI concerns mapped to the 4Responsible.AI contribution</span></a></li>
74<li><a href="/human-coding-agi-rsi" style=""><span class="no" style="">13</span><span class="name" style="">Human Coding vs AGI vs RSI</span><span class="desc" style="">20-dimension comparison across the three capability regimes</span></a></li>
75</ul>
76</section>
77<footer data-persist-part-id="4r-site-footer" data-persist-part-kind="footer"><style data-persist-part-style-for="4r-site-footer">[data-persist-part-id="4r-site-footer"]:is(*),[data-persist-part-id="4r-site-footer"] *{box-sizing:border-box}[data-persist-part-id="4r-site-footer"]:is(.sp-4r-footer){background:var(--panel);border-top:1px solid var(--line);padding:48px 24px 32px;margin-top:64px;font-family:"Times New Roman",serif}[data-persist-part-id="4r-site-footer"]:is(.sp-4r-footer-inner){max-width:720px;margin:0 auto;text-align:center}[data-persist-part-id="4r-site-footer"]:is(.sp-4r-footer-text){font-size:1rem;color:var(--text);margin:0 0 12px;line-height:1.5}[data-persist-part-id="4r-site-footer"]:is(.sp-4r-footer-architect){font-size:.95rem;color:var(--text);margin:0 0 16px}[data-persist-part-id="4r-site-footer"]:is(.sp-4r-footer-spec){font-size:.85rem;color:var(--muted);margin:0}[data-persist-part-id="4r-site-footer"]:is(.sp-4r-footer-spec a){color:var(--cyan);text-decoration:none}[data-persist-part-id="4r-site-footer"]:is(.sp-4r-footer-spec a:hover){text-decoration:underline}[data-persist-part-id="4r-site-footer"]:is(a),[data-persist-part-id="4r-site-footer"] a{color:var(--cyan)}[data-persist-part-id="4r-site-footer"]{--cyan:#23d9ff}[data-persist-part-id="4r-site-footer"]:is(p),[data-persist-part-id="4r-site-footer"] p,[data-persist-part-id="4r-site-footer"]:is(div),[data-persist-part-id="4r-site-footer"] div{color:rgba(238,247,255,.85)}[data-persist-part-id="4r-site-footer"]:is(p),[data-persist-part-id="4r-site-footer"] p{margin:.4rem 0}</style><div class="sp-4r-footer"><div class="sp-4r-footer-inner"><p class="sp-4r-footer-text" style="">This concept forms part of the 4Responsible.AI Human Agency Architecture for Responsible AI Workforce Adoption.</p><p class="sp-4r-footer-architect" style="">
77Architect: Carlton L. Robinson, DBA.</p><p class="sp-4r-footer-spec" style="">Open specification and version history: <a href="https://github.com/docclr/4ResponsibleAI-4responsible-ai-standard/tree/main" target="_blank" rel="noopener">GitHub</a> · Version: 1.0</p></div></div></footer>
78
79<script data-htmlpub-analytics>
80(function(){
81var PID="rm90klif4ys3o04jw9zgjfpw";
82var EXP_ID="";
83var EXP_VARIANT="";
84var API="/collect";
85var APP_ORIGIN="https://htmlpub.com";
86var FLUSH_INTERVAL=5000;
87var FLUSH_MAX=20;
88
89// HP-4568 consent gate. Runs onGranted() at most once, only when analytics
90// storage is permitted. See app/src/lib/analytics-consent-gate.ts.
91function hpConsent(onGranted){
92var started=false;
93function start(){if(started)return;started=true;try{onGranted();}catch(e){}}
94
95// Browser-level objection (Global Privacy Control / legacy Do-Not-Track) is
96// terminal: it is the visitor speaking directly, so no later CMP "accept all"
97// re-enables tracking for this page view. Mirrors the Customers.ai pixel gate.
98try{
99if(navigator.globalPrivacyControl===true)return;
100var dnt=navigator.doNotTrack||window.doNotTrack||navigator.msDoNotTrack;
101if(dnt==="1"||dnt===1||dnt===true)return;
102}catch(e){}
103
104// Analytics/statistics consent per whichever CMP the page carries:
105//    1 granted / 0 denied / -1 present but undecided / null no CMP detected
106function hpConsentState(){
107// Google consent mode v2 first: vendor-neutral, so a CMP writing it (directly
108// or via GTM) is authoritative even before its own JS API initialises. Last
109// write wins, matching gtag's 'default' then 'update'.
110try{
111var dl=window.dataLayer;
112if(dl&&dl.length){
113var seen=null;
114for(var i=0;i<dl.length;i++){
115var entry=dl[i];
116if(!entry||entry[0]!=="consent"||!entry[2])continue;
117var mode=entry[2].analytics_storage;
118if(mode==="granted")seen=1;else if(mode==="denied")seen=0;
119}
120if(seen!==null)return seen;
121}
122}catch(e){}
123// Cookiebot. Its object appears only once uc.js has run, so fall back to the
124// script tag it is installed as to tell "undecided" from "absent".
125try{
126var cb=window.Cookiebot||window.CookieConsent;
127if(cb&&cb.consent)return cb.consent.statistics?1:(cb.hasResponse?0:-1);
128if(document.getElementById("Cookiebot")||document.querySelector("script[src*='consent.cookiebot.com'],script[src*='consentcdn.cookiebot.com']"))return -1;
129}catch(e){}
130// OneTrust: statistics is the reserved C0002 group. OneTrust comma-pads the
131// list itself, so the delimited match cannot hit C00020. Every runtime signal
132// it exposes appears only after otSDKStub.js runs, so without the script-tag
133// fallback a blocked or async stub reads as "no CMP" and we would track — the
134// exact bug, on the pages most likely to care.
135try{
136var groups=window.OptanonActiveGroups||window.OnetrustActiveGroups;
137if(typeof groups==="string"&&groups)return groups.indexOf(",C0002,")>-1?1:0;
138if(window.OneTrust||window.Optanon||document.getElementById("onetrust-consent-sdk")||document.querySelector("script[src*='cookielaw.org'],script[src*='onetrust.com']"))return -1;
139}catch(e){}
140// CookieYes
141try{
142if(typeof window.getCkyConsent==="function"){
143var cky=window.getCkyConsent();
144if(cky&&cky.categories)return cky.categories.analytics?1:0;
145}
146if(document.querySelector("script[src*='cookieyes.com'],script[src*='cookie-script.com']"))return -1;
147}catch(e){}
148// Iubenda: purpose 4 is measurement.
149try{
150var iub=window._iub;
151if(iub&&iub.cs&&iub.cs.consent&&iub.cs.consent.purposes)return iub.cs.consent.purposes[4]?1:0;
152if(iub||document.querySelector("script[src*='iubenda.com']"))return -1;
153}catch(e){}
154return null;
155}
156
157var state=hpConsentState();
158if(state===null||state===1){start();return;}
159
160// CMP present, analytics not granted: write nothing, send nothing, and listen
161// for a later grant. The poll is a catch-all for CMPs whose event names we do
162// not know; it stops after 60s rather than running forever.
163function recheck(){if(hpConsentState()===1)start();}
164var EVENTS=["CookiebotOnAccept","CookiebotOnConsentReady","CookieConsentDeclaration","cookieyes-consent-update","consent.onetrust","OneTrustGroupsUpdated","iubenda-consent-given","hp-analytics-consent"];
165for(var evt=0;evt<EVENTS.length;evt++){try{window.addEventListener(EVENTS[evt],recheck,false);}catch(e){}}
166var polls=0;
167var timer=setInterval(function(){recheck();if(started||++polls>=120)clearInterval(timer);},500);
168}
169
170// Everything below mints an identifier or sends a beacon, so all of it waits
171// for consent. window.__hpupPushEvent is the exception: it binds synchronously
172// here and only forwards once consent arrives. Binding it late would let the
173// popup embed bundle's fallback shim (app/src/lib/popups/embed-bundle.ts,
174// registered under the same "if not already defined" test) claim the name first
175// and reroute pop-up events. Pre-consent calls are dropped, not queued — they
176// have no visitorId. SYNC with app/src/lib/analytics-tracking-script.ts.
177var hpPush=null;
178if(!window.__hpupPushEvent){window.__hpupPushEvent=function(t,p){try{if(hpPush)hpPush(t,p);}catch(e){}};}
179hpConsent(function(){
180
181function uuid(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(c){var r=Math.random()*16|0;return(c==="x"?r:(r&3|8)).toString(16)});}
182function getOrSet(store,key){try{var v=store.getItem(key);if(v)return v;v=uuid();store.setItem(key,v);return v;}catch(e){return uuid();}}
183
184var visitorId=getOrSet(localStorage,"_hp_vid");
185var sessionId=getOrSet(sessionStorage,"_hp_sid");
186
187var params=new URLSearchParams(location.search);
188var utmSource=params.get("utm_source")||undefined;
189var utmMedium=params.get("utm_medium")||undefined;
190var utmCampaign=params.get("utm_campaign")||undefined;
191// Ad-platform click IDs (conversion feedback loop) — captured like the UTMs.
192// SYNC with canonical at app/src/lib/analytics-tracking-script.ts.
193var gclid=params.get("gclid")||undefined;
194var fbclid=params.get("fbclid")||undefined;
195var msclkid=params.get("msclkid")||undefined;
196
197var vw=window.innerWidth||0;
198var vh=window.innerHeight||0;
199var ua=navigator.userAgent||"";
200var isMobileUA=/Android|iPhone|iPod|iPad|webOS|BlackBerry|IEMobile|Opera Mini/i.test(ua);
201var maxScreenDim=Math.max(screen.width||0,screen.height||0);
202var deviceType=isMobileUA?(maxScreenDim<=812||vw<768?"mobile":"tablet"):"desktop";
203
204function detectBrowser(){
205if(ua.indexOf("Edg/")>-1)return"Edge";
206if(ua.indexOf("Chrome/")>-1)return"Chrome";
207if(ua.indexOf("Safari/")>-1&&ua.indexOf("Chrome/")===-1)return"Safari";
208if(ua.indexOf("Firefox/")>-1)return"Firefox";
209return"Other";
210}
211function detectOS(){
212if(ua.indexOf("iPhone")>-1||ua.indexOf("iPad")>-1)return"iOS";
213if(ua.indexOf("Android")>-1)return"Android";
214if(ua.indexOf("Mac OS")>-1)return"macOS";
215if(ua.indexOf("Windows")>-1)return"Windows";
216if(ua.indexOf("Linux")>-1)return"Linux";
217return"Other";
218}
219var browserFamily=detectBrowser();
220var osFamily=detectOS();
221
222var buffer=[];
223function makeEvent(type,payload){
224return{pageId:PID,eventType:type,visitorId:visitorId,sessionId:sessionId,payload:payload||undefined,referrer:document.referrer||undefined,utmSource:utmSource,utmMedium:utmMedium,utmCampaign:utmCampaign,gclid:gclid,fbclid:fbclid,msclkid:msclkid,deviceType:deviceType,browserFamily:browserFamily,osFamily:osFamily,viewportWidth:vw,viewportHeight:vh,experimentId:EXP_ID||undefined,experimentVariant:EXP_VARIANT||undefined,timestamp:Date.now()};
225}
226function pushEvent(type,payload){buffer.push(makeEvent(type,payload));if(buffer.length>=FLUSH_MAX)flush();}
227
228// HP-1557 — expose a single-arg helper so the popups runtime (separate IIFE,
229// no shared closure with analytics) can feed events into the same buffer.
230// Defensive: only set if not already defined so a future second analytics
231// injection doesn't clobber an in-flight handler. HP-4568: the name is bound
232// above, outside the consent gate; this is where it starts forwarding.
233hpPush=pushEvent;
234// NOTE (HP-3461): this copy deliberately does NOT register window.__hpTrack, even
235// though the app copy registers both names. checkout-script.ts guards on
236// __hpTrack, and inject.ts appends that script to every edge-served page, so
237// adding the name here would start writing checkout_started for the entire edge
238// fleet (pubhtml.com + all custom domains) where that beacon has always been a
239// no-op — beginning an ecommerce event series mid-history, and moving the derived
240// checkout_abandoned with it, from a change about page engagement. It is a real
241// gap and wants its own change with a backfill story.
242// (No backticks anywhere in this file: it is one big template literal, so a
243// backtick in a comment terminates the string. This exact comment did it once.)
244
245function readMmuid(){try{var v=localStorage.getItem("mmuid");return v&&v.length<=128?v:undefined;}catch(e){return undefined;}}
246
247function flush(){
248if(buffer.length===0)return;
249var batch=buffer.splice(0,FLUSH_MAX);
250var payload={events:batch};
251var mmuid=readMmuid();
252if(mmuid)payload.mmuid=mmuid;
253try{navigator.sendBeacon(API,JSON.stringify(payload));}catch(e){}
254}
255
256// Engaged time-on-page (HP-3592) — count a second only while the tab is
257// visible AND the visitor was active in the last 30s; emit the accrued DELTA
258// every 30 engaged seconds and on hide/pagehide. Registered BEFORE the flush
259// handlers below so the final delta lands in the buffer before the last
260// beacon fires (listener registration order = firing order).
261// SYNC with canonical at app/src/lib/analytics-tracking-script.ts.
262var dwellSec=0,dwellSent=0,dwellLastActive=Date.now();
263var DWELL_IDLE_MS=30000,DWELL_EMIT_SEC=30,DWELL_CAP_SEC=1800;
264function dwellActive(){dwellLastActive=Date.now();}
265document.addEventListener("scroll",dwellActive,{passive:true});
266document.addEventListener("mousemove",dwellActive,{passive:true});
267document.addEventListener("keydown",dwellActive,true);
268document.addEventListener("touchstart",dwellActive,{passive:true});
269document.addEventListener("click",dwellActive,true);
270function dwellEmit(){var d=dwellSec-dwellSent;if(d>0){dwellSent=dwellSec;pushEvent("dwell",{seconds:d});}}
271var dwellTimer=setInterval(function(){
272if(document.visibilityState!=="visible")return;
273if(Date.now()-dwellLastActive>DWELL_IDLE_MS)return;
274dwellSec++;
275if(dwellSec-dwellSent>=DWELL_EMIT_SEC)dwellEmit();
276if(dwellSec>=DWELL_CAP_SEC){dwellEmit();clearInterval(dwellTimer);}
277},1000);
278document.addEventListener("visibilitychange",function(){if(document.visibilityState==="hidden")dwellEmit();});
279window.addEventListener("pagehide",dwellEmit);
280
281setInterval(flush,FLUSH_INTERVAL);
282document.addEventListener("visibilitychange",function(){if(document.visibilityState==="hidden")flush();});
283window.addEventListener("pagehide",flush);
284
285pushEvent("page_view");
286
287// On-platform conversion goal ("a thank-you page I own"): report the current
288// pageId plus the visitor's experiment assignments (_hp_exp_<id> cookies) so the
289// server can count this page-view as a conversion. Fires once per session per
290// page. HP-2925 Layer 1: must fire on EVERY page, not just experiment
291// participants — a standalone "Offsite event" goal has no experiment cookie, so
292// gating on cookies means the server never gets to attribute it. The server
293// no-ops cheaply when the page is neither an experiment nor a goal target.
294// SYNC with canonical at app/src/lib/analytics-tracking-script.ts.
295(function(){
296try{
297if(!APP_ORIGIN)return;
298var guard="_hp_goal:"+PID;
299if(sessionStorage.getItem(guard))return;
300sessionStorage.setItem(guard,"1");
301var exps=[];var ck=document.cookie.split(";");
302for(var i=0;i<ck.length;i++){var kv=ck[i].split("=");var k=(kv[0]||"").trim();if(k.indexOf("_hp_exp_")===0){exps.push({id:k.slice(8),variant:decodeURIComponent((kv[1]||"").trim())});}}
303var body=JSON.stringify({pageId:PID,visitorId:visitorId,sessionId:sessionId,experiments:exps});
304var url=APP_ORIGIN+"/api/experiments/onsite-conversion";
305if(navigator.sendBeacon){navigator.sendBeacon(url,new Blob([body],{type:"text/plain"}));}
306else{fetch(url,{method:"POST",headers:{"Content-Type":"application/json"},body:body,keepalive:true}).catch(function(){});}
307}catch(e){}
308})();
309
310// HP-3461 — milestones reached WITHOUT the visitor scrolling carry {auto:1}.
311// checkScroll runs on a 1s timer regardless of input and at scrollY=0 pct is
312// just winH/docH, so any page under ~4 screens tall banked scroll_depth:25 from
313// every visitor who did nothing (a page fitting the viewport banked 100). Those
314// rows mark the session engaged in the rollup, which pushed the bounce rate of
315// every short page toward zero and inflated avgScrollDepth the same way. The
316// events still fire (that depth WAS visible, and the scroll funnel wants it);
317// the stamp lets the bounce query demand a real interaction.
318// SYNC with app/src/lib/analytics-tracking-script.ts.
319var scrollMilestones={25:false,50:false,75:false,100:false};
320var scrollUserMoved=false;
321function checkScroll(){
322var docH=Math.max(document.body.scrollHeight,document.documentElement.scrollHeight);
323var scrollY=window.scrollY||window.pageYOffset;
324var winH=window.innerHeight;
325if(docH<=winH){if(!scrollMilestones[100]){scrollMilestones[100]=true;pushEvent("scroll_depth",{depth:100,auto:1});}return;}
326var pct=Math.round(((scrollY+winH)/docH)*100);
327[25,50,75,100].forEach(function(m){if(pct>=m&&!scrollMilestones[m]){scrollMilestones[m]=true;pushEvent("scroll_depth",scrollUserMoved?{depth:m}:{depth:m,auto:1});}});
328}
329window.addEventListener("scroll",function(){scrollUserMoved=true;checkScroll();},{passive:true});
330setTimeout(checkScroll,1000);
331
332// HP-1557 — when a click/submit happens inside a pop-up, add the popup's id +
333// name to the payload. Customers track conversions on the standard form_submit /
334// element_click types; this is just attribution metadata so the analytics surface
335// can say "this conversion came from the Welcome offer pop-up". Matches the
336// canonical data-persist-popup marker and the legacy data-htmlpub-popup one.
337function popupAttribution(node){
338var p=node&&node.closest&&node.closest("[data-persist-popup],[data-htmlpub-popup]");
339if(!p)return null;
340var nm=p.getAttribute("data-persist-popup-name");if(nm===null)nm=p.getAttribute("data-htmlpub-popup-name");
341return{popupId:p.getAttribute("id")||"",popupName:(nm||"").replace(/^\s+|\s+$/g,"")};
342}
343
344document.addEventListener("click",function(e){
345var el=e.target;
346if(!el||!el.tagName)return;
347var tag=el.tagName.toLowerCase();
348if(!(tag==="a"||tag==="button"||tag==="input"||tag==="select"||tag==="textarea"||el.getAttribute("role")==="button"||el.closest("a,button,[role=button]")))return;
349var docH=Math.max(document.body.scrollHeight,document.documentElement.scrollHeight,1);
350var rect=el.getBoundingClientRect();
351var payload={x:Math.min(1,Math.max(0,(rect.left+rect.width/2)/(document.documentElement.clientWidth||1))),y:Math.min(1,Math.max(0,(rect.top+rect.height/2+(window.scrollY||0))/docH)),selector:el.id?"#"+el.id:tag+(el.className&&typeof el.className==="string"?"."+el.className.trim().split(/\s+/).slice(0,2).join("."):""),viewportWidth:vw,viewportHeight:vh};
352// HP-3227 — toggling "count clicks as conversions" on a button stamps
353// data-lp-conversion="<configId>" on that element, and both the rollup worker
354// and the dashboard read match a click goal by that id. This tracker never
355// reported it, so a goal whose stored selector is the placeholder
356// a[data-lp-conversion] had nothing to match on and counted zero conversions
357// forever, no matter how many buttons were toggled on. Read from the closest
358// stamped ancestor, since the click target is often an inner <span> of the CTA.
359// SYNC with app/src/lib/analytics-tracking-script.ts.
360var lpConvEl=el.closest?el.closest("[data-lp-conversion]"):null;
361var lpConvId=lpConvEl?lpConvEl.getAttribute("data-lp-conversion"):null;
362if(lpConvId)payload.lpConversion=lpConvId;
363// HP-1557 — only attribute element_click to a pop-up when the actual
364// click target is a button-shaped CTA. A click on the email input
365// inside a lead-capture popup would otherwise inflate the popup's
366// "Button clicks" count on the dashboard.
367var isCtaClick=(tag==="a"||tag==="button"||el.getAttribute("role")==="button"||(el.closest&&el.closest("a,button,[role=button]")));
368if(isCtaClick){var pa=popupAttribution(el);if(pa){payload.popupId=pa.popupId;payload.popupName=pa.popupName;}}
369// HP-3461 — two NON-IDENTIFYING facts about the clicked anchor: the host it leaves
370// for (external only) and the file extension. The selector above records only "#id"
371// or "tag.class", so a PDF link and any other button were indistinguishable and no
372// past click can be reclassified. The full href is NOT stored: pathnames carry
373// customer identity and this is the highest-volume table (see absUrl).
374// SYNC with app/src/lib/analytics-tracking-script.ts.
375var hrefEl=tag==="a"?el:(el.closest&&el.closest("a[href]"));
376var clickUrl=hrefEl?absUrl(hrefEl):null;
377var clickFacts=clickUrl?linkFacts(clickUrl):null;
378if(clickFacts&&clickFacts.external)payload.linkHost=clickFacts.host;
379if(clickFacts&&clickFacts.ext)payload.linkExt=clickFacts.ext;
380var clickKey=clickUrl?linkClickKey(clickUrl):null;
381if(clickKey)payload.linkKey=clickKey;
382pushEvent("element_click",payload);
383// Classify into a micro-conversion. Emitted ALONGSIDE the element_click, never
384// instead of it, so a classification we later decide was wrong can be re-derived.
385if(hrefEl)classifyLinkClick(hrefEl,clickUrl);
386// HP-2811 — same-tab anchor navigation races the page unload before the 5s
387// flush timer; visibilitychange/pagehide flushes are unreliable (esp. iOS
388// Safari, same-tab nav), so element_click goals for outbound CTAs (e.g. a
389// HubSpot Meetings link) see 0 conversions despite real clicks. Mirror the
390// form_submit flush pattern for anchors that will navigate this tab.
391var navAnchor=tag==="a"?el:(el.closest&&el.closest("a[href]"));
392if(navAnchor&&navAnchor.getAttribute("target")!=="_blank"&&!e.ctrlKey&&!e.metaKey&&!e.shiftKey){
393var navHref=(navAnchor.getAttribute("href")||"").replace(/^\s+/,"");
394if(navHref&&navHref.charAt(0)!=="#"&&navHref.slice(0,11).toLowerCase()!=="javascript:")flush();
395}
396},true);
397
398document.addEventListener("submit",function(e){
399var f=e.target;
400if(!f||f.tagName!=="FORM")return;
401var payload={formIdentifier:f.id||f.getAttribute("name")||"form-"+([].indexOf.call(document.forms,f))};
402var pa=popupAttribution(f);
403if(pa){payload.popupId=pa.popupId;payload.popupName=pa.popupName;}
404pushEvent("form_submit",payload);
405flush();// send now — a thank-you-page redirect may navigate away before the 5s timer, dropping the experiment conversion
406},true);
407
408// ─── HP-3461 on-page micro-conversions ──────────────────────────────────────
409// Kinds + classification rules mirror app/src/lib/analytics/page-engagement-kinds.ts
410// (the server-side reference). Literal here because this compiles to an inline
411// string and cannot import; page-engagement-kinds.test.ts asserts both script
412// copies only ever emit registered kinds.
413// SYNC with app/src/lib/analytics-tracking-script.ts.
414
415// Resolve an anchor for RULE MATCHING only — the url itself is never sent. Only
416// host + extension are reported (linkFacts): customer delivery links routinely put
417// identity in the PATH (/thank-you/jane-doe, /download/lead/8842/Invoice-Jane-Doe.pdf),
418// and AnalyticsEvent is the highest-volume table with no redaction pass and no
419// consent gate, so a stored pathname is PII a DSAR erase cannot reach.
420function absUrl(a){
421try{
422var raw=(a.getAttribute("href")||"").replace(/^\s+/,"");
423if(!raw||raw.charAt(0)==="#"||raw.slice(0,11).toLowerCase()==="javascript:")return null;
424var u=new URL(raw,location.href);
425if(u.protocol!=="http:"&&u.protocol!=="https:")return null;
426return u;
427}catch(e){return null;}
428}
429// Host + extension, derived from the WHOLE url (never a truncated copy) so a long
430// signed URL cannot lose its extension to a length cap and read as plain navigation.
431function linkFacts(u){
432var p=u.pathname.toLowerCase();
433var dot=p.lastIndexOf(".");
434var ext=dot<0?"":p.slice(dot+1);
435if(ext.length>8)ext="";
436return{host:u.hostname.toLowerCase(),ext:ext,external:u.hostname!==location.hostname};
437}
438// Stable join key for WHICH link was clicked: FNV-1a hash of the canonicalized
439// URL (lowercase host, no fragment, no trailing slash, query kept minus our own
440// runtime decoration params). The raw href is still never stored (HP-3461) —
441// the dashboard re-derives each key from the page's OWN html and joins on the
442// hash (app/src/lib/analytics/link-click-key.ts, the reference implementation;
443// its test extracts this copy and asserts they agree).
444// SYNC with app/src/lib/analytics-tracking-script.ts and lib/analytics/link-click-key.ts.
445function linkClickKey(u){
446try{
447var c=new URL(u.toString());
448c.hash="";
449c.hostname=c.hostname.toLowerCase();
450if(c.pathname.length>1&&c.pathname.slice(-1)==="/")c.pathname=c.pathname.slice(0,-1);
451if(c.search){
452var strip=["lp_click_id","lp_page_id","lp_ts","_exp","_var","_vid","_pvar","client_reference_id"];
453var kept=new URLSearchParams();
454c.searchParams.forEach(function(v,k){if(strip.indexOf(k)<0)kept.append(k,v);});
455var q=kept.toString();
456c.search=q?"?"+q:"";
457}
458var t=c.toString();
459var h=0x811c9dc5;
460for(var i=0;i<t.length;i++){h^=t.charCodeAt(i);h=Math.imul(h,0x01000193);}
461var x=(h>>>0).toString(16);
462while(x.length<8)x="0"+x;
463return x;
464}catch(e){return null;}
465}
466
467var DL_EXT=["pdf","epub","zip","csv","xlsx","xls","docx","doc","pptx","ppt","mp3","wav"];
468// calendar.google.com is deliberately ABSENT: the standard add-to-calendar button
469// is not a booking, and host-only matching cannot tell them apart, so including it
470// reported bookings on pages where nobody booked. Host entries only — this copy
471// matches on hostname, so a host/path entry could never match here.
472var BOOK_HOSTS=["calendly.com","cal.com","acuityscheduling.com"];
473
474// Every kind counts at most once PER PAGE LOAD: three clicks on the same PDF is one
475// download, and a scrubbed video cannot re-bank a quartile.
476//
477// IN-MEMORY, and it must stay that way. This used to consult sessionStorage and
478// return true from its catch block, so wherever storage throws (sandboxed iframe,
479// "block all cookies", in-app webviews, full quota) every timeupdate re-emitted
480// three quartiles — 12+/sec, which trips the collect route's 30-req/min-per-IP
481// limit and then 429s that visitor's REAL page_view / form_submit / conversion
482// beacons. A session-scoped key also outlived the page load, so a batch lost to a
483// 429 or a closed tab could never be re-reported. The server counts DISTINCT
484// SESSIONS per kind, so duplicates across page views are deduped there anyway.
485var microFired={};
486function latch(kind,id){
487var k=kind+":"+(id||"");
488if(microFired[k])return false;
489microFired[k]=true;
490return true;
491}
492function microEvent(kind,id){
493if(!latch(kind,id))return;
494pushEvent("page_engagement",id?{kind:kind,id:id}:{kind:kind});
495}
496
497function classifyLinkClick(a,u){
498try{
499if(!u)return;
500var f=linkFacts(u);
501if((a.hasAttribute&&a.hasAttribute("download"))||(f.ext&&DL_EXT.indexOf(f.ext)>-1)){
502// id is the EXTENSION, not the file name: "Invoice-Jane-Doe.pdf" is as
503// identifying as the path we refuse to store, and phase 1 renders only an
504// aggregate count.
505microEvent("file_download",f.ext||undefined);
506return;
507}
508if(!f.external)return;
509for(var i=0;i<BOOK_HOSTS.length;i++){
510var b=BOOK_HOSTS[i];
511if(f.host===b||f.host.slice(-(b.length+1))==="."+b){microEvent("booking_open",b);return;}
512}
513}catch(e){}
514}
515
516// <video> only. YouTube/Vimeo/Loom/Wistia are cross-origin iframes whose
517// playback is invisible without the provider's JS API plus an embed-URL rewrite,
518// both deferred out of phase 1 — a page whose only video is an embed reports NO
519// video kinds, which the read side must treat as "not measurable", not as 0%.
520function videoId(v){
521try{
522if(v.id)return v.id.slice(0,128);
523var src=v.currentSrc||v.getAttribute("src")||"";
524if(!src)return undefined;
525var seg=src.split("?")[0].split("/");
526return (seg[seg.length-1]||"").slice(0,128)||undefined;
527}catch(e){return undefined;}
528}
529// Autoplay policy only lets a video start by itself while MUTED, so "muted and the
530// visitor has never touched the page" is the signature of decorative playback.
531// A <video autoplay muted loop playsinline> hero fires 'play' on every load, so
532// counting it would report "100% played the video" on exactly the pages most likely
533// to have a hero video. Unmuted playback always counts: it needs a gesture.
534// mousedown as well as pointerdown: a path delivering only mouse events would
535// otherwise never set this, silently disabling the feature with nothing in the logs.
536var userGestured=false;
537function markGesture(){userGestured=true;}
538document.addEventListener("pointerdown",markGesture,true);
539document.addEventListener("mousedown",markGesture,true);
540document.addEventListener("keydown",markGesture,true);
541document.addEventListener("touchstart",markGesture,{passive:true,capture:true});
542// Decide ONCE at 'play' and LATCH on the element. Re-checking the live flag on every
543// timeupdate was a real bug found in Chrome: a decorative muted autoplay correctly
544// suppressed video_start, then one unrelated trusted keystroke flipped userGestured
545// and every later timeupdate passed — banking p25/p50/p75 for a video nobody chose
546// to watch, with NO video_start, leaving the read side dividing by a zero
547// denominator. Unmuted playback always counts: it cannot start without a gesture.
548function decidePlay(v){
549var intent=(!v.muted)||userGestured;
550try{v.__hpIntent=intent?1:0;}catch(e){}
551return intent;
552}
553// Undefined = never saw this element's 'play' (script injected mid-playback): fail
554// closed, since crediting is the direction that corrupts the headline percentage.
555function countedPlay(v){return v.__hpIntent===1;}
556// Unmuting a decorative video IS intent, and no 'play' fires for it, so re-decide.
557// Emitting video_start keeps the stream coherent: a start precedes its quartiles.
558function onVideoVolume(e){
559var v=e.target;
560if(!v||v.tagName!=="VIDEO")return;
561if(v.muted||v.paused||v.__hpIntent===1)return;
562v.__hpIntent=1;
563microEvent("video_start",videoId(v));
564}
565function onVideoTime(e){
566var v=e.target;
567if(!v||v.tagName!=="VIDEO")return;
568// Consult the LATCHED decision, never the live flag — see decidePlay.
569if(!countedPlay(v))return;
570var d=v.duration;
571// Live streams / metadata not yet loaded report 0, NaN or Infinity — a quartile
572// of an unknown duration is meaningless, so report the play and nothing more.
573if(!isFinite(d)||d<=0)return;
574var pct=(v.currentTime/d)*100;
575var id=videoId(v);
576if(pct>=25)microEvent("video_p25",id);
577if(pct>=50)microEvent("video_p50",id);
578if(pct>=75)microEvent("video_p75",id);
579// p100 comes from 'ended', not a threshold: the last timeupdate can land a
580// fraction short of duration, so a full watch would never bank it.
581}
582document.addEventListener("play",function(e){var v=e.target;if(v&&v.tagName==="VIDEO"&&decidePlay(v))microEvent("video_start",videoId(v));},true);
583document.addEventListener("timeupdate",onVideoTime,true);
584document.addEventListener("volumechange",onVideoVolume,true);
585// SYNC-START embed-video-listeners (app/edge twins pinned by page-engagement-kinds.test.ts)
586// ─── Embedded video: Vimeo (HP-3461) ────────────────────────────────────────
587// <video> is handled above by reading the element directly. A Vimeo embed is a
588// CROSS-ORIGIN iframe, so its state is unreadable — but the player reports its own
589// progress over postMessage, and needs NO change to the embed URL. A plainly pasted
590// Vimeo embed becomes measurable as-is. (YouTube is different: it is silent unless
591// enablejsapi=1 is on the URL. Handled separately.)
592//
593// Verified against live players, and two details cost an attempt each:
594//   1. SUBSCRIBE ON THE PLAYER'S ready MESSAGE, not the iframe's load event.
595//      Subscribing on load races the player's own init: the addEventListener calls
596//      are silently dropped, no ACK comes back, and no timeupdate ever arrives — it
597//      looks exactly like "Vimeo does not work".
598//   2. ATTRIBUTE BY e.source, not player_id. Vimeo only echoes a player_id if the
599//      embed URL carries one, and a customer's pasted embed has none.
600//
601// Vimeo sends OBJECTS (YouTube sends JSON strings), and timeupdate carries
602// data.percent as a 0..1 fraction, so quartiles need no arithmetic of our own.
603function vimeoHostOk(origin){
604try{
605var h=new URL(origin).hostname.toLowerCase();
606return h==="player.vimeo.com"||h==="vimeo.com"||h.slice(-10)===".vimeo.com";
607}catch(e){return false;}
608}
609// Low-cardinality, non-identifying label: the numeric video id off the embed URL,
610// prefixed so it cannot collide with a <video> filename in the tally.
611function vimeoLabel(f){
612try{
613var m=(f.getAttribute("src")||"").match(/vimeo\.com\/video\/(\d+)/);
614return m?"vimeo:"+m[1]:"vimeo";
615}catch(e){return "vimeo";}
616}
617function vimeoFrameFor(src){
618try{
619var fr=document.querySelectorAll("iframe");
620for(var i=0;i<fr.length;i++){if(fr[i].contentWindow===src)return fr[i];}
621}catch(e){}
622return null;
623}
624function vimeoSubscribe(f){
625try{
626var w=f.contentWindow;
627var evs=["play","timeupdate","ended"];
628for(var i=0;i<evs.length;i++)w.postMessage({method:"addEventListener",value:evs[i]},"*");
629}catch(e){}
630}
631window.addEventListener("message",function(e){
632try{
633if(!vimeoHostOk(e.origin))return;
634var d=e.data;
635if(!d||typeof d!=="object")return;
636var f=vimeoFrameFor(e.source);
637if(!f)return;
638if(d.event==="ready"){vimeoSubscribe(f);return;}
639var id=vimeoLabel(f);
640if(d.event==="play"){
641// Same intent rule as <video>, minus the muted check we cannot read through an
642// iframe: browser policy only permits unprompted autoplay while MUTED, so
643// "playback began before the visitor touched anything" is the decorative case.
644if(!userGestured)return;
645f.__hpEmbedCounted=1;
646microEvent("video_start",id);
647return;
648}
649if(f.__hpEmbedCounted!==1)return;
650if(d.event==="timeupdate"){
651var pct=d.data&&typeof d.data.percent==="number"?d.data.percent*100:-1;
652if(pct<0)return;
653if(pct>=25)microEvent("video_p25",id);
654if(pct>=50)microEvent("video_p50",id);
655if(pct>=75)microEvent("video_p75",id);
656// p100 comes from 'ended', as with <video>: the last timeupdate lands short.
657}else if(d.event==="ended"){
658microEvent("video_p25",id);microEvent("video_p50",id);microEvent("video_p75",id);microEvent("video_p100",id);
659}
660}catch(err){}
661});
662
663// ─── Embedded video: YouTube (HP-3461) ──────────────────────────────────────
664// YouTube is SILENT unless the embed URL carries enablejsapi=1 — verified live: a
665// plain embed answers nothing, even if we send it the handshake. So this listener
666// only hears players whose URL has that flag; adding it to customer HTML at serve
667// time is a separate commit, deliberately, because that mutates what visitors load.
668//
669// With the flag, the protocol is: post a listening message, then the player streams
670// infoDelivery with playerState, currentTime and duration.
671//
672// The handshake repeats on a timer, and the honest reason is that it catches frames
673// that appear after load (a tab, an accordion, a script-mounted embed). Whether a
674// player also STOPS streaming when we stop asking was not established: repeats are
675// answered "alreadyInitialized", which suggests the first one is what arms it. The
676// polling stays because the version with polling is the one verified to work end to
677// end, and trading that for an unverified optimisation would risk silently losing
678// every embed metric. The steady-state cost is one postMessage per api-enabled
679// frame every 2s.
680//
681// Unlike Vimeo, YouTube sends JSON STRINGS, so the payload needs parsing before it
682// looks like anything.
683//
684// playerState is YouTube's enum: -1 unstarted, 0 ended, 1 playing, 2 paused,
685// 3 buffering, 5 cued.
686function ytHostOk(origin){
687try{
688var h=new URL(origin).hostname.toLowerCase();
689return h==="www.youtube.com"||h==="youtube.com"||h==="www.youtube-nocookie.com"||h==="youtube-nocookie.com";
690}catch(e){return false;}
691}
692function ytLabel(f){
693try{
694var m=(f.getAttribute("src")||"").match(/\/embed\/([A-Za-z0-9_-]{11})/);
695return m?"youtube:"+m[1]:"youtube";
696}catch(e){return "youtube";}
697}
698function ytFrameFor(src){
699try{
700var fr=document.querySelectorAll("iframe");
701for(var i=0;i<fr.length;
701i++){if(fr[i].contentWindow===src)return fr[i];}
702}catch(e){}
703return null;
704}
705// Ask every api-enabled YouTube frame to start reporting, and keep asking. Cheap: a
706// postMessage to a frame that is not a YouTube player is simply ignored.
707function ytListen(){
708try{
709var fr=document.querySelectorAll('iframe[src*="enablejsapi=1"]');
710for(var i=0;i<fr.length;i++){
711try{fr[i].contentWindow.postMessage('{"event":"listening","id":"hp","channel":"widget"}',"*");}catch(e){}
712}
713}catch(e){}
714}
715setTimeout(ytListen,1200);
716setInterval(ytListen,2000);
717window.addEventListener("message",function(e){
718try{
719if(!ytHostOk(e.origin))return;
720var d=e.data;
721// YouTube sends strings; anything else is not its API talking.
722if(typeof d!=="string")return;
723try{d=JSON.parse(d);}catch(x){return;}
724if(!d||typeof d!=="object"||d.event!=="infoDelivery")return;
725var info=d.info;
726if(!info||typeof info!=="object")return;
727var f=ytFrameFor(e.source);
728if(!f)return;
729var id=ytLabel(f);
730var state=info.playerState;
731// Same intent rule as Vimeo and <video>, but YouTube hands us no discrete 'play'
732// event: infoDelivery repeats playerState 1 for the WHOLE of playback. So the
733// decision is made on the TRANSITION into playing and latched there, never
734// re-read. Re-reading it is precisely the bug decidePlay documents, and it is
735// reachable here in a way it is not for <video>: a muted autoplay is correctly
736// suppressed, then one unrelated trusted click (the form, alt-tab, a screenshot)
737// flips userGestured and the next state-1 update promotes a video nobody chose
738// into a counted play, quartiles and all. Latching on the transition also keeps
739// the <video> semantics that a chosen REPLAY after a decorative autoplay counts,
740// because that arrives as 2 then 1.
741var prev=f.__hpYtPrev;
742f.__hpYtPrev=state;
743if(state===1&&prev!==1){
744f.__hpEmbedCounted=userGestured?1:0;
745if(f.__hpEmbedCounted===1)microEvent("video_start",id);
746}
747if(f.__hpEmbedCounted!==1)return;
748if(state===0){
749// Ended implies every quartile, even if a throttled tab skipped an update.
750microEvent("video_p25",id);microEvent("video_p50",id);microEvent("video_p75",id);microEvent("video_p100",id);
751return;
752}
753var dur=info.duration,cur=info.currentTime;
754// A live stream reports duration 0, and a quartile of an unknown length is
755// meaningless — report the play and nothing more.
756if(typeof dur!=="number"||typeof cur!=="number"||!isFinite(dur)||dur<=0)return;
757var pct=(cur/dur)*100;
758if(pct>=25)microEvent("video_p25",id);
759if(pct>=50)microEvent("video_p50",id);
760if(pct>=75)microEvent("video_p75",id);
761// p100 comes from playerState 0, not a threshold: the final update lands short.
762}catch(err){}
763});
764// SYNC-END embed-video-listeners
765document.addEventListener("ended",function(e){
766var v=e.target;
767if(!v||v.tagName!=="VIDEO")return;
768if(!countedPlay(v))return;
769var id=videoId(v);
770// Watched to the end implies every quartile, even if a throttled background tab
771// skipped a timeupdate.
772microEvent("video_p25",id);microEvent("video_p50",id);microEvent("video_p75",id);microEvent("video_p100",id);
773},true);
774
775// ─── Scheduler widgets (HP-3461 phase 3) ────────────────────────────────────
776// An inline Calendly/Cal.com embed is a CROSS-ORIGIN iframe, so we cannot see a
777// click inside it — which is why booking_open (a click on an outbound scheduling
778// link) is absent on exactly the pages most likely to take bookings. The widget
779// broadcasts its own progress to the parent window, so we get told instead of
780// guessing. No SDK needed; Calendly posts these regardless.
781//
782// SECURITY: any frame can post a message claiming to be a booking, and this ships
783// on every published page, so the sender's ORIGIN must be a recognised scheduling
784// host AND the message must match the provider's shape. Everything else is ignored.
785// PRIVACY: only WHICH STEP is read. event.data.payload (invitee details on
786// event_scheduled) is never touched.
787// SYNC with app/src/lib/analytics-tracking-script.ts.
788function schedulerOrigin(origin){
789try{
790var h=new URL(origin).hostname.toLowerCase();
791for(var i=0;i<BOOK_HOSTS.length;i++){
792var b=BOOK_HOSTS[i];
793if(h===b||h.slice(-(b.length+1))==="."+b)return b;
794}
795}catch(e){}
796return null;
797}
798// calendly.profile_page_viewed is deliberately ignored: it fires for a profile
799// listing rather than a bookable calendar, so it would inflate the funnel's top.
800window.addEventListener("message",function(e){
801try{
802var host=schedulerOrigin(e.origin);
803if(!host)return;
804var d=e.data;
805if(!d||typeof d!=="object")return;
806// Calendly's vocabulary reads off d.event and is prefixed; Cal.com's reads off
807// d.type and is not. Verified against live widgets on both, plus Cal.com's own
808// EventDataMap in packages/embeds/embed-core/src/sdk-action-manager.ts.
809// calendly.profile_page_viewed is ignored: it fires for a profile listing rather
810// than a bookable calendar, so it would inflate the top of the funnel.
811//
812// Cal.com HAS NO time-selection event. Its full enum (27 keys) contains nothing for
813// picking a date or slot -- availabilityLoaded and slotsFetched are the widget
814// LOADING options, not a visitor choosing one. So Cal.com reports 2 of the 3 steps,
815// and booking_time_selected is left ABSENT for it rather than synthesised from the
816// booking. Fabricating it would make every Cal.com page show a flat 100% from
817// "selected" to "completed" -- a number that looks measured and is not. Absent
818// already means "not measurable here" everywhere else in this feature (see the
819// video quartiles), and the read side honours that.
820//
821// Emissions are LITERALS, one branch per step, so the sync seam in
822// page-engagement-kinds.test.ts can see them -- it caught booking_completed going
823// unverifiable when this was table-driven.
824var name=typeof d.event==="string"?d.event:(typeof d.type==="string"?d.type:null);
825if(!name)return;
826if(host==="calendly.com"){
827if(name==="calendly.event_scheduled"){
828microEvent("booking_viewed",host);
829microEvent("booking_time_selected",host);
830microEvent("booking_completed",host);
831// The booking is the outcome this exists to catch and the visitor usually leaves
832// for the confirmation, so flush rather than wait out the 5s timer.
833flush();
834}else if(name==="calendly.date_and_time_selected"){
835microEvent("booking_viewed",host);
836microEvent("booking_time_selected",host);
837}else if(name==="calendly.event_type_viewed"){
838microEvent("booking_viewed",host);
839}
840}else if(host==="cal.com"){
841// bookingSuccessfulV2 deprecates bookingSuccessful and both are still in the enum,
842// so accept either -- customers sit on different embed versions.
843if(name==="bookingSuccessfulV2"||name==="bookingSuccessful"){
844// NOTE: no booking_time_selected here. Cal.com cannot report it.
845microEvent("booking_viewed",host);
846microEvent("booking_completed",host);
847flush();
848}else if(name==="bookerViewed"){
849microEvent("booking_viewed",host);
850}
851}
852// No acuityscheduling.com branch, and there never can be one. Its embed script
853// exchanges only sizing/load/scrollTo/scrollBy/gaMeasurementId with the parent and
854// takes a Google Analytics client id INWARD — there is no booking message to hear.
855// Acuity reports outward only through its webhooks (HP-4041's inbound conversion
856// webhook is the route). A branch here would be dead code. See lib/scheduling-hosts.
857// Its origin stays accepted by the gate above, which costs nothing: an origin that
858// never speaks simply never matches.
859}catch(err){}
860});
861
862// Consent usually arrives seconds after load, so a bare load listener would
863// never fire and rum_timing would vanish from every CMP-gated page. The
864// navigation/paint entries report the same absolute numbers whenever read.
865function hpRumTiming(){
866setTimeout(function(){
867try{
868var perf=performance.getEntriesByType("navigation")[0];
869if(perf){
870var ev=makeEvent("rum_timing",{loadTimeMs:Math.round(perf.loadEventEnd-perf.startTime),ttfbMs:Math.round(perf.responseStart-perf.startTime),fcpMs:undefined});
871var paintEntries=performance.getEntriesByType("paint");
872for(var i=0;i<paintEntries.length;i++){if(paintEntries[i].name==="first-contentful-paint"){ev.payload.fcpMs=Math.round(paintEntries[i].startTime);break;}}
873buffer.push(ev);
874}
875}catch(e){}
876},2000);
877}
878if(document.readyState==="complete")hpRumTiming();else window.addEventListener("load",hpRumTiming);
879});
880})();
881</script>
881<script data-page-id="rm90klif4ys3o04jw9zgjfpw" data-page-slug="site-index" data-api-url="https://htmlpub.com" data-turnstile-key="0x4AAAAAAD7Sp6X7R_qRx-Cz" data-htmlpub-forms>
882(function(){
883var s=document.querySelector("script[data-htmlpub-forms]");
884var pageId=s.getAttribute("data-page-id");
885var slug=s.getAttribute("data-page-slug");
886var api=s.getAttribute("data-api-url")+"/api/forms/submit";
887// Owner-mode: editor previews and the in-app shell load this script with
888// data-owner="1". We still listen for submits (the editor surfaces success
889// state to the user via the toolbar), but we MUST NOT fire conversion
890// pixels — that would pollute the customer's Meta / TikTok pixel data
891// with editor test traffic indistinguishable from real visitors.
892var _hpIsOwner=s.getAttribute("data-owner")==="1";
893// Preview-mode: the editor's Preview loads owner-gated (draft access + no pixel
894// pollution) but must show the true visitor experience. _hpIsPreview lets visitor-UX
895// blocks (multi-step paging) run even though _hpIsOwner is set; pixel/submission gates
896// stay owner-only.
897var _hpIsPreview=s.getAttribute("data-preview")==="1";
898var _hpAutomaticConversionEventsEnabled=s.getAttribute("data-automatic-conversion-events")!=="0";
899var _hpTurnstileKey=s.getAttribute("data-turnstile-key")||"";
900// data-persist-hidden: fields hidden via the inspector — hidden from visitors on
901// the live page but still submitted (attribution/UTM). The edit canvas (owner, NOT
902// preview) skips the rule so the inspector keeps them visible with a "Hidden" chip;
903// Preview applies it so it matches the published page.
904if(!_hpIsOwner||_hpIsPreview){try{var _hpHideStyle=document.createElement("style");_hpHideStyle.setAttribute("data-htmlpub-forms-hide","");_hpHideStyle.textContent="[data-persist-hidden]{display:none!important}";(document.head||document.documentElement).appendChild(_hpHideStyle);}catch(e){}}
905// Multiple-choice radio/checkbox width normalization is NOT injected at runtime.
906// The editor bakes an inline width:auto onto each option input (idempotently), so the
907// fix persists in the saved HTML and follows the normal draft -> publish flow instead
908// of overriding every served form's checkboxes/radios on every page load.
909// A field hidden from visitors (data-persist-hidden -> display:none) that ALSO carries native
910// required cannot be focused, so on a single-step form native constraint validation would
911// silently abort submit on it (the multi-step path already skips hidden fields via
912// _hpValidatable). Drop the required attribute from hidden controls on the served page so
913// submission is never blocked by a field the visitor can't see; the field still submits its value.
914if(!_hpIsOwner||_hpIsPreview){try{var _hpHidReq=document.querySelectorAll("[data-persist-hidden][required],[data-persist-hidden] [required]");for(var _hphr=0;_hphr<_hpHidReq.length;_hphr++)_hpHidReq[_hphr].removeAttribute("required");}catch(e){}}
915// The form's real submit button — never one of the authored multi-step nav buttons
916// (data-persist-ms-back/next), which may be type-less if hand-authored.
917function _hpFindSubmit(form){return form.querySelector("[type=submit]:not([data-persist-ms-back]):not([data-persist-ms-next]),button[type=submit]:not([data-persist-ms-back]):not([data-persist-ms-next]),button:not([type]):not([data-persist-ms-back]):not([data-persist-ms-next])");}
918// A checkbox group marked required-as-a-group (data-persist-group-required) is satisfied when
919// AT LEAST ONE member is checked — native required on each box would wrongly demand ALL.
920// Returns true if every such group within the given scope is satisfied; otherwise reports validity on
921// the first empty group (clearing on the next change) and returns false.
922function _hpGroupReqOK(scope){
923var boxes=scope.querySelectorAll('input[type="checkbox"][data-persist-group-required]');
924var groups={};
925for(var i=0;i<boxes.length;i++){var n=boxes[i].name||("__hp"+i);if(!groups[n])groups[n]=[];groups[n].push(boxes[i]);}
926for(var nm in groups){if(!Object.prototype.hasOwnProperty.call(groups,nm))continue;
927var g=groups[nm],any=false,first=null;
928for(var j=0;j<g.length;j++){if(g[j].closest&&g[j].closest("[data-persist-hidden]"))continue;if(!first)first=g[j];if(g[j].checked)any=true;}
929if(first&&!any){try{first.setCustomValidity("Please select at least one option.");
929if(first.reportValidity)first.reportValidity();var _clr=function(){try{first.setCustomValidity("");}catch(e){}for(var k=0;k<g.length;k++)g[k].removeEventListener("change",_clr);};for(var k2=0;k2<g.length;k2++)g[k2].addEventListener("change",_clr);}catch(e){}return false;}}
930return true;
931}
932// Pick a Turnstile theme (light/dark) from the first opaque background behind the form, so the
933// widget matches the PAGE rather than defaulting to the visitor's system theme ("auto"). Parses
934// the rgb() string without a regex escape (template-literal safe). Falls back to "auto".
935function _hpPickTheme(el){
936try{
937var n=el;
938while(n&&n.nodeType===1){
939var bg=window.getComputedStyle(n).backgroundColor||"";
940if(bg.indexOf("rgb")===0){
941var p=bg.replace(/[^0-9.,]/g,"").split(",");
942var a=p.length>3?parseFloat(p[3]):1;
943if(a>0.1){var lum=(0.299*+p[0]+0.587*+p[1]+0.114*+p[2])/255;return lum>0.5?"light":"dark";}
944}
945n=n.parentElement;
946}
947}catch(e){}
948return "auto";
949}
950// Inject the Turnstile widget on a spam-protected form via the platform-origin bridge iframe.
951// The widget CANNOT render inline on a customer's custom domain: Turnstile binds a widget to a
952// hostname allowlist (Enterprise caps at 200, no wildcards), so customer domains can't be listed.
953// Instead we embed /turnstile-bridge served from data-api-url (a platform origin that IS on the
954// allowlist); it renders the widget there and postMessages the token back (see the listener below).
955// Placed ABOVE the submit's outermost row so on a multi-step footer nav it sits on its own line.
956// Starts 0px (interaction-only is invisible); the bridge posts a resize only if a challenge appears.
957// Idempotent; no-op without the feature flag or inside a sandboxed iframe (the /p shell renders its
958// own widget on the app origin).
959function _hpInjectCaptcha(f){
960if(!_hpTurnstileKey||inSandbox||!f||f.querySelector("iframe[data-hp-turnstile]"))return;
961var base=(s.getAttribute("data-api-url")||"");
962if(base&&base.charAt(base.length-1)==="/")base=base.slice(0,-1);
963if(!base)return;
964var fr=document.createElement("iframe");
965fr.setAttribute("data-hp-turnstile","");
966fr.setAttribute("title","Spam protection");
967fr.setAttribute("scrolling","no");
968fr.style.cssText="width:100%;max-width:320px;height:0;border:0;display:block;overflow:hidden;";
969fr.src=base+"/turnstile-bridge?theme="+encodeURIComponent(_hpPickTheme(f));
970var sb=_hpFindSubmit(f);
971if(sb){var row=sb;while(row.parentNode&&row.parentNode!==f)row=row.parentNode;if(row.parentNode===f)f.insertBefore(fr,row);else if(sb.parentNode)sb.parentNode.insertBefore(fr,sb);else f.appendChild(fr);}else f.appendChild(fr);
972}
973// Receive the Turnstile token from the platform-origin bridge iframe. Origin-checked against
974// data-api-url. Stores the token on the owning form for the submit path; resizes the iframe when
975// Managed mode shows an actual challenge (invisible/auto-pass stays 0px). One listener serves
976// every captcha form on the page.
977(function(){
978var _hpBO="";try{_hpBO=new URL(s.getAttribute("data-api-url")||"",location.href).origin;}catch(e){}
979window.addEventListener("message",function(e){
980if(!_hpBO||e.origin!==_hpBO)return;
981var d=e.data;if(!d||d.source!=="hp-turnstile")return;
982var frs=document.querySelectorAll("iframe[data-hp-turnstile]"),host=null;
983for(var i=0;i<frs.length;i++){if(frs[i].contentWindow===e.source){host=frs[i];break;}}
984if(!host)return;
985var form=host.closest?host.closest("form"):null;
986if(d.type==="token"){if(form){form.__hpCaptchaToken=(d.token||"");form.__hpCaptchaError=false;}}
987else if(d.type==="expired"){if(form)form.__hpCaptchaToken="";}
988// A terminal bridge error (misconfigured/unreachable widget, keys mismatch) means no token can
989// ever be produced; flag it so the submit gate fails OPEN rather than trapping the visitor forever
990// (the server still fails open for the same unconfigured case). Distinct from expired (retr
990yable).
991else if(d.type==="error"){if(form){form.__hpCaptchaToken="";form.__hpCaptchaError=true;}}
992else if(d.type==="resize"){var h=+d.height||0;host.style.height=(h>0?h:0)+"px";host.style.margin=(h>0?"12px 0":"0");}
993});
994})();
995// Bounded wait for a Turnstile token, with a single re-challenge (decided fail policy #1/#2).
996// The bridge delivers the token via postMessage, which can land slightly AFTER the visitor
997// clicks submit (invisible challenge still resolving). Give it a bounded window; if none
998// arrives, force ONE fresh challenge (reload the bridge iframe) and wait a final window; if
999// STILL none, proceed anyway — the server accepts the lead and flags it unverified rather
1000// than dropping it. done() runs exactly once: as soon as a token appears, the bridge reports
1001// a terminal error, or the budget is spent.
1002function _hpWaitForCaptcha(f,done){
1003var WAIT=3000,STEP=100,called=false;
1004function finish(){if(called)return;called=true;done();}
1005function poll(deadline,onExpire){
1006if((f.__hpCaptchaToken||"")!==""||f.__hpCaptchaError){finish();return;}
1007if(Date.now()>=deadline){onExpire();return;}
1008setTimeout(function(){poll(deadline,onExpire);},STEP);
1009}
1010poll(Date.now()+WAIT,function(){
1011if((f.__hpCaptchaToken||"")!==""||f.__hpCaptchaError){finish();return;}
1012try{var fr=f.querySelector("iframe[data-hp-turnstile]");if(fr)fr.src=fr.src;}catch(e){}
1013poll(Date.now()+WAIT,finish);
1014});
1015}
1016// reCAPTCHA enforcement (HP-1475): only pages whose owner opted in emit this
1017// flag. It gates the client-side submit block below so pages that merely carry
1018// a decorative widget (the pre-HP-1475 status quo) keep working unchanged.
1019var _hpRecaptchaEnforced=s.getAttribute("data-recaptcha-enforced")==="1";
1020var corrId=0;
1021var pendingCallbacks={};
1022
1023// Detect if we're in a sandboxed iframe
1024var inSandbox=false;
1025try{inSandbox=window!==window.parent;}catch(e){inSandbox=true;}
1026
1027function cleanKey(s){return s.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_|_$/g,"").substring(0,30);}
1028// SYNC: mirror worker-pages/src/scripts/forms.ts and app/src/lib/extract-form-fields.ts
1029function looksLikeRawId(s){return /^[a-f0-9]{32}$/i.test(s)||/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(s)||/^[a-f0-9]{32}-[A-Za-z0-9_-]{20,40}$/.test(s);}
1030function fieldKey(el,idx){
1031if(el.name&&!looksLikeRawId(el.name))return el.name;
1032if(el.id&&!looksLikeRawId(el.id))return el.id;
1033var df=el.getAttribute("data-field")||el.getAttribute("data-name");
1034if(df&&!looksLikeRawId(df))return df;
1035// Type-based semantic hint. <input type="email"> without an explicit name
1036// should still be identified as "email" so the lead-capture extractor can
1037// find the address. Same for tel/url. Critical when the page builder
1038// (HTMLPub editor / Sacca's offers.leadpages.com pages) emits inputs
1039// without name attributes — without this, the email field gets keyed off
1040// its placeholder ("alex_at_youragency_com") and no lead is extracted.
1041if(el.type==="email")return "email";
1042if(el.type==="tel")return "phone";
1043if(el.type==="url")return "url";
1044// Sibling label pattern: <label>Foo</label><input ...> inside a wrapper.
1045// Common in modern form layouts. The existing closest(label) check below
1046// only catches WRAPPING labels, not sibling ones.
1047var prev=el.previousElementSibling;
1048if(prev&&prev.tagName==="LABEL"&&prev.textContent){var pk=cleanKey(prev.textContent);if(pk)return pk;}
1049if(el.placeholder){var ck=cleanKey(el.placeholder);if(ck)return ck;}
1050var al=el.getAttribute("aria-label");
1051if(al){var ck2=cleanKey(al);if(ck2)return ck2;}
1052if(el.id){var lbl=document.querySelector('label[for="'+el.id+'"]');if(lbl&&lbl.textContent){var ck3=cleanKey(lbl.textContent);if(ck3)return ck3;}}
1053var par=el.closest?el.closest("label"):null;
1054if(par&&par.textContent){var ck4=cleanKey(par.textContent);if(ck4)return ck4;}
1055return (el.type||"field")+"_"+idx;
1056}
1057// Known third-party tracking field patterns to ignore
1058var _blocklist=/^(lp[_s]?plan|ub[_s]?plan|gtrans|__hstc|__hssc|__hsfp|hutk|_mkto_trk|_biz_uid|_biz_nA|_biz_pendingA|_biz_flagsA|mkt_tok|li_fat_id|_ga|_gid|_fbp|_fbc|_uetsid|_uetvid|mc_cid|mc_eid|_drip_client_|__ss|__ss_referrer|ss_campaign|ss_source|ss_cid|siq_|sq_|_uc_|tl|sl|vote|query|_hp_vid|g-recaptcha-response)$/i;
1059function _isInjected(el){
1060// Skip hidden inputs not present in the original page HTML.
1061// Third-party scripts (Customers.ai, HubSpot, etc.) inject hidden fields
1062// at runtime. We detect these by checking if they were present at DOMContentLoaded.
1063if(el.type==="hidden"&&!el.hasAttribute("data-hp-original"))return true;
1064return false;
1065}
1066function collect(f){
1067var d={};
1068var els=f.elements||f.querySelectorAll("input,textarea,select");
1069var used={};
1070var hasVisibleValue=false;
1071for(var i=0;i<els.length;i++){
1072var el=els[i];
1073if(el.type==="file"||el.type==="submit"||el.type==="button")continue;
1074if((el.type==="checkbox"||el.type==="radio")&&!el.checked)continue;
1075if(_isInjected(el))continue;
1076// International-phone controls: the visible national input (data-hp-phone-nat) and
1077// the injected country <select> (data-hp-phone-cc) are NOT collected — the hidden
1078// E.164 companion carries the value under the field's name. The national input still
1079// counts toward hasVisibleValue so a phone-only form isn't treated as empty.
1080if(el.hasAttribute&&el.hasAttribute("data-hp-phone-nat")){if(el.value&&el.value.trim())hasVisibleValue=true;continue;}
1081if(el.hasAttribute&&el.hasAttribute("data-hp-phone-cc"))continue;
1082// Empty URL-param inputs are dropped from the payload so direct-traffic
1083// submissions from a form with data-persist-capture-utms (or hand-authored
1084// data-persist-url-param inputs) don't ship 7 blank attribution columns
1085// into every submission, integration webhook, CSV export, and CAPI event.
1086// Author-provided value="direct" (or any non-empty default) still ships.
1087if(el.type==="hidden"&&!(el.value||"").trim()&&el.hasAttribute("data-persist-url-param"))continue;
1088var key=fieldKey(el,i);
1089if(_blocklist.test(key))continue;
1090if(used[key]){key=key+"_"+i;}
1091used[key]=true;
1092d[key]=el.value;
1093if(el.type!=="hidden"&&el.value&&el.value.trim())hasVisibleValue=true;
1094}
1095// Require at least one visible field with a value — pure hidden-field submissions are noise
1096if(!hasVisibleValue)return {};
1097return d;
1098}
1099
1100// Generate a UUID v4 (browser-side). Used as the eventID that ties the
1101// fbq/ttq browser event to the matching CAPI event the server sends via
1102// the integration queue → provider workflow, so pixel platforms can dedup.
1103function _hpUuid(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(c){var r=Math.random()*16|0;return(c==="x"?r:(r&3|8)).toString(16);});}
1104
1105// Fire the canonical "Lead" event on every installed pixel platform. Only
1106// platforms that booted (window.fbq, window.ttq, window.gtag, window.dataLayer)
1107// receive a call — others no-op cleanly. The eventID is threaded into the
1108// provider-specific dedup parameter (Meta: eventID, TikTok: event_id).
1109function _hpFireLead(formIdent,data,eventId){
1110  if(_hpIsOwner||!_hpAutomaticConversionEventsEnabled)return;
1111  var em=null,ph=null,fn=null,ln=null;
1112  for(var k in data){
1113    if(!Object.prototype.hasOwnProperty.call(data,k))continue;
1114    var v=(data[k]||"").toString().trim();
1115    if(!v)continue;
1116    var kl=k.toLowerCase();
1117    if(!em&&/email/.test(kl)&&/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v))em=v;
1118    else if(!ph&&/phone|tel|mobile/.test(kl))ph=v;
1119    else if(!fn&&/first.?name|fname/.test(kl))fn=v;
1120    else if(!ln&&/last.?name|lname/.test(kl))ln=v;
1121  }
1122  var custom={};if(em)custom.em=em;if(ph)custom.ph=ph;if(fn)custom.fn=fn;if(ln)custom.ln=ln;
1123  try{if(window.fbq)window.fbq("track","Lead",custom,{eventID:eventId});}catch(e){}
1124  try{if(window.ttq&&window.ttq.track)window.ttq.track("SubmitForm",custom,{event_id:eventId});}catch(e){}
1125  try{if(window.gtag)window.gtag("event","generate_lead",{event_id:eventId,lp_form_id:formIdent});}catch(e){}
1126  try{if(window.dataLayer&&window.dataLayer.push)window.dataLayer.push({event:"lp_form_submit",lp_event_id:eventId,lp_form_id:formIdent});}catch(e){}
1127}
1128
1129// First-interaction hook: when the visitor focuses any form field for the
1130// first time on this page, fire the platform's "started a form" event.
1131// Once per form per page (debounced via _hpFiredInitFocus).
1132var _hpFiredInitFocus={};
1133function _hpFireInitiateCheckout(formIdent){
1134  if(_hpIsOwner||!_hpAutomaticConversionEventsEnabled)return;
1135  if(_hpFiredInitFocus[formIdent])return;
1136  _hpFiredInitFocus[formIdent]=true;
1137  var eid=_hpUuid();
1138  try{if(window.fbq)window.fbq("track","InitiateCheckout",{}
1138,{eventID:eid});}catch(e){}
1139  try{if(window.ttq&&window.ttq.track)window.ttq.track("InitiateCheckout",{},{event_id:eid});}catch(e){}
1140  try{if(window.gtag)window.gtag("event","form_start",{event_id:eid,lp_form_id:formIdent});}catch(e){}
1141  try{if(window.dataLayer&&window.dataLayer.push)window.dataLayer.push({event:"lp_form_start",lp_event_id:eid,lp_form_id:formIdent});}catch(e){}
1142}
1143// HP-3461 — the same first-interaction moment, reported to OUR analytics. It has
1144// been detected here all along and told to Facebook, TikTok, Google and GTM
1145// (above) while our own funnel never heard about it, so "169 started the form, 17
1146// finished" was unanswerable from first-party data.
1147//
1148// Deliberately NOT inside _hpFireInitiateCheckout: that is gated on
1149// _hpAutomaticConversionEventsEnabled, the customer's AD-PIXEL preference, and
1150// switching off automatic ad conversion events must not blind our own analytics.
1151//
1152// Requires a real visitor gesture. A focusin alone is not evidence of intent:
1153// <input autofocus>, a popup that focuses its email field on open, and browser
1154// autofill all fire it with nobody touching the keyboard, which would report a
1155// form_start for ~every session and make the abandonment number this feature
1156// exists to answer ("169 started, 17 finished") read as a 100% start rate on a
1157// page nobody typed into. _hpFireInitiateCheckout above has always had this bug;
1158// it only ever fed ad pixels, so nobody noticed.
1159//
1160// Owner previews self-gate: analyticsTrackingScript returns on data-owner="1"
1161// BEFORE registering window.__hpupPushEvent, so this finds no bridge and no-ops.
1162// (The edge serves only public traffic, so the same code is correct on both
1163// hosts. Known gap, pre-existing and not widened by form_start: the Nova page
1164// switcher omits ?token=, so isOwnerForAnalytics misses it and page_view /
1165// scroll_depth / element_click already report from that path.)
1166// SYNC with worker-pages/src/scripts/forms.ts — these files must stay byte-identical.
1167var _hpFormStartGesture=false;
1168function _hpMarkFormGesture(){_hpFormStartGesture=true;}
1169document.addEventListener("pointerdown",_hpMarkFormGesture,true);
1170document.addEventListener("keydown",_hpMarkFormGesture,true);
1171document.addEventListener("touchstart",_hpMarkFormGesture,{passive:true,capture:true});
1172// Object.create(null), not {}: the key is formId(), i.e. the author-controlled
1173// id/name attribute. With a bare object a form named "constructor" or "toString"
1174// reads as already-fired on its very first focus and never reports at all, and
1175// "__proto__" cannot even be latched — the page would show submissions with no
1176// starts whatsoever.
1177var _hpFiredFormStart=Object.create(null);
1178function _hpReportFormStart(formIdent){
1179  if(!_hpFormStartGesture)return;
1180  var k=String(formIdent||"");
1181  if(_hpFiredFormStart[k])return;
1182  _hpFiredFormStart[k]=true;
1183  try{if(window.__hpupPushEvent)window.__hpupPushEvent("page_engagement",{kind:"form_start",id:k.slice(0,128)});}catch(e){}
1184}
1185document.addEventListener("focusin",function(e){
1186  var el=e.target;
1187  if(!el||(el.tagName!=="INPUT"&&el.tagName!=="TEXTAREA"&&el.tagName!=="SELECT"))return;
1188  if(el.type==="hidden"||el.type==="submit"||el.type==="button")return;
1189  var f=el.closest&&(el.closest("form")||el.closest("[data-htmlpub-fake-form]"));
1190  if(!f)return;
1191  _hpReportFormStart(formId(f));
1192  _hpFireInitiateCheckout(formId(f));
1193},true);
1194
1195// Set _hp_email cookie when we find an email in form data (for CRM personalization)
1196function setEmailCookie(data){
1197var email=null;
1198for(var k in data){
1199if(!data.hasOwnProperty(k))continue;
1200var v=(data[k]||"").toString();
1201if(/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim())){email=v.trim();break;}
1202}
1203if(email){
1204try{var enc=btoa(email.toLowerCase().trim());document.cookie="_hp_email="+encodeURIComponent(enc)+";path=/;max-age=31536000;SameSite=Lax;Secure";}catch(e){}
1205}
1206}
1207
1208// Read _hpid from URL and set cookie for future visits
1209(function(){
1210var p=new URLSearchParams(window.location.search);
1211var hpid=p.get("_hpid");
1212if(hpid){
1213try{var decoded=atob(hpid);if(decoded.indexOf("@")!==-1){document.cookie="_hp_email="+encodeURIComponent(hpid)+";path=/;max-age=31536000;SameSite=Lax;Secure";}}catch(e){}
1214}
1215})();
1216
1217// URL-parameter auto-populate. Any input/textarea/select with
1218// data-persist-url-param="<name>" gets its value set from
1219// URLSearchParams(location.search).get(<name>) on script boot. Enables UTM /
1220// gclid / fbclid / any-custom-param capture into form submissions without a
1221// custom head script. An author (or the AI builder) drops
1222//   <input type="hidden" name="utm_source" data-persist-url-param="utm_source">
1223// and the runtime fills it in from ?utm_source=... on page load. Runs
1224// BEFORE the data-hp-original tagging pass below so the populated hidden
1225// inputs get stamped and pass _isInjected. utm_* names are not on
1226// _blocklist. Missing params leave any pre-existing value intact so authors
1227// can specify a fallback via a plain value="" on the input. Selector is
1228// scoped to form fields so a stray attribute on a non-form element is a
1229// no-op rather than a silent property write. Repeated params
1230// (?utm_source=a&utm_source=b) resolve to the first value ("a") per the
1231// URLSearchParams.get() spec.
1232try{
1233var _hpUrlParams=new URLSearchParams(window.location.search);
1234document.querySelectorAll("input[data-persist-url-param],textarea[data-persist-url-param],select[data-persist-url-param]").forEach(function(el){
1235var pname=el.getAttribute("data-persist-url-param");
1236if(!pname)return;
1237var pv=_hpUrlParams.get(pname);
1238if(pv!==null&&pv!=="")el.value=pv;
1239});
1240}catch(e){}
1241
1242// Standard-UTM auto-capture. A form with data-persist-capture-utms gets the
1243// seven standard attribution params (utm_source, utm_medium, utm_campaign,
1244// utm_term, utm_content, gclid, fbclid, msclkid) auto-injected as hidden inputs +
1245// populated from URLSearchParams if the URL carries them. Editor UI toggles
1246// the attribute; runtime does the injection. Idempotent by name, so a form
1247// with a pre-existing named hidden input (author-written or AI-authored)
1248// keeps its own definition and is only value-populated by the URL-param
1249// pass above. Runs BEFORE the data-hp-original tagging pass below so the
1250// injected inputs get stamped and pass _isInjected on submission.
1251try{
1252var _HP_STD_UTMS=["utm_source","utm_medium","utm_campaign","utm_term","utm_content","gclid","fbclid","msclkid"];
1253document.querySelectorAll("form[data-persist-capture-utms]").forEach(function(f){
1254_HP_STD_UTMS.forEach(function(n){
1255if(f.querySelector('input[name="'+n+'"]'))return;
1256var inp=document.createElement("input");
1257inp.type="hidden";
1258inp.name=n;
1259inp.setAttribute("data-persist-url-param",n);
1260var v=_hpUrlParams.get(n);
1261if(v!==null&&v!=="")inp.value=v;
1262f.appendChild(inp);
1263});
1264});
1265}catch(e){}
1266
1267// Cloudflare Turnstile CAPTCHA. A form with data-persist-captcha gets the bridge iframe
1268// injected before its submit control when the feature flag is set. TOP-LEVEL path only — in
1269// the sandboxed /p/ iframe the parent shell renders the widget (opaque origin can't run
1270// Turnstile). Owner/editor previews skip it. The bridge posts the token back (listener near
1271// _hpInjectCaptcha), stashed on the form and read by submitData as the top-level captchaToken.
1272if(!_hpIsOwner&&!inSandbox){try{
1273document.querySelectorAll("form[data-persist-captcha]").forEach(function(f){
1274// Multi-step forms defer injection to the LAST step (see the multi-step render), so the
1275// widget renders fresh + visible next to the submit and never appears on an earlier step.
1276if(f.hasAttribute("data-persist-multistep"))return;
1277_hpInjectCaptcha(f);
1278});
1279}catch(e){}}
1280
1281// Authored multi-step progress indicator — a real [data-persist-ms-progress] element the
1282// editor builds as page content (variant "text" | "bar" | "dots"), so the author restyles it
1283// like anything else on the page. Only its STATE is written here; the structure is authored
1284// markup and is never rebuilt, or a restyle would be thrown away on every step. MIRROR of
1285// syncMsProgressState in visual-editor/iframe-script/utils.ts, which does the same for the
1286// edit canvas — that file cannot be imported from here (this script is a self-contained twin
1287// of a separate Cloudflare-worker package; see form-scripts-sync.test.ts), so the two copies
1288// must agree on every attribute name below. Not to be confused with the TRANSIENT
1289// data-htmlpub-ms-progress line further down, injected only for a form with no authored nav.
1290function _hpMsProgress(f,cur,total){
1291var p=f.querySelector("[data-persist-ms-progress]");
1292if(!p)return;
1293var t=p.querySelector("[data-persist-ms-progress-text]");
1294if(t)t.textContent="Step "+(cur+1)+" of "+total;
1295var fill=p.querySelector("[data-persist-ms-progress-fill]");
1296if(fill)fill.style.width=(total>0?((cur+1)/total)*100:100)+"%";
1297var dots=p.querySelectorAll("[data-persist-ms-progress-dot]");
1298for(var i=0;i<dots.length;i++)dots[i].style.opacity=(i<=cur?"1":"0.25");
1299// A connector counts as travelled only once the step AFTER it is reached.
1300var lines=p.querySelectorAll("[data-persist-ms-progress-line]");
1301for(var j=0;j<lines.length;j++)lines[j].style.opacity=(j<cur?"1":"0.25");
1302}
1303
1304// ── Multi-step forms ── a form[data-persist-multistep] shows one [data-persist-step]
1305// container at a time, and validates the current step's required fields before advancing.
1306// Navigation uses the AUTHORED, page-styled buttons the editor creates —
1307// [data-persist-ms-next] (advance) + [data-persist-ms-back] (retreat) — with the real submit
1308// shown only on the last step. Legacy / hand-authored forms with no such buttons get a
1309// transient injected Back/Next + progress line (data-htmlpub-*, never saved) so they still
1310// work. The controller is stashed on the form as __hpMultiStep so the submit handler advances
1311// instead of submitting until the last step. The edit canvas (owner, NOT preview) skips this
1312// so every field stays visible for editing; Preview (data-preview="1") runs it.
1313if(!_hpIsOwner||_hpIsPreview){try{
1314document.querySelectorAll("form[data-persist-multistep]").forEach(function(f){
1315var steps=[].slice.call(f.querySelectorAll("[data-persist-step]"));
1316if(steps.length<2){
1317// A single-step form has no navigation — hide any authored Back/Next so they don't render
1318// as dead buttons; the submit stays as the only action.
1319var _hpDead=f.querySelectorAll("[data-persist-ms-back],[data-persist-ms-next]");
1320for(var _hpd=0;_hpd<_hpDead.length;_hpd++)_hpDead[_hpd].style.display="none";
1321// Same reasoning for an authored progress indicator: with no steps to advance through it
1322// would sit there reading "Step 1 of 1".
1323var _hpProg1=f.querySelector("[data-persist-ms-progress]");
1324if(_hpProg1)_hpProg1.style.display="none";
1325// A degenerate multi-step form (0 or 1 real steps) behaves like a plain form, but the boot-time
1326// captcha pass skipped it (it carries data-persist-multistep) and the last-step inject below
1327// never runs. Inject the widget here — otherwise the server enforces a token this page never
1328// produces and every submission is rejected (100% lead loss), while the editor still shows the
1329// placeholder. Mirrors the non-multistep boot path (owner/preview skip via !_hpIsOwner).
1330if(!_hpIsOwner&&f.hasAttribute("data-persist-captcha"))_hpInjectCaptcha(f);
1331return;
1332}
1333steps.sort(function(a,b){return (parseInt(a.getAttribute("data-persist-step"),10)||0)-(parseInt(b.getAttribute("data-persist-step"),10)||0);});
1334// Suppress native whole-form constraint validation: a required field in a
1335// not-yet-reached (display:none) step would otherwise abort submission and block
1336// Enter-to-advance. We validate each step in JS instead (validateStep on every hop
1337// + the last step on final submit), so required fields are still enforced.
1338f.noValidate=true;
1339// Exclude the authored nav buttons so a type-less [data-persist-ms-*] button is never picked as the submit.
1340var submit=_hpFindSubmit(f);
1341var submitDisp=submit?submit.style.display:"";
1342var cur=0;
1343// Prefer the authored nav buttons; fall back to injecting them for legacy forms.
1344var next=f.querySelector("[data-persist-ms-next]");
1345var back=f.querySelector("[data-persist-ms-back]");
1346var prog=null;
1347if(!next){
1348// Skipped when the form carries an AUTHORED indicator: the transient one exists only to
1349// stand in for a form that has none, and rendering both reads as a duplicate.
1350if(!f.querySelector("[data-persist-ms-progress]")){
1351prog=document.createElement("div");
1352prog.setAttribute("data-htmlpub-ms-progress","");
1353prog.style.cssText="font-family:system-ui,sans-serif;font-size:13px;opacity:0.7;margin-bottom:8px;";
1354if(steps[0].parentNode)steps[0].parentNode.insertBefore(prog,steps[0]);
1355}
1356var nav=document.createElement("div");
1357nav.setAttribute("data-htmlpub-ms-nav","");
1358nav.style.cssText="display:flex;gap:8px;margin-top:8px;";
1359// Reuse an authored Back if the form has one (only Next was missing) — don't inject a duplicate.
1360if(!back){back=document.createElement("button");back.type="button";back.setAttribute("data-htmlpub-ms-back","");back.textContent="Back";}
1361next=document.createElement("button");next.type="button";next.setAttribute("data-htmlpub-ms-next","");next.textContent="Next";
1362if(submit&&submit.className){next.className=submit.className;if(!back.className)back.className=submit.className;}
1363nav.appendChild(back);nav.appendChild(next);
1364var lastStep=steps[steps.length-1];
1365if(lastStep.parentNode)lastStep.parentNode.insertBefore(nav,lastStep.nextSibling);
1366}
1367var nextDisp=next?next.style.display:"";
1368var backDisp=back?back.style.display:"";
1369function render(){
1370for(var i=0;i<steps.length;i++)steps[i].style.display=(i===cur?"":"none");
1371if(prog)prog.textContent="Step "+(cur+1)+" of "+steps.length;
1372_hpMsProgress(f,cur,steps.length);
1373var last=cur===steps.length-1;
1374if(back)back.style.display=cur>0?backDisp:"none";
1375if(next)next.style.display=last?"none":nextDisp;
1376if(submit)submit.style.display=last?submitDisp:"none";
1377// Spam protection: inject the Turnstile widget the moment the visitor reaches the last step
1378// (fresh + visible so its token populates reliably — hiding it via display:none does not).
1379if(last&&!_hpIsOwner&&f.hasAttribute("data-persist-captcha"))_hpInjectCaptcha(f);
1380}
1381function _hpValidatable(c){
1382// Skip fields the visitor can't see/act on — a data-persist-hidden (display:none) or
1383// disabled required field can't show a validation bubble and would silently trap them.
1384if(c.disabled)return false;
1385if(c.closest&&c.closest("[data-persist-hidden]"))return false;
1386return typeof c.checkValidity==="function";
1387}
1388function validateStep(){
1389var ctrls=steps[cur].querySelectorAll("input,select,textarea");
1390for(var i=0;i<ctrls.length;i++){
1391if(_hpValidatable(ctrls[i])&&!ctrls[i].checkValidity()){
1392if(typeof ctrls[i].reportValidity==="function")ctrls[i].reportValidity();
1393return false;
1394}
1395}
1396return _hpGroupReqOK(steps[cur]);
1397}
1398function validateOutside(){
1399// Required controls authored OUTSIDE any step container are never in validateStep's scope;
1400// f.noValidate is on form-wide, so validate them on final submit or they'd submit empty.
1401var all=f.querySelectorAll("input,select,textarea");
1402for(var i=0;i<all.length;i++){
1403var c=all[i];
1404if(c.closest&&c.closest("[data-persist-step]"))continue;
1405if(_hpValidatable(c)&&!c.checkValidity()){
1406if(typeof c.reportValidity==="function")c.reportValidity();
1407return false;
1408}
1409}
1410return true;
1411}
1412function advance(){if(cur<steps.length-1&&validateStep()){cur++;render();}}
1413if(next)next.addEventListener("click",function(e){if(e&&e.preventDefault)e.preventDefault();advance();});
1414if(back)back.addEventListener("click",function(e){if(e&&e.preventDefault)e.preventDefault();if(cur>0){cur--;render();}});
1415// validate() re-checks the CURRENT step + any required controls outside all steps — the
1416// submit handler calls it on the last step before sending (native validation is off).
1417f.__hpMultiStep={isLast:function(){return cur>=steps.length-1;},advance:advance,validate:function(){return validateStep()&&validateOutside();}};
1418render();
1419});
1420}catch(e){}}
1421
1422// ── International phone ── input[type=tel][data-persist-phone] gets a country
1423// dial-code <select> injected before it + a hidden E.164 companion that carries the
1424// real submitted value (+<dial><national digits>) under the field's name. The visible
1425// tel input keeps the national number for display and is excluded from collect()
1426// (data-hp-phone-nat); the <select> is excluded too (data-hp-phone-cc). On every
1427// input / country change the E.164 is reassembled and validated — E.164 spec (7–15
1428// total digits) plus a GENEROUS per-country national-length range where the length is
1429// well known — via setCustomValidity, so native validation blocks a malformed submit
1430// on single-step forms and validateStep() enforces it on multi-step ones. Runs BEFORE
1431// the data-hp-original tagging pass so the hidden companion is stamped and collected.
1432// Runs in owner mode too so the editor preview shows the selector (the validity flag
1433// is harmless there — owner previews don't submit real leads). data-persist-phone /
1434// data-persist-phone-country are persistent (never stripped from the AI diff); the
1435// injected select + companion are data-hp-* (transient) and never saved.
1436try{
1437var _HP_PHONE=[["US","United States","1",10,10,0],["CA","Canada","1",10,10,0],["GB","United Kingdom","44",9,10,0],["AU","Australia","61",9,9,0],["DE","Germany","49",0,0,0],["FR","France","33",9,9,0],["IN","India","91",10,10,0],["BR","Brazil","55",10,11,0],["MX","Mexico","52",10,10,0],["ES","Spain","34",9,9,0],["IT","Italy","39",9,11,1],["NL","Netherlands","31",9,9,0],["AE","United Arab Emirates","971",9,9,0],["SG","Singapore","65",8,8,0],["JP","Japan","81",9,11,0],["CN","China","86",8,11,0],["ZA","South Africa","27",9,9,0],["NG","Nigeria","234",10,10,0],["IE","Ireland","353",7,9,0],["NZ","New Zealand","64",8,10,0],["AR","Argentina","54",0,0,0],["AT","Austria","43",0,0,0],["BE","Belgium","32",0,0,0],["BD","Bangladesh","880",0,0,0],["BG","Bulgaria","359",0,0,0],["BH","Bahrain","973",8,8,0],["BO","Bolivia","591",0,0,0],["CH","Switzerland","41",9,9,0],["CL","Chile","56",0,0,0],["CO","Colombia","57",8,10,0],["CR","Costa Rica","506",8,8,0],["CY","Cyprus","357",8,8,0],["CZ","Czechia","420",9,9,0],["DK","Denmark","45",8,8,0],["DO","Dominican Republic","1",10,10,0],["EC","Ecuador","593",0,0,0],["EG","Egypt","20",0,0,0],["EE","Estonia","372",0,0,0],["FI","Finland","358",0,0,0],["GR","Greece","30",10,10,0],["GT","Guatemala","502",8,8,0],["HK","Hong Kong","852",8,8,0],["HR","Croatia","385",0,0,0],["HU","Hungary","36",0,0,0],["ID","Indonesia","62",0,0,0],["IL","Israel","972",8,9,0],["JO","Jordan","962",0,0,0],["KE","Kenya","254",0,0,0],["KR","South Korea","82",0,0,0],["KW","Kuwait","965",8,8,0],["LK","Sri Lanka","94",9,9,0],["LT","Lithuania","370",8,8,0],["LU","Luxembourg","352",0,0,0],["LV","Latvia","371",8,8,0],["MA","Morocco","212",9,9,0],["MY","Malaysia","60",0,0,0],["NO","Norway","47",8,8,0],["OM","Oman","968",8,8,0],["PA","Panama","507",0,0,0],["PE","Peru","51",9,9,0],["PH","Philippines","63",9,10,0],["PK","Pakistan","92",10,10,0],["PL","Poland","48",9,9,0],["PT","Portugal","351",9,9,0],["PY","Paraguay","595",0,0,0],["QA","Qatar","974",8,8,0],["RO","Romania","40",9,9,0],["RS","Serbia","381",0,0,0],["RU","Russia","7",10,10,0],["SA","Saudi Arabia","966",9,9,0],["SE","Sweden","46",0,0,0],["SK","Slovakia","421",9,9,0],["SI","Slovenia","386",0,0,0],["TH","Thailand","66",9,9,0],["TN","Tunisia","216",8,8,0],["TR","Turkey","90",10,10,0],["TW","Taiwan","886",0,0,0],["UA","Ukraine","380",9,9,0],["UY","Uruguay","598",0,0,0],["VE","Venezuela","58",0,0,0],["VN","Vietnam","84",0,0,0]];
1438var _hpPhoneByIso={};for(var _hppi=0;_hppi<_HP_PHONE.length;_hppi++){_hpPhoneByIso[_HP_PHONE[_hppi][0]]=_HP_PHONE[_hppi];}
1439var _hpDigits=function(s){return (s||"").replace(/[^0-9]/g,"");};
1440var _hpFlag=function(iso){try{return String.fromCodePoint(0x1F1E6+(iso.charCo
1440deAt(0)-65))+String.fromCodePoint(0x1F1E6+(iso.charCodeAt(1)-65));}catch(e){return "";}};
1441// Reduce the typed digits to the country's national significant number (NSN): strip a
1442// user-typed country code (guarded by the expected NSN length so a valid number is never
1443// shortened) and a national trunk prefix "0" (guarded so it never drops below the country
1444// minimum; suppressed for keepLeadingZero countries like Italy). Lead-safe: each strip
1445// fires only when it yields a still-plausible NSN, so "07911 123456", "06 12 34 56 78" and
1446// "1 (415) 555-2671" all assemble to correct E.164 and pass validation.
1447var _hpNSN=function(nat,cur){
1448var dial=cur[2],min=cur[3],max=cur[4],keep0=cur[5];
1449if(dial&&nat.length>dial.length&&nat.substring(0,dial.length)===dial){
1450var rest=nat.substring(dial.length);
1451var restT=(!keep0&&rest.charAt(0)==="0")?rest.replace(/^0+/,""):rest;
1452if(min&&max){
1453// Known national length: keep whichever candidate fits the country's range.
1454if(restT.length>=min&&restT.length<=max)nat=restT;
1455else if(rest.length>=min&&rest.length<=max)nat=rest;
1456}
1457// Variable-length country (min/max unknown): do NOT strip the leading dial-code digits —
1458// for those countries a real national number can legitimately begin with its own dial
1459// code, and chopping it corrupts the number (F4). "+"-prefixed input is handled before
1460// _hpNSN, so this only affects the ambiguous no-"+" case; keep the digits as typed.
1461}
1462if(!keep0&&nat.charAt(0)==="0"){var s=nat.replace(/^0+/,"");if(!min||s.length>=min)nat=s;}
1463// Russia/Kazakhstan (dial 7) use "8" as the national trunk prefix, not "0". Strip a lone
1464// leading "8", guarded by the country minimum so a valid NSN is never shortened (F3).
1465if(dial==="7"&&nat.charAt(0)==="8"){var s8=nat.replace(/^8/,"");if(!min||s8.length>=min)nat=s8;}
1466return nat;
1467};
1468document.querySelectorAll('input[type="tel"][data-persist-phone]').forEach(function(inp){
1469if(inp.getAttribute("data-hp-phone-on")==="1")return;
1470inp.setAttribute("data-hp-phone-on","1");
1471// Key the E.164 companion the SAME way collect()/extract-form-fields key a plain tel
1472// input (name -> id -> data-field -> "phone"), computed BEFORE any DOM mutation so
1473// enabling international never changes which lead column the number lands in.
1474var nm=fieldKey(inp,0);
1475var iso=(inp.getAttribute("data-persist-phone-country")||"US").toUpperCase();
1476if(!_hpPhoneByIso[iso])iso="US";
1477// Custom country combobox. A native <select> can't show a compact "flag +dial" when
1478// CLOSED and full country names when OPEN: its closed text IS the selected option's
1479// text, and a same-value close fires no event to swap it back. So we build a small
1480// self-contained combobox — a styled trigger button showing "flag +dial" + a panel
1481// listing full country names. curIso tracks the selection for E.164 assembly. Lay it
1482// out side by side with the number in a flex row (standard international-phone layout);
1483// the trigger copies the number input's border/radius/padding/font/colors so it matches
1484// whatever the page's forms look like (align-items:stretch matches their heights).
1485var curIso=iso;
1486// Brand accent for the selected-row tint + checkmark, sampled from the form's primary
1487// (submit) button background so the dropdown matches the page. A light tint keeps the
1488// dark option text legible. Falls back to a neutral blue; transparent / near-white
1489// buttons are ignored so the accent stays visible.
1490var _hpAccent="#2563eb",_hpAccentTint="rgba(37,99,235,0.12)";
1491try{
1492var _hpBtnForm=inp.closest?inp.closest("form"):null;
1493var _hpBtn=_hpBtnForm?_hpFindSubmit(_hpBtnForm):null;
1494if(_hpBtn){
1495var _bg=window.getComputedStyle(_hpBtn).backgroundColor||"";
1496var _pp=_bg.replace(/[^0-9,.]/g,"").split(",");
1497var _rr=parseInt(_pp[0],10),_gg=parseInt(_pp[1],10),_bb=parseInt(_pp[2],10),_aa=_pp.length>3?parseFloat(_pp[3]):1;
1498if(!isNaN(_rr)&&!isNaN(_gg)&&!isNaN(_bb)&&_aa>0.1&&(_rr+_gg+_bb)<720){_hpAccent="rgb("+_rr+","+_gg+","+_bb+")";_hpAccentTint="rgba("+_rr+","+_gg+","+_bb+",0.12)";}
1499}
1500}catch(e){}
1501// Use the AUTHORED wrapper (data-persist-phone-field) that the editor saved, so the
1502// published layout is identical to the edit canvas. Create one only as a fallback for
1503// legacy pages saved before the authored wrapper existed (served pages aren't re-saved,
1504// so a runtime-created wrapper is harmless there).
1505var wrap=inp.closest?inp.closest("[data-persist-phone-field]"):null;
1506if(!wrap){
1507wrap=document.createElement("span");
1508wrap.setAttribute("data-persist-phone-field","");
1509wrap.style.cssText="display:flex;align-items:stretch;gap:6px;width:100%;position:relative;";
1510if(inp.parentNode)inp.parentNode.insertBefore(wrap,inp);
1511wrap.appendChild(inp);
1512}
1513if(!wrap.style.position)wrap.style.position="relative";
1514// A <span role=button>, NOT a <button>: the widget sits inside the field's <label>, and
1515// a <label> forwards clicks to its first LABELABLE descendant. A <button> would become
1516// that control, so clicking a list item fires a synthetic click on the trigger and
1517// re-opens the panel. A <span> isn't labelable, so the label keeps targeting the number
1518// input; preventDefault on the trigger/list clicks stops any residual label forwarding.
1519var trig=document.createElement("span");
1520trig.setAttribute("role","button");
1521trig.setAttribute("tabindex","0");
1522trig.setAttribute("data-hp-phone-cc","");
1523trig.setAttribute("aria-haspopup","listbox");
1524trig.setAttribute("aria-expanded","false");
1525trig.setAttribute("aria-label","Country calling code");
1526trig.style.cssText="flex:0 0 auto;display:inline-flex;align-items:center;gap:6px;cursor:pointer;white-space:nowrap;-webkit-user-select:none;user-select:none;box-sizing:border-box;";
1527try{
1528var _cs=window.getComputedStyle(inp);
1529var _cp=["fontFamily","fontSize","fontWeight","color","backgroundColor","borderTopWidth","borderRightWidth","borderBottomWidth","borderLeftWidth","borderTopStyle","borderRightStyle","borderBottomStyle","borderLeftStyle","borderTopColor","borderRightColor","borderBottomColor","borderLeftColor","borderTopLeftRadius","borderTopRightRadius","borderBottomRightRadius","borderBottomLeftRadius","paddingTop","paddingBottom","paddingLeft","paddingRight","boxSizing","lineHeight"];
1530for(var _cpi=0;_cpi<_cp.length;_cpi++){try{if(_cs[_cp[_cpi]])trig.style[_cp[_cpi]]=_cs[_cp[_cpi]];}catch(e){}}
1531}catch(e){}
1532var trigLabel=document.createElement("span");
1533trig.appendChild(trigLabel);
1534var trigCaret=document.createElement("span");trigCaret.textContent="▾";trigCaret.style.cssText="opacity:0.55;font-size:0.8em;";
1535trig.appendChild(trigCaret);
1536var panel=document.createElement("ul");
1537panel.setAttribute("role","listbox");
1538panel.setAttribute("data-hp-phone-list","");
1539panel.style.cssText="display:none;position:absolute;top:100%;left:0;z-index:2147483000;margin:4px 0 0;padding:4px 0;list-style:none;max-height:260px;overflow-y:auto;min-width:240px;background:#fff;color:#111;border:1px solid rgba(0,0,0,0.15);border-radius:8px;box-shadow:0 8px 28px rgba(0,0,0,0.18);font-family:system-ui,-apple-system,sans-serif;font-size:14px;text-align:left;";
1540// Hover + selected-item styling (one shared, id-guarded stylesheet — inline styles can't
1541// express :hover / ::after). Injected on the live page only; never touches the editor.
1542if(!document.getElementById("hp-phone-cc-style")){var _phSt=document.createElement("style");_phSt.id="hp-phone-cc-style";_phSt.textContent="ul[data-hp-phone-list] li[role=option]:hover{background:rgba(0,0,0,0.06);}ul[data-hp-phone-list] li[aria-selected=true]{font-weight:600;background:var(--hp-accent-tint,rgba(37,99,235,0.12));}ul[data-hp-phone-list] li[aria-selected=true]::after{content:'✓';float:right;margin-left:12px;color:var(--hp-accent,#2563eb);}";(document.head||document.documentElement).appendChild(_phSt);}
1543panel.style.setProperty("--hp-accent",_hpAccent);
1544panel.style.setProperty("--hp-accent-tint",_hpAccentTint);
1545for(var pci=0;pci<_HP_PHONE.length;pci++){
1546var pc=_HP_PHONE[pci];
1547var li=document.createElement("li");
1548li.setAttribute("role","option");
1549li.setAttribute("data-iso",pc[0]);
1550li.setAttribute("data-dial",pc[2]);
1551li.setAttribute("aria-selected",pc[0]===iso?"true":"false");
1552li.textContent=_hpFlag(pc[0])+" "+pc[1]+" +"+pc[2];
1553li.style.cssText="padding:7px 14px;cursor:pointer;white-space:nowrap;";
1554panel.appendChild(li);
1555}
1556var renderTrig=function(){var c=_hpPhoneByIso[curIso];trigLabel.textContent=_hpFlag(curIso)+" +"+(c?c[2]:"");};
1557var closePanel=function(){panel.style.display="none";trig.setAttribute("aria-expanded","false");};
1558var openPanel=function(){panel.style.display="block";trig.setAttribute("aria-expanded","true");};
1559inp.style.flex="1";
1560inp.style.minWidth="0";
1561wrap.insertBefore(trig,inp); // trigger sits before the number input (input already in the wrap)
1562wrap.appendChild(panel);
1563var hid=document.createElement("input");
1564hid.type="hidden";
1565hid.name=nm;
1566hid.setAttribute("data-hp-phone-e164","");
1567wrap.appendChild(hid);
1568inp.setAttribute("data-hp-phone-nat","");
1569inp.removeAttribute("name");
1570var _hpSyncPhone=function(){
1571var raw=(inp.value||"").trim();
1572var cur=_hpPhoneByIso[curIso];
1573var dial=cur?cur[2]:"";
1574var e164="",nsnLen=0,isIntl=false;
1575if(raw.charAt(0)==="+"){e164="+"+_hpDigits(raw);isIntl=true;}
1576else{var nsn=_hpNSN(_hpDigits(raw),cur);nsnLen=nsn.length;e164=nsn?("+"+dial+nsn):"";}
1577hid.value=e164;
1578var msg="";
1579if(raw){
1580var total=_hpDigits(e164).length;
1581// Hard rules only: E.164 spec (7-15 total digits) + a per-country MINIMUM NSN length
1582// (catches a genuinely incomplete number, e.g. a US local number with no area code). No
1583// per-country maximum -> an over-long entry (usually the visitor including extra
1584// country/trunk digits) is accepted, never rejected. A "+"-prefixed entry skips the
1585// per-country floor entirely (the visitor owns their own format).
1586if(total<7||total>15)msg="Enter a valid phone number.";
1587else if(!isIntl&&nsnLen&&cur&&cur[3]&&nsnLen<cur[3])msg="Enter a valid "+cur[1]+" phone number.";
1588}
1589if(typeof inp.setCustomValidity==="function")inp.setCustomValidity(msg);
1590};
1591var selectIso=function(newIso){if(_hpPhoneByIso[newIso])curIso=newIso;for(var _si=0;_si<panel.children.length;_si++){var _c=panel.children[_si];if(_c.getAttribute&&_c.getAttribute("role")==="option")_c.setAttribute("aria-selected",_c.getAttribute("data-iso")===curIso?"true":"false");}renderTrig();closePanel();_hpSyncPhone();};
1592panel.addEventListener("click",function(e){var t=e.target;while(t&&t!==panel&&!(t.getAttribute&&t.getAttribute("role")==="option"))t=t.parentNode;if(t&&t.getAttribute&&t.getAttribute("role")==="option"){e.preventDefault();selectIso(t.getAttribute("data-iso"));try{inp.focus();}catch(_e){}}});
1593trig.addEventListener("click",function(e){e.preventDefault();if(panel.style.display==="block")closePanel();else openPanel();});
1594trig.addEventListener("keydown",function(e){if(e.key==="Escape"){closePanel();}else if(e.key==="ArrowDown"||e.key==="Enter"||e.key===" "){e.preventDefault();openPanel();}});
1595document.addEventListener("click",function(e){if(!wrap.contains(e.target))closePanel();});
1596inp.addEventListener("input",_hpSyncPhone);
1597inp.addEventListener("blur",_hpSyncPhone);
1598renderTrig();
1599_hpSyncPhone();
1600});
1601}catch(e){}
1602
1603// Tag all hidden inputs present in the original HTML so we can distinguish
1604// them from ones injected later by third-party scripts (Customers.ai, etc.)
1605document.querySelectorAll('input[type="hidden"]').forEach(function(el){el.setAttribute("data-hp-original","1");});
1606
1607function showNotice(f,text,ok){
1608var m=document.createElement("div");
1609m.textContent=text;
1610m.style.cssText="padding:12px 16px;margin:8px 0;border-radius:6px;font-family:system-ui,sans-serif;font-size:14px;"+(ok?"background:#f0fdf4;color:#166534;border:1px solid #bbf7d0":"background:#fef2f2;color:#991b1b;border:1px solid #fecaca");
1611f.parentNode.insertBefore(m,f.nextSibling);
1612setTimeout(function(){m.remove()},5000);
1613}
1614// showMsg wraps showNotice; the optional 3rd arg lets callers override the default success/error
1615// copy (e.g. the Turnstile "still verifying" gate). Omit it for the standard success/error notices.
1616function showMsg(f,ok,msg){showNotice(f,msg||(ok?(f.getAttribute("data-success-message")||"Thank you! Your submission has been received."):"Something went wrong. Please try again."),ok);}
1617// reCAPTCHA v2 (HP-1475). The response token lives in a hidden
1618// <textarea name="g-recaptcha-response"> that grecaptcha creates inside the
1619// widget's container; scoping the lookup to the submitted form/container picks
1620// the right token when a page renders more than one widget.
1621function _hpCaptchaToken(f){
1622try{var ta=f.querySelector&&f.querySelector('textarea[name="g-recaptcha-response"]');return ta&&ta.value?ta.value:"";}catch(e){return "";}
1623}
1624
1625function formId(f){
1626if(f.tagName==="FORM")return f.id||f.getAttribute("name")||"form-"+([].indexOf.call(document.forms,f));
1627return f.id||"section-"+([].indexOf.call(document.querySelectorAll("[data-htmlpub-fake-form]"),f));
1628}
1629
1630// Listen for postMessage responses from parent shell
1631if(inSandbox){
1632window.addEventListener("message",function(e){
1633var d=e.data;
1634if(d&&d.type==="htmlpub-form-submit-result"&&d.correlationId){
1635var cb=pendingCallbacks[d.correlationId];
1636if(cb){
1637delete pendingCallbacks[d.correlationId];
1638cb(d.success,d.captchaRequired,d.error);
1639}
1640}
1641});
1642}
1643
1644function isSafeRedirectUrl(url){
1645if(!url)return false;
1646if(/^https?:\/\//i.test(url))return true;
1647if(url.charAt(0)==="/"&&url.charAt(1)!=="/")return true;
1648return false;
1649}
1650// Migrated Builder3 site headers use relative hrefs that break on trailing-slash landing URLs; drop the trailing slash on same-origin targets (/thank-you/ -> /thank-you).
1651function stripTrailingSlash(url){
1652try{var u=new URL(url,window.location.origin);if(u.origin===window.location.origin&&u.pathname.length>1&&u.pathname.charAt(u.pathname.length-1)==="/")return u.pathname.replace(/\/+$/,"")+u.search+u.hash;}catch(e){}
1653return url;
1654}
1655function maybeRedirectUrl(url){
1656if(window.htmlpubEditorMode)return;
1657if(!isSafeRedirectUrl(url))return;
1658if(/\/api\/forms\/submit$/.test(url))return;
1659setTimeout(function(){window.location.href=stripTrailingSlash(url);},600);
1660}
1661function maybeRedirect(f){maybeRedirectUrl(f.getAttribute("action"));}
1662function reportFail(kind,status,fident){
1663try{if(window.__hpupPushEvent)window.__hpupPushEvent("form_submit_failed",{failureKind:kind,httpStatus:status||0,host:location.host,path:location.pathname,apiBase:s.getAttribute("data-api-url")||"",formIdentifier:fident,pageId:pageId,pageSlug:slug});}catch(e){}
1664}
1665function submitData(f,data){
1666var btn=_hpFindSubmit(f);
1667if(btn)btn.disabled=true;
1668
1669// Generate one eventID per submission, threaded through three places:
1670//   (a) the JSON payload to /api/forms/submit → integration queue
1671//   (b) the postMessage to the parent shell (which forwards to the same API)
1672//   (c) the browser pixel events fired on success below
1673// Both browser and server emit the SAME id to the same downstream provider
1674// so Meta / TikTok dedup them as a single conversion.
1675var eventId=_hpUuid();
1676var fident=formId(f);
1677// Per-form Turnstile token — delivered by the bridge iframe via postMessage (see the listener
1678// near _hpInjectCaptcha) and stashed on the form element.
1679var _hpCapTok=(f.__hpCaptchaToken||"");
1680// stable editor-stamped form id → native lead-magnet routing; "" when unbound
1681var fkey=f.getAttribute("data-persist-form-id")||"";
1682// Classic form uuid re-stamped on migrated forms → per-form scope for carried Classic Zaps
1683var fsrc=f.getAttribute("data-persist-form-source-id")||"";
1684
1685// reCAPTCHA gate (HP-1475): when the page enforces reCAPTCHA but the visitor
1686// hasn't solved the challenge, block the submit with an inline error instead
1687// of POSTing an unverifiable payload the server will reject anyway. Gated on
1688// the enforcement flag (not mere widget presence) so pages with a decorative
1689// widget are unaffected. The server remains the security boundary — it
1690// re-verifies the token against Google. A form that opts into per-form Turnstile
1691// (data-persist-captcha) is handled by the Turnstile gate below instead — skip it here so a
1692// Turnstile form on a reCAPTCHA-enforced page isn't blocked for lacking a reCAPTCHA token
1693// (mirrors the server, which skips its reCAPTCHA block for forms in captchaFormIds).
1694var captchaToken=_hpCaptchaToken(f);
1695if(_hpRecaptchaEnforced&&!captchaToken&&!(f.hasAttribute&&f.hasAttribute("data-persist-captcha"))){
1696if(btn)btn.disabled=false;
1697showNotice(f,"Please complete the reCAPTCHA challenge before submitting.",false);
1698reportFail("captcha_required",0,fident);
1699return;
1700}
1701
1702if(inSandbox){
1703var id="fs-"+(++corrId);
1704var timer=setTimeout(function(){
1705delete pendingCallbacks[id];
1706if(btn)btn.disabled=false;
1707reportFail("timeout",0,fident);showMsg(f,false);
1708},10000);
1709
1710pendingCallbacks[id]=function(success,captchaRequired,errMsg){
1711clearTimeout(timer);
1712if(btn)btn.disabled=false;
1713if(success){setEmailCookie(data);_hpFireLead(fident,data,eventId);showMsg(f,true);if(typeof f.reset==="function")f.reset();maybeRedirect(f);}
1714else if(captchaRequired){showMsg(f,false,"Spam protection is still verifying — please wait a moment and submit again.");}
1715else{reportFail("relay_error",0,fident);showMsg(f,false,errMsg||undefined);}
1716};
1717
1718var vid="";try{vid=localStorage.getItem("_hp_vid")||"";}catch(e){}
1719parent.postMessage({type:"htmlpub-form-submit",correlationId:id,pageId:pageId,pageSlug:slug,formIdentifier:fident,formKey:fkey,classicFormUuid:fsrc,data:data,visitorId:vid,eventId:eventId,captchaToken:(_hpCapTok||captchaToken),formNeedsCaptcha:!!(f.hasAttribute&&f.hasAttribute("data-persist-captcha"))},"*");
1720}else{
1721// Send in top-level (direct fetch) mode. Wrapped in _hpProceed so it can be deferred
1722// until the Turnstile token has had a bounded chance to arrive (see _hpWaitForCaptcha).
1723function _hpProceed(){
1724var vid2="";try{vid2=localStorage.getItem("_hp_vid")||"";}catch(e){}
1725// The captcha token rides EVERY attempt (not just the first). It is single-use,
1726// but the server checks its verified-eventId cache before re-verifying, so a
1727// re-sent already-spent token short-circuits to pass, while a token that was never
1728// spent (attempt 0 failed at the transport / rate-limit layer before verify) still
1729// verifies on retry. Stripping it from retries silently dropped leads on captcha
1730// forms whenever attempt 0 hit a 429/5xx/network error. [HP-2467] Prefer the per-form
1731// Turnstile token; fall back to the reCAPTCHA token (a form uses one mechanism). Read
1732// the FRESHEST token here — it may have arrived via postMessage during the bounded wait,
1733// after _hpCapTok was snapshotted at the top of submitData.
1734var _capTok=(f.__hpCaptchaToken||"")||_hpCapTok||captchaToken;
1735var _hpBody=JSON.stringify({pageId:pageId,pageSlug:slug,formIdentifier:fident,formKey:fkey,classicFormUuid:fsrc,data:data,visitorId:vid2,eventId:eventId,captchaToken:_capTok});
1736// Retry transient failures so a network blip / 5xx / 429 doesn't silently lose
1737// a lead. Every attempt reuses the same eventId, so the server dedups to
1738// exactly one row + one set of side-effects. Backoff: immediate, 15s, 30s,
1739// 2min, then give up. Failed submits don't redirect, so the page stays open
1740// and the delayed retries can run. The submit button stays disabled across
1741// retries to prevent a manual re-submit (which would mint a new eventId). [HP-2467]
1742var _hpDelays=[0,15000,30000,120000];
1743function _hpAttempt(n){
1744fetch(api,{method:"POST",keepalive:true,headers:{"Content-Type":"application/json"},body:_hpBody}).then(function(r){
1745if(r.ok){if(btn)btn.disabled=false;setEmailCookie(data);_hpFireLead(fident,data,eventId);showMsg(f,true);if(typeof f.reset==="function")f.reset();
1746// The Turnstile token is single-use; after a successful submit clear it and reload the bridge
1747// iframe so a subsequent submit on the same (non-reset / non-redirected) form gets a FRESH token
1748// instead of re-sending the spent one and being rejected.
1749try{f.__hpCaptchaToken="";f.__hpCaptchaError=false;var _hpCf=f.querySelector("iframe[data-hp-turnstile]");if(_hpCf)_hpCf.src=_hpCf.src;}catch(e){}
1750maybeRedirect(f);return;}
1751if((r.status>=500||r.status===429)&&n<_hpDelays.length){setTimeout(function(){_hpAttempt(n+1);},_hpDelays[n]);return;}
1752if(btn)btn.disabled=false;reportFail("http_error",r.status,fident);
1753// A 403 on a captcha-protected form means the token was spent/invalid — refresh the widget
1754// so a retry gets a fresh one (otherwise "try again" resends the same rejected token).
1755try{if(r.status===403){f.__hpCaptchaToken="";f.__hpCaptchaError=false;var _hpCfe=f.querySelector("iframe[data-hp-turnstile]");if(_hpCfe)_hpCfe.src=_hpCfe.src;}}catch(e){}
1756// Surface the server's specific message (e.g. "CAPTCHA verification failed") instead of a
1757// generic one; fall back to the generic message when the body isn't JSON.
1758try{r.json().then(function(d){showMsg(f,false,(d&&d.error)||undefined);},function(){showMsg(f,false);});}catch(e){showMsg(f,false);}
1759}).catch(function(){
1760if(n<_hpDelays.length){setTimeout(function(){_hpAttempt(n+1);},_hpDelays[n]);return;}
1761if(btn)btn.disabled=false;reportFail("network",0,fident);showMsg(f,false);
1762});
1763}
1764_hpAttempt(0);
1765}
1766// Spam-protected form whose Turnstile widget exists but hasn't produced a token yet
1767// (fast submit, or the challenge is still loading). Per the decided fail policy, give the
1768// widget a bounded window plus ONE re-challenge, then submit anyway (the server accepts +
1769// flags unverified — never lose a lead) instead of the old "please submit again" dead end.
1770// Skip the wait when: owner preview (widget is owner-gated), a token is already in hand, the
1771// bridge reported a TERMINAL error (no token could ever come), or no widget is present (site
1772// key unconfigured → nothing to wait for, server fails open). The button stays disabled
1773// during the (sub-second-typical, 6s-worst-case) wait so the visitor can't double-submit.
1774if(_hpIsOwner||_hpCapTok!==""||f.__hpCaptchaError||!f.querySelector("iframe[data-hp-turnstile]")){
1775_hpProceed();
1776}else{
1777_hpWaitForCaptcha(f,_hpProceed);
1778}
1779}
1780}
1781
1782// A pasted third-party form the migration kept (data-persist-third-party-form: Kit, Mailchimp,
1783// PayPal, ...) submits to its own action, natively or through the vendor's script, so it is never
1784// cancelled. Its lead is also sent here, fire-and-forget: keepalive survives the navigation, and
1785// there is no UI, retry or redirect because the page is leaving and the vendor owns the outcome.
1786function _hpBackgroundCapture(f,data){
1787var eventId=_hpUuid();
1788var fident=formId(f);
1789var vid="";try{vid=localStorage.getItem("_hp_vid")||"";}catch(e){}
1790var msg={pageId:pageId,pageSlug:slug,formIdentifier:fident,formKey:f.getAttribute("data-persist-form-id")||"",classicFormUuid:f.getAttribute("data-persist-form-source-id")||"",data:data,visitorId:vid,eventId:eventId,captchaToken:_hpCaptchaToken(f)};
1791try{
1792if(inSandbox){msg.type="htmlpub-form-submit";msg.correlationId="fs-"+(++corrId);parent.postMessage(msg,"*");}
1793else{fetch(api,{method:"POST",keepalive:true,headers:{"Content-Type":"application/json"},body:JSON.stringify(msg)}).catch(function(){reportFail("network",0,fident);});}
1794setEmailCookie(data);_hpFireLead(fident,data,eventId);
1795}catch(e){}
1796}
1797
1798// Snapshot forms in the server-rendered HTML at init; third-party embeds (ConvertKit/ActiveCampaign) inject their <form> later, so they're excluded.
1799var _hpNativeForms=(function(){try{return new WeakSet([].slice.call(document.querySelectorAll("form")));}catch(e){return null;}})();
1800document.addEventListener("submit",function(e){
1801var f=e.target;
1802if(f.tagName!=="FORM")return;
1803if(_hpNativeForms&&!_hpNativeForms.has(f)){reportFail("skipped_non_native",0,formId(f));return;}// injected after init (popup/third-party) — don't hijack it; telemeter so silently-dropped non-native submits are visible [HP-2467]
1804if(f.hasAttribute("data-persist-third-party-form")){if(window.htmlpubEditorMode||(_hpIsOwner&&!_hpIsPreview)){e.preventDefault();e.stopPropagation();return;}// edit canvas: no navigation, and stopPropagation keeps the vendor script (Kit, ...) from posting a real subscriber
1805var tpData=collect(f);if(Object.keys(tpData).length)_hpBackgroundCapture(f,tpData);return;}
1806e.preventDefault();
1807// Multi-step: if this form is split into steps and the visitor isn't on the last
1808// one yet, advance (validate current step + Next) instead of submitting. Captures
1809// Enter-in-field and any early submit so a partial lead is never sent. On the last
1810// step, validate it in JS first (the form is novalidate, so nothing else does). [P3]
1811if(f.__hpMultiStep){
1812if(!f.__hpMultiStep.isLast()){f.__hpMultiStep.advance();return;}
1813if(f.__hpMultiStep.validate&&!f.__hpMultiStep.validate())return;
1814}
1815// "Select at least one" enforcement for required checkbox groups (single-step forms have no
1816// __hpMultiStep validate pass, and it's a cheap final guard for multi-step too).
1817if(!_hpGroupReqOK(f))return;
1818var data=collect(f);
1819if(Object.keys(data).length===0){if(f.querySelector&&f.querySelector("input,textarea,select"))reportFail("skipped_empty",0,formId(f));return;}// nothing collected — telemeter only when the form actually has fields (real form, nothing captured) [HP-2467]
1820submitData(f,data);
1821},true);
1822
1823// --- Fake form interception (inputs outside <form>) ---
1824var lastFakeSubmit=0;
1825// Walk up from the clicked element to find a "form-like" container:
1826//   - has 1-20 inputs/textareas/selects (excluding hidden/submit/button types)
1827//   - has ≤4 button-like descendants (more = page scaffolding, not a form)
1828//   - the clicked element sits AFTER the last input in document order
1829//     (real submits sit below fields; uses DOCUMENT_POSITION_PRECEDING = 2)
1830// Walk depth capped at 3 — further up is <section>, <main>, or <body>.
1831function findFormLikeContainer(t){
1832var cur=t.parentElement;
1833for(var depth=0;cur&&depth<3;depth++){
1834if(cur.tagName==="FORM")return null;// real form, skip
1835var inputs=cur.querySelectorAll("input:not([type=hidden]):not([type=submit]):not([type=button]),textarea,select");
1836if(inputs.length>=1&&inputs.length<=20){
1837var ctas=cur.querySelectorAll("button,a[href],[role=button],input[type=submit]");
1838if(ctas.length<=4){
1839return (t.compareDocumentPosition(inputs[inputs.length-1])&2)?cur:null;
1840}
1841}
1842cur=cur.parentElement;
1843}
1844return null;
1845}
1846// <a> with a real href is navigation, never a submit. Allow empty href / "#" /
1847// no href (button-styled anchors). <button type="button"> is explicitly non-submit.
1848function isPlausibleSubmit(t){
1849if(t.tagName==="A"){
1850var href=t.getAttribute("href");
1851if(href!==null){
1852href=(href+"").trim();
1853if(href!==""&&href!=="#")return false;
1854}
1855}
1856if(t.tagName==="BUTTON"&&t.getAttribute("type")==="button")return false;
1857return true;
1858}
1859document.addEventListener("click",function(e){
1860var t=e.target.closest?e.target.closest("button,a,[role=button],input[type=submit]"):null;
1861if(!t)return;
1862if(t.closest("form"))return;// inside a real form, native handler covers it
1863// Third-party consent-management (CMP) dialogs (Cookiebot/OneTrust/Osano/
1864// TrustArc/CookieYes) inject a body-level dialog whose "Accept" button + single
1865// pre-checked "necessary" checkbox otherwise look form-like. Cheap early-exit
1866// for known vendors; the lead-field guard below is the load-bearing check.
1867// Vendor tokens are boundary-anchored (CookieYes cky-* matched as whole class
1868// tokens; TrustArc truste-/truste_/trustarc) so they never collide with
1869// sticky-*/trusted* names. [HP-3292]
1870try{if(t.closest("[id*='Cookiebot' i],[id*='CybotCookiebot' i],[id*='onetrust' i],[id*='osano' i],[id*='truste-' i],[id*='truste_' i],[id*='trustarc' i],[class*='cookie-consent' i],[class*='cookieconsent' i],[class^='cky-' i],[class*=' cky-' i]"))return;}catch(e){}
1871if(!isPlausibleSubmit(t))return;
1872var now=Date.now();
1873if(now-lastFakeSubmit<2000)return;
1874var container=findFormLikeContainer(t);
1875if(!container)return;
1876// A genuine lead always carries at least one typed (non checkbox/radio) field
1877// with a value. A form-like container whose only populated field is a
1878// checkbox/radio is a consent/cookie dialog, not a form — never fire a lead for
1879// it. Keys off DOM field TYPE, never payload values, so real leads whose data
1880// merely contains vendor-name substrings are unaffected. [HP-3292]
1881var _leadFields=container.querySelectorAll("input:not([type=hidden]):not([type=submit]):not([type=button]):not
1881([type=checkbox]):not([type=radio]),textarea,select");
1882var _hasLeadField=false;
1883for(var _lf=0;_lf<_leadFields.length;_lf++){if((_leadFields[_lf].value||"").trim()){_hasLeadField=true;break;}}
1884if(!_hasLeadField)return;
1885var data=collect(container);
1886if(Object.keys(data).length===0)return;
1887// Skip if all field values are empty (button click near empty form fields)
1888var hasAnyValue=Object.keys(data).some(function(k){return data[k]&&String(data[k]).trim();});
1889if(!hasAnyValue)return;
1890lastFakeSubmit=now;
1891container.setAttribute("data-htmlpub-fake-form","");
1892submitData(container,data);
1893// Notify analytics script about this fake form submission
1894try{document.dispatchEvent(new CustomEvent("htmlpub:form-submit",{detail:{formIdentifier:formId(container)}}));}catch(e){}
1895},true);
1896
1897// Always report form count to parent (even for non-owner views)
1898function reportFormCount(){
1899var forms=document.querySelectorAll("form");
1900var count=forms.length;
1901var labels=[];
1902for(var i=0;i<forms.length;i++){
1903var ff=forms[i];
1904var label=ff.getAttribute("aria-label")||ff.getAttribute("name")||ff.getAttribute("id")||"";
1905if(!label){
1906var heading=ff.querySelector("h1,h2,h3,h4,h5,h6,label,legend");
1907if(heading) label=heading.textContent.trim().substring(0,50);
1908}
1909labels.push(label||("Form "+(i+1)));
1910}
1911// needsCaptcha tells the parent shell (sandbox path) to render a Turnstile
1912// widget on its own origin: the sandboxed iframe has an opaque origin so it
1913// can't run Turnstile itself. Owner previews never require the token.
1914var _hpNeedsCaptcha=!_hpIsOwner&&!!document.querySelector("form[data-persist-captcha]");
1915if(inSandbox) parent.postMessage({type:"htmlpub-form-count",count:count,labels:labels,needsCaptcha:_hpNeedsCaptcha},"*");
1916}
1917function initFormCount(){
1918reportFormCount();
1919if(typeof MutationObserver!=="undefined"){
1920new MutationObserver(function(){reportFormCount();}).observe(document.body||document.documentElement,{childList:true,subtree:true});
1921}
1922}
1923if(document.readyState==="loading"){document.addEventListener("DOMContentLoaded",initFormCount);}
1924else{initFormCount();}
1925
1926// Leadbox/leadbar iframe form bridge. SYNC with worker-pages/src/scripts/forms.ts.
1927window.addEventListener("message",function(e){
1928if(!e||!e.data||e.data.type!=="htmlpub:iframe-form-submit")return;
1929var fid=String(e.data.formIdentifier||"iframe-form").slice(0,255);
1930var fsid=e.data.formSourceId?String(e.data.formSourceId).slice(0,255):"";
1931var raw=e.data.data;
1932if(!raw||typeof raw!=="object")return;
1933var clean={};
1934for(var k in raw){
1935if(!Object.prototype.hasOwnProperty.call(raw,k))continue;
1936if(looksLikeRawId(k))continue;
1937if(_blocklist.test(k))continue;
1938clean[k]=raw[k];
1939}
1940var redirect=typeof e.data.redirect==="string"?e.data.redirect:"";
1941var relayCaptcha=typeof e.data.captchaToken==="string"?e.data.captchaToken:undefined;
1942fetch(api,{method:"POST",keepalive:true,headers:{"Content-Type":"application/json"},body:JSON.stringify({pageId:pageId,pageSlug:slug,formIdentifier:fid,formSourceId:fsid,data:clean,captchaToken:relayCaptcha})}).then(function(r){if(r.ok){if(redirect)maybeRedirectUrl(redirect);}else{reportFail("http_error",r.status,fid);}}).catch(function(){reportFail("network",0,fid);});
1943},false);
1944})();
1945</script>
1945<script data-htmlpub-popups>
1946(function(){
1947"use strict";
1948
1949var SCOPE = "pathname";
1950var PAGE_ID = "rm90klif4ys3o04jw9zgjfpw";
1951
1952var DEFAULTS = {
1953  overlay:   { color: "#000000", opacity: 0.5,  clickToClose: true },
1954  close:     { enabled: true, position: "top-right", escapeToClose: true },
1955  animation: { open: "fade", close: "fade", durationMs: 250 },
1956  frequency: { visitsBefore: 0, daysCooldown: 0 }
1957};
1958
1959function parseJsonAttr(raw, fallback) {
1960  if (!raw) return fallback;
1961  try {
1962    var p = JSON.parse(raw);
1963    if (p && typeof p === "object") return p;
1964  } catch(e) {}
1965  return fallback;
1966}
1967
1968// Back-compat: pages published before the data-persist-popup* rename carry the
1969// legacy data-htmlpub-popup* names. Read canonical first, fall back to legacy.
1970// suffix is "" for the marker, "-name", "-overlay", "-target", etc.
1971function pAttr(el, suffix) {
1972  var v = el.getAttribute("data-persist-popup" + suffix);
1973  return v !== null ? v : el.getAttribute("data-htmlpub-popup" + suffix);
1974}
1975
1976function readConfig(el) {
1977  var overlay   = parseJsonAttr(pAttr(el, "-overlay"),   {});
1978  var closeCfg  = parseJsonAttr(pAttr(el, "-close"),     {});
1979  var anim      = parseJsonAttr(pAttr(el, "-animation"), {});
1980  var triggers  = parseJsonAttr(pAttr(el, "-triggers"),  {});
1981  var frequency = parseJsonAttr(pAttr(el, "-frequency"), {});
1982  return {
1983    overlay: {
1984      color:        typeof overlay.color === "string"   ? overlay.color   : DEFAULTS.overlay.color,
1985      opacity:      typeof overlay.opacity === "number" ? overlay.opacity : DEFAULTS.overlay.opacity,
1986      clickToClose: overlay.clickToClose !== false
1987    },
1988    close: {
1989      enabled:        closeCfg.enabled !== false,
1990      position:       closeCfg.position === "top-left" ? "top-left" : "top-right",
1991      escapeToClose:  closeCfg.escapeToClose !== false
1992    },
1993    animation: {
1994      open:        typeof anim.open === "string"  ? anim.open       : DEFAULTS.animation.open,
1995      close:       typeof anim.close === "string" ? anim.close      : DEFAULTS.animation.close,
1996      durationMs:  typeof anim.durationMs === "number" ? anim.durationMs : DEFAULTS.animation.durationMs
1997    },
1998    triggers: triggers,
1999    frequency: {
2000      visitsBefore: typeof frequency.visitsBefore === "number" ? frequency.visitsBefore : 0,
2001      daysCooldown: typeof frequency.daysCooldown === "number" ? frequency.daysCooldown : 0
2002    },
2003    // "none" = the marker's content owns its own chrome, so this runtime paints none of it.
2004    // Migrated Classic pop-ups: the marker holds an <iframe> whose document already draws the
2005    // mask, centring, card and close button. Nova-authored pop-ups never set it.
2006    chrome: pAttr(el, "-chrome") === "none" ? "none" : "default"
2007  };
2008}
2009
2010// ── LocalStorage helpers — scoped per pathname so two pages on the same
2011// origin track their popups independently. Wrapped in try/catch because
2012// Safari private mode + iframe sandboxing can throw on access.
2013var STORE_PREFIX = SCOPE === "popup-id" ? "_hpup:popup:" : ("_hpup:" + (location.pathname || "/") + ":");
2014function lsGet(key) {
2015  try { return localStorage.getItem(STORE_PREFIX + key); } catch(e) { return null; }
2016}
2017function lsSet(key, value) {
2018  try { localStorage.setItem(STORE_PREFIX + key, value); } catch(e) {}
2019}
2020
2021// Bump the visit counter once per pageload. Used by visitsBefore. In embed
2022// (popup-id) scope callers pass the popup id as a suffix so each standalone
2023// popup tracks its own visit count — a busy host page with two embedded popups
2024// must not let one popup's pageviews satisfy another's visitsBefore gate.
2025function recordVisit(suffix) {
2026  var key = "visits" + (suffix ? ":" + suffix : "");
2027  var prev = parseInt(lsGet(key) || "0", 10);
2028  if (!isFinite(prev) || prev < 0) prev = 0;
2029  lsSet(key, String(prev + 1));
2030  return prev + 1;
2031}
2032
2033function isEligible(cfg, popupId, currentVisit) {
2034  // visitsBefore: 0 = show on first visit. So eligible iff currentVisit > visitsBefore.
2035  if (cfg.frequency.visitsBefore > 0 && currentVisit <= cfg.frequency.visitsBefore) return false;
2036  // daysCooldown: don't reshow until N days have passed since last close.
2037  if (cfg.frequency.daysCooldown > 0) {
2038    var last = parseInt(lsGet("close:" + popupId) || "0", 10);
2039    if (last > 0) {
2040      var elapsedMs = Date.now() - last;
2041      if (elapsedMs < cfg.frequency.daysCooldown * 86400000) return false;
2042    }
2043  }
2044  return true;
2045}
2046
2047function composeRgba(color, opacity) {
2048  var hex = color.match(/^#([0-9a-f]{6})$/i);
2049  if (hex) {
2050    var n = parseInt(hex[1], 16);
2051    return "rgba(" + ((n >> 16) & 0xff) + "," + ((n >> 8) & 0xff) + "," + (n & 0xff) + "," + opacity + ")";
2052  }
2053  var rgb = color.match(/^rgb(?:a)?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
2054  if (rgb) return "rgba(" + rgb[1] + "," + rgb[2] + "," + rgb[3] + "," + opacity + ")";
2055  return "rgba(0,0,0," + opacity + ")";
2056}
2057
2058function animStart(kind) {
2059  if (kind === "slide-up")   return { opacity: 0, transform: "translate(-50%, -40%)" };
2060  if (kind === "slide-down") return { opacity: 0, transform: "translate(-50%, -60%)" };
2061  if (kind === "zoom")       return { opacity: 0, transform: "translate(-50%, -50%) scale(0.92)" };
2062  if (kind === "none")       return { opacity: 1, transform: "translate(-50%, -50%)" };
2063  return { opacity: 0, transform: "translate(-50%, -50%)" }; // fade
2064}
2065function animEnd() {
2066  return { opacity: 1, transform: "translate(-50%, -50%) scale(1)" };
2067}
2068
2069// Body scroll lock — refcounted because two popups can technically be in
2070// the open state at once (different triggers firing back-to-back). The
2071// first lock saves the body's prior overflow + padding-right and replaces
2072// them; subsequent locks just bump the count. We only restore once every
2073// popup that locked has closed, so a still-open popup keeps the page held.
2074// padding-right compensates for the scrollbar disappearing — without it,
2075// the dimmed page behind the overlay shifts right at the moment of open.
2076var _hpupLockCount = 0;
2077var _hpupSavedOverflow = "";
2078var _hpupSavedPaddingRight = "";
2079function lockBodyScroll() {
2080  if (_hpupLockCount === 0) {
2081    var scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
2082    _hpupSavedOverflow = document.body.style.overflow || "";
2083    _hpupSavedPaddingRight = document.body.style.paddingRight || "";
2084    if (scrollbarWidth > 0) document.body.style.paddingRight = scrollbarWidth + "px";
2085    document.body.style.overflow = "hidden";
2086  }
2087  _hpupLockCount++;
2088}
2089function unlockBodyScroll() {
2090  if (_hpupLockCount === 0) return;
2091  _hpupLockCount--;
2092  if (_hpupLockCount === 0) {
2093    if (_hpupSavedOverflow) document.body.style.overflow = _hpupSavedOverflow;
2094    else document.body.style.removeProperty("overflow");
2095    if (_hpupSavedPaddingRight) document.body.style.paddingRight = _hpupSavedPaddingRight;
2096    else document.body.style.removeProperty("padding-right");
2097  }
2098}
2099
2100// Build the overlay backdrop. Returns the element. Wired with the right
2101// pointer behaviour for the popup's clickToClose setting.
2102function buildOverlay(cfg, onOverlayClick) {
2103  var ov = document.createElement("div");
2104  ov.setAttribute("data-htmlpub-popup-backdrop", "");
2105  ov.style.cssText = "position:fixed;inset:0;background:" + composeRgba(cfg.overlay.color, cfg.overlay.opacity) +
2106    ";z-index:2147483646;opacity:0;transition:opacity " + cfg.animation.durationMs + "ms ease;";
2107  if (cfg.overlay.clickToClose) ov.addEventListener("click", onOverlayClick);
2108  return ov;
2109}
2110
2111// Inject a close ✕ button if the popup doesn't already have one. We mark our
2112// injected button so re-show calls don't duplicate it.
2113function ensureCloseButton(el, cfg, onClose) {
2114  if (el.querySelector("[data-htmlpub-popup-close-btn]")) return;
2115  if (!cfg.close.enabled) return;
2116  var btn = document.createElement("button");
2117  btn.setAttribute("type", "button");
2118  btn.setAttribute("aria-label", "Close pop-up");
2119  btn.setAttribute("data-htmlpub-popup-close-btn", "");
2120  var posStyle = cfg.close.position === "top-left" ? "left:12px;" : "right:12px;";
2121  btn.style.cssText = "position:absolute;top:12px;" + posStyle +
2122    "width:28px;height:28px;border-radius:9999px;border:0;background:rgba(0,0,0,0.45);color:#fff;" +
2123    "font:16px/1 system-ui,-apple-system,sans-serif;cursor:pointer;display:flex;align-items:center;" +
2124    "justify-content:center;z-index:1;padding:0;";
2125  btn.textContent = "×";
2126  btn.addEventListener("click", function(e){ e.preventDefault(); e.stopPropagation(); onClose(); });
2127  // Ensure the popup itself is a positioning context so absolute close button
2128  // anchors to it. Cheap: only sets if not already positioned.
2129  var pos = getComputedStyle(el).position;
2130  if (pos === "static" || !pos) el.style.position = "relative";
2131  el.appendChild(btn);
2132}
2133
2134// ── Per-popup wiring ──────────────────────────────────────────────────────
2135// Each popup gets its own controller closing over its element + config +
2136// trigger cleanup so we can tear down listeners on close.
2137
2138// HP-1557 — fire an analytics event for this popup. Hands off to the
2139// analytics.ts buffer via window.__hpupPushEvent (registered there). If
2140// analytics isn't loaded for some reason we silently no-op rather than
2141// breaking the popup runtime.
2142function emitPopupEvent(eventType, popup, extras) {
2143  if (typeof window.__hpupPushEvent !== "function") return;
2144  var payload = {
2145    popupId: popup.getAttribute("id") || "",
2146    popupName: (pAttr(popup, "-name") || "").replace(/^\s+|\s+$/g, "")
2147  };
2148  // Standalone-embed pop-ups carry their page id via data-hp-popup-cuid (set by
2149  // the embed bundle) so analytics can attribute to a specific pop-up; the DOM
2150  // id in popupId is a shared name-slug. In-page pop-ups have no cuid so this
2151  // stays absent.
2152  var _hpCuid = popup.getAttribute("data-hp-popup-cuid");
2153  if (_hpCuid) payload.popupCuid = _hpCuid;
2154  if (extras && typeof extras === "object") {
2155    for (var k in extras) {
2156      if (Object.prototype.hasOwnProperty.call(extras, k)) payload[k] = extras[k];
2157    }
2158  }
2159  window.__hpupPushEvent(eventType, payload);
2160}
2161
2162// Single-open invariant: only one popup may be visible at a time.
2163// show() force-closes whatever is up before painting itself.
2164var _hpupCurrentOpen = null;
2165
2166function wirePopup(el, currentVisit) {
2167  var cfg = readConfig(el);
2168  var popupId = el.getAttribute("id") || "";
2169  if (!popupId) return;
2170  // The editor stores popups hidden via inline display:none. We never strip
2171  // that — we toggle it at show/close time so a never-shown popup stays
2172  // invisible (matching its rendered state in the DOM).
2173  var originalDisplay = el.style.display || "";
2174  var isOpen = false;
2175  var overlay = null;
2176  var keyListener = null;
2177  var sizeObserver = null;
2178  var cleanups = []; // trigger listener removers; called on first show
2179
2180  function cleanupTriggers() {
2181    for (var i = 0; i < cleanups.length; i++) {
2182      try { cleanups[i](); } catch(e) {}
2183    }
2184    cleanups = [];
2185  }
2186
2187  // Why a popup was opened — informs the parent analytics event so the
2188  // dashboard can distinguish "fired by timer" vs. "explicit user click".
2189  // Updated by callers before show() and reset to null after emit.
2190  var lastShowReason = null;
2191
2192  // HP-4147 — the overflow choice in show() is a measurement, and measurements
2193  // go stale. Content can grow AFTER open: an inline checkout's Stripe Payment
2194  // Element progressively reveals card fields and then a "Save your info"
2195  // section as the buyer types. A popup that opened short (overflow:visible, so
2196  // its shadow paints) and then grows past maxHeight spills below the viewport
2197  // instead of scrolling, and because show() locked the body scroll there is
2198  // nothing left for a touch drag to move — the Pay button becomes unreachable
2199  // and closing + reopening is the only way out (reopening re-runs show(), which
2200  // re-measures against the now-tall content and lands on "auto"). So re-decide
2201  // on every resize — but only ever upgrade to "auto", never back to "visible".
2202  //
2203  // One-way on purpose. Reverting to "visible" when content shrinks back under
2204  // maxHeight sounds harmless (it hands HP-3771 its shadow back) but it
2205  // re-releases horizontal spill that "auto" had been containing, so a wide
2206  // child paints outside the popup over the page, and it discards scrollTop,
2207  // bouncing the buyer to the top of the form. Both were measured in Chromium
2208  // at a Pixel viewport. Staying in "auto" costs only the shadow, and only for
2209  // the rest of an open in which content already exceeded the viewport: "auto"
2210  // paints no scrollbar once content fits again, it merely clips. close()
2211  // strips every inline style, so the next open re-decides from scratch.
2212  function syncOverflow() {
2213    if (!isOpen) return;
2214    // Guarded on a real change. Toggling overflow lays out a scrollbar, which
2215    // resizes content, which re-enters this observer — an unguarded write there
2216    // is a feedback loop that never settles.
2217    if (el.scrollHeight > el.clientHeight + 1 && el.style.overflow !== "auto") {
2218      el.style.overflow = "auto";
2219    }
2220  }
2221
2222  function observeSize() {
2223    if (sizeObserver || typeof ResizeObserver === "undefined") return;
2224    sizeObserver = new ResizeObserver(syncOverflow);
2225    // Observing el catches the growth that crosses maxHeight, since its own box
2226    // tracks content right up to the clamp. Observing its element children
2227    // catches growth that continues past the clamp, where el's box is pinned and
2228    // reports no resize at all — content 1px under maxHeight that then expands
2229    // would otherwise never re-decide.
2230    sizeObserver.observe(el);
2231    for (var childIndex = 0; childIndex < el.children.length; childIndex++) {
2232      sizeObserver.observe(el.children[childIndex]);
2233    }
2234  }
2235
2236  function unobserveSize() {
2237    if (!sizeObserver) return;
2238    sizeObserver.disconnect();
2239    sizeObserver = null;
2240  }
2241
2242  function show(reason) {
2243    if (isOpen) return;
2244    if (_hpupCurrentOpen && _hpupCurrentOpen !== el && typeof _hpupCurrentOpen._hpupClose === "function") {
2245      _hpupCurrentOpen._hpupClose("replaced", true);
2246    }
2247    _hpupCurrentOpen = el;
2248    isOpen = true;
2249    lastShowReason = reason || "explicit";
2250    cleanupTriggers();
2251    lockBodyScroll();
2252    // chrome:none — the content paints its own ✕ (Classic's .js-close, inside the iframe).
2253    var bare = cfg.chrome === "none";
2254    if (!bare) ensureCloseButton(el, cfg, function(){ close("close_button"); });
2255
2256    var start = animStart(cfg.animation.open);
2257    var end   = animEnd();
2258
2259    // Centered fixed positioning with a high z-index so we float above any
2260    // page content + checkout / chat widgets.
2261    el.style.display      = "block";
2262    el.style.position     = "fixed";
2263    el.style.zIndex       = "2147483647";
2264
2265    if (bare) {
2266      // Full-viewport shell. An <iframe> has no intrinsic content height (~150px, and it never
2267      // reports its content size to the parent), so the shrink-wrapped-card model below cannot
2268      // measure it — centring, the maxHeight/maxWidth clamps and the overflow logic all assume a
2269      // box that sizes to its content. Reproduce CDA's model instead: the frame fills the
2270      // viewport and the Classic document inside owns mask, centring and card.
2271      //
2272      // No transform either: every animStart transform carries translate(-50%,-50%), which
2273      // centres a card and would shove a full-viewport shell off screen. Opacity alone.
2274      el.style.top        = "0";
2275      el.style.left       = "0";
2276      el.style.width      = "100%";
2277      el.style.height     = "100%";
2278      el.style.opacity    = String(start.opacity);
2279      el.style.transition = "opacity " + cfg.animation.durationMs + "ms ease";
2280      openClassicFrame(el);
2281    } else {
2282    el.style.top          = "50%";
2283    el.style.left         = "50%";
2284    el.style.transform    = start.transform;
2285    el.style.opacity      = String(start.opacity);
2286    el.style.maxHeight    = "calc(100vh - 32px)";
2287    el.style.maxWidth     = "calc(100vw - 32px)";
2288    // HP-3771 — "auto" clips any box-shadow set on the popup's own content:
2289    // box-shadow doesn't affect layout size, so this element shrink-wraps to its
2290    // content and its own overflow then clips the shadow it should cast.
2291    //
2292    // But "visible" unconditionally is not safe either. This element is
2293    // position:fixed and centred, so content taller than maxHeight does not
2294    // clip — it spills past the viewport with no scrollbar and nothing can
2295    // reach it. A lead-capture form taller than the viewport would lose its
2296    // submit button. So decide per popup, after layout: overflow stays "auto"
2297    // only when the content genuinely overflows (scrolling wins), and is
2298    // "visible" otherwise (shadow wins). Short popups are the overwhelmingly
2299    // common case, so the shadow renders for almost everyone.
2300    //
2301    // syncOverflow re-applies this same rule for the rest of the open (HP-4147).
2302    el.style.overflow     = "visible";
2303    syncOverflow();
2304    el.style.transition   = "opacity " + cfg.animation.durationMs + "ms ease, transform " + cfg.animation.durationMs + "ms ease";
2305    }
2306
2307    if (!el.parentNode) document.body.appendChild(el);
2308    if (!bare) {
2309      // Backdrop. Inserted before the popup so the popup naturally paints on top.
2310      // chrome:none skips it — the Classic document paints its own .js-leadbox-mask, and a second
2311      // backdrop would darken the page twice.
2312      overlay = buildOverlay(cfg, function(){ close("overlay_click"); });
2313      (el.parentNode || document.body).insertBefore(overlay, el);
2314
2315      // Started only once el is attached, so the observer's first delivery measures
2316      // real geometry rather than the 0x0 of a detached node.
2317      observeSize();
2318    }
2319
2320    // Force reflow so the next style change animates from start, not jumps.
2321    /* eslint-disable-next-line no-unused-expressions */ void el.offsetWidth;
2322
2323    el.style.opacity   = String(end.opacity);
2324    if (!bare) el.style.transform = end.transform;
2325    if (overlay) overlay.style.opacity = "1";
2326
2327    if (cfg.close.escapeToClose) {
2328      keyListener = function(e) { if (e.key === "Escape") close("escape"); };
2329      document.addEventListener("keydown", keyListener);
2330    }
2331
2332    emitPopupEvent("popup_view", el, { reason: lastShowReason });
2333    lastShowReason = null;
2334  }
2335
2336  function close(reason, hard) {
2337    if (!isOpen) return;
2338    isOpen = false;
2339    emitPopupEvent("popup_dismiss", el, { reason: reason || "unknown" });
2340    var start = animStart(cfg.animation.close);
2341    el.style.opacity   = String(start.opacity);
2342    // Same reason as show(): the close transform centres a card, and applying it to a
2343    // full-viewport shell would slide it off screen instead of fading it out.
2344    if (cfg.chrome !== "none") el.style.transform = start.transform;
2345    if (overlay) overlay.style.opacity = "0";
2346
2347    if (keyListener) {
2348      document.removeEventListener("keydown", keyListener);
2349      keyListener = null;
2350    }
2351
2352    var afterAnim = function() {
2353      if (_hpupCurrentOpen === el) _hpupCurrentOpen = null;
2354      unobserveSize();
2355      el.style.display = originalDisplay || "none";
2356      el.style.removeProperty("position");
2357      el.style.removeProperty("top");
2358      el.style.removeProperty("left");
2359      el.style.removeProperty("transform");
2360      el.style.removeProperty("opacity");
2361      el.style.removeProperty("z-index");
2362      el.style.removeProperty("max-height");
2363      el.style.removeProperty("max-width");
2364      // Only the chrome:none branch sets these, and teardown must remove exactly what this show
2365      // set — a marker can legitimately carry an author's own inline width/height, and stripping
2366      // it on close would resize the pop-up the second time it opens.
2367      if (cfg.chrome === "none") {
2368        el.style.removeProperty("width");
2369        el.style.removeProperty("height");
2370      }
2371      el.style.removeProperty("overflow");
2372      el.style.removeProperty("transition");
2373      if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay);
2374      overlay = null;
2375      // Tear down the close button — its click handler closes over THIS
2376      // show's overlay reference. Leaving it across opens means a second
2377      // show's X would call the previous close() with a stale overlay and
2378      // leave the current overlay floating.
2379      var existingBtn = el.querySelector("[data-htmlpub-popup-close-btn]");
2380      if (existingBtn && existingBtn.parentNode) existingBtn.parentNode.removeChild(existingBtn);
2381      // Release the scroll lock only after the popup is fully torn down so
2382      // the page doesn't pop back to scrollable while the close animation
2383      // is still painting.
2384      unlockBodyScroll();
2385      lsSet("close:" + popupId, String(Date.now()));
2386    };
2387    if (hard || cfg.animation.close === "none") afterAnim();
2388    else setTimeout(afterAnim, cfg.animation.durationMs);
2389  }
2390
2391  // ── Triggers ────────────────────────────────────────────────────────────
2392  // Only wire if the popup is eligible to show this pageload (frequency
2393  // rules). Click-to-open via [data-htmlpub-popup-target] always works,
2394  // regardless of eligibility — it's an explicit user action.
2395  var eligible = isEligible(cfg, popupId, currentVisit);
2396
2397  if (eligible) {
2398    var t = cfg.triggers || {};
2399
2400    if (t.timed && typeof t.timed.delaySec === "number") {
2401      var delayMs = Math.max(0, t.timed.delaySec) * 1000;
2402      var timer = setTimeout(function(){ show("timed"); }, delayMs);
2403      cleanups.push(function(){ clearTimeout(timer); });
2404    }
2405
2406    if (t.exitIntent === true) {
2407      // Mobile / touch devices don't have a meaningful "exit toward chrome"
2408      // gesture. Standard heuristic: skip if no mouse pointer is available.
2409      if (!matchMedia("(pointer: coarse)").matches) {
2410        var onMove = function(e) {
2411          if (e.clientY < 8) show("exit_intent");
2412        };
2413        document.addEventListener("mousemove", onMove);
2414        cleanups.push(function(){ document.removeEventListener("mousemove", onMove); });
2415      }
2416    }
2417
2418    if (typeof t.scrollPercent === "number" && t.scrollPercent > 0) {
2419      var threshold = Math.max(1, Math.min(100, t.scrollPercent));
2420      var onScroll = function() {
2421        var doc = document.documentElement;
2422        var scrolled = (window.scrollY || doc.scrollTop || 0);
2423        var max = (doc.scrollHeight - doc.clientHeight) || 1;
2424        var pct = (scrolled / max) * 100;
2425        if (pct >= threshold) show("scroll");
2426      };
2427      window.addEventListener("scroll", onScroll, { passive: true });
2428      cleanups.push(function(){ window.removeEventListener("scroll", onScroll); });
2429    }
2430  }
2431
2432  // Note: in-popup form submits and CTA clicks are NOT emitted as separate
2433  // popup_* events. They already fire as form_submit / element_click via
2434  // analytics.ts's global listeners, which now attach popupId + popupName
2435  // to the payload so customers can still see which pop-up the conversion
2436  // came from while continuing to track the standard goal types.
2437
2438  // Expose the show function on the element so [data-htmlpub-popup-target]
2439  // clicks (wired globally below) can find and trigger it without re-parsing
2440  // the config. _hpupShow / _hpupClose are deliberately namespaced + ugly to
2441  // avoid colliding with user code.
2442  el._hpupShow = show;
2443  el._hpupClose = close;
2444  // Returned so boot() can flush unfired trigger listeners on pagehide.
2445  return cleanupTriggers;
2446}
2447
2448// A served Classic leadbox renders INACTIVE: its controller only adds js-active to the dialog
2449// and mask when it receives an "open" message, so a frame nobody tells to open is a full-viewport
2450// sheet showing nothing.
2451//
2452// TWO WIRE FORMATS, because there are two leadbox builds in the corpus and they do NOT agree —
2453// measured across all 462 live clones on 2026-09-09, not inferred from one of them (D71):
2454//
2455//   · newer build (410 of 462): a handleMessage controller that accepts EITHER the bare string
2456//     "open" or {type:"open"}.
2457//   · OLDER build (52 of 462): a hand-rolled listener whose whole test is
2458//         window.addEventListener('message', function(e){ e && e.data && 'open' === e.data && j.open() })
2459//     read verbatim out of the served bundle. The test 'open' === e.data is a STRING identity check, so
2460//     an object payload fails it silently and that pop-up never opens.
2461//
2462// This function used to post ONLY {type:"open"}, and the comment here used to assert both builds
2463// took it. They do not: every old-build pop-up rendered a full-viewport dark sheet with an
2464// invisible modal — no error, no console message, and zero lead capture. So post BOTH forms. The
2465// duplicate is harmless in each direction: the old build ignores the object, and the newer one's
2466// open() just re-adds the class and refocuses.
2467//
2468// The string form is also what Classic's own embed.js sent, which is why the defect stayed hidden
2469// on the two pages that still carried embed.js — its copy opened correctly beside our blank one.
2470// (The close relay below had this right already: it reads Classic's bare-string "close leadbox"
2471// off the wire. Open is the direction that was guessed.)
2472//
2473// Posted on both paths because the ordering is not ours to control: if the frame is already
2474// loaded the immediate post lands, and if it is not, the load listener catches it. Opening twice
2475// is harmless — open() just re-adds the class and refocuses.
2476function openClassicFrame(el) {
2477  var frame = el.querySelector("iframe");
2478  if (!frame) return;
2479  var post = function() {
2480    // targetOrigin "*": the frame is served sandboxed, so its origin is opaque and cannot be
2481    // named. The payload is a bare open signal carrying nothing worth protecting.
2482    try {
2483      if (!frame.contentWindow) return;
2484      // String FIRST: it is the format the old build needs, and the one that was missing.
2485      frame.contentWindow.postMessage("open", "*");
2486      frame.contentWindow.postMessage({ type: "open" }, "*");
2487    } catch(e) {}
2488  };
2489  post();
2490  frame.addEventListener("load", post);
2491}
2492
2493// Global close relay. A chrome:none pop-up's ✕ lives INSIDE its iframe (Classic's .js-close), so
2494// the click is invisible to this document — the frame posts htmlpub:leadbox-close instead and we
2495// close the marker holding it. Without this the Classic card hides itself and the full-viewport
2496// iframe stays put, covering the page with an invisible sheet that swallows every click.
2497//
2498// Identify the marker by matching iframe.contentWindow against e.source, NOT by the message's
2499// uuid: a page can carry several migrated pop-ups, and a uuid can be placed more than once. This
2500// is the technique CDA's own injected handler used.
2501function handleCloseRelay(e) {
2502  if (!e || !e.data) return;
2503  // Classic's own leadbox controller posts the bare STRING "close leadbox" to window.parent when
2504  // it closes (300ms after the fade, from its close() method) — read out of the served bundle, not
2505  // guessed. It is already the wire format, so nothing needs injecting into the frame. The
2506  // namespaced form is accepted too, for anything we serve ourselves later.
2507  var type = typeof e.data === "string" ? e.data : e.data.type;
2508  if (type !== "close leadbox" && type !== "htmlpub:leadbox-close") return;
2509  var frames = document.querySelectorAll("[data-persist-popup] iframe, [data-htmlpub-popup] iframe");
2510  for (var i = 0; i < frames.length; i++) {
2511    if (frames[i].contentWindow !== e.source) continue;
2512    var marker = frames[i].closest("[data-persist-popup], [data-htmlpub-popup]");
2513    if (marker && typeof marker._hpupClose === "function") marker._hpupClose("close_relay");
2514    return;
2515  }
2516}
2517
2518// The migrated pop-up runs in a sandboxed cross-origin iframe and cannot read this page's anal
2518ytics
2519// session/visitor ids (opaque origin blocks its storage). Hand them over when the in-frame capture
2520// script asks — matching e.source to one of our own pop-up iframes so we only answer our own frames.
2521// The ids exist only once analytics consent has minted them, so with no consent nothing is sent and
2522// the ingest keeps the lead but skips the conversion event (there is no page_view session to join).
2523// The page id rides along either way, so the lead files under the page that was served, whatever
2524// URL the browser reports.
2525function handleSessionReq(e) {
2526  if (!e || !e.data || e.data.type !== "hp-popup-session-req" || !e.source) return;
2527  var frames = document.querySelectorAll("[data-persist-popup] iframe, [data-htmlpub-popup] iframe");
2528  var mine = false;
2529  for (var i = 0; i < frames.length; i++) { if (frames[i].contentWindow === e.source) { mine = true; break; } }
2530  if (!mine) return;
2531  var sid = null, vid = null;
2532  try { sid = sessionStorage.getItem("_hp_sid"); } catch(err) {}
2533  try { vid = localStorage.getItem("_hp_vid"); } catch(err) {}
2534  try { e.source.postMessage({ type: "hp-popup-session", sid: sid, vid: vid, pid: PAGE_ID }, "*"); } catch(err) {}
2535}
2536
2537// Global click-to-open dispatcher. Targets carry [data-persist-popup-target]
2538// (or legacy [data-htmlpub-popup-target]) or href="#popup-…" (for plain anchors
2539// that the link picker auto-generated).
2540function handleTargetClick(e) {
2541  var t = e.target;
2542  if (!t || !t.closest) return;
2543  var hit = t.closest("[data-persist-popup-target], [data-htmlpub-popup-target], a[href^='#popup-'], a[href^='#modal-'], a[href^='#dialog-']");
2544  if (!hit) return;
2545  var id = pAttr(hit, "-target");
2546  if (!id) {
2547    var href = hit.getAttribute("href") || "";
2548    if (href.charAt(0) === "#") id = href.slice(1);
2549  }
2550  if (!id) return;
2551  var popup = document.getElementById(id);
2552  if (!popup || pAttr(popup, "") === null) return;
2553  e.preventDefault();
2554  if (typeof popup._hpupShow === "function") popup._hpupShow("click_target");
2555}
2556
2557// ── Boot ──────────────────────────────────────────────────────────────────
2558
2559function flushTeardowns(list) {
2560  for (var j = 0; j < list.length; j++) {
2561    try { list[j](); } catch(e) {}
2562  }
2563}
2564
2565function boot() {
2566  if (SCOPE === "popup-id") {
2567    // Embed mode: each standalone popup ships its OWN copy of this runtime, so
2568    // a single global boot guard would make a second embedded popup bail. Wire
2569    // each popup once via a per-element marker, count visits per popup id, and
2570    // attach the global click / pagehide listeners only once across copies.
2571    var list = document.querySelectorAll("[data-persist-popup],[data-htmlpub-popup]");
2572    if (!list.length) return;
2573    if (!window.__hpupTeardowns) window.__hpupTeardowns = [];
2574    for (var n = 0; n < list.length; n++) {
2575      var pel = list[n];
2576      if (pel.__hpupWired) continue;
2577      pel.__hpupWired = true;
2578      var pt = wirePopup(pel, recordVisit(pel.getAttribute("id") || ""));
2579      if (typeof pt === "function") window.__hpupTeardowns.push(pt);
2580    }
2581    if (!window.__hpupClickWired) {
2582      window.__hpupClickWired = true;
2583      document.addEventListener("click", handleTargetClick, false);
2584      window.addEventListener("message", handleCloseRelay, false);
2585      window.addEventListener("message", handleSessionReq, false);
2586      window.addEventListener("pagehide", function(){ flushTeardowns(window.__hpupTeardowns || []); });
2587    }
2588    return;
2589  }
2590
2591  // In-page mode: one runtime per page wires every popup. Guard against
2592  // double-injection (HMR, accidental double-include).
2593  if (window.__hpupBooted) return;
2594  window.__hpupBooted = true;
2595  var popups = document.querySelectorAll("[data-persist-popup],[data-htmlpub-popup]");
2596  if (!popups.length) return;
2597  var visit = recordVisit();
2598  var teardowns = [];
2599  for (var i = 0; i < popups.length; i++) {
2600    var t = wirePopup(popups[i], visit);
2601    if (typeof t === "function") teardowns.push(t);
2602  }
2603  document.addEventListener("click", handleTargetClick, false);
2604  window.addEventListener("message", handleCloseRelay, false);
2605  window.addEventListener("message", handleSessionReq, false);
2606  // Flush unfired trigger listeners on pagehide.
2607  window.addEventListener("pagehide", function(){ flushTeardowns(teardowns); });
2608}
2609
2610if (document.readyState === "loading") {
2611  document.addEventListener("DOMContentLoaded", boot);
2612} else {
2613  boot();
2614}
2615
2616})();
2617</script>
2617<script data-htmlpub-countdown-runtime>
2618(function(){
2619var nodes=document.querySelectorAll('[data-htmlpub-countdown]');
2620if(!nodes.length)return;
2621function pad(n){n=String(n);return n.length<2?'0'+n:n;}
2622function setDigit(root,sel,val){var el=root.querySelector(sel);if(el)el.textContent=pad(val);}
2623function tick(node,endMs){
2624var diff=Math.max(0,endMs-Date.now());
2625var s=Math.floor(diff/1000);
2626var d=Math.floor(s/86400);s-=d*86400;
2627var h=Math.floor(s/3600);s-=h*3600;
2628var m=Math.floor(s/60);s-=m*60;
2629setDigit(node,'.countdown-days',d);
2630setDigit(node,'.countdown-hours',h);
2631setDigit(node,'.countdown-minutes',m);
2632setDigit(node,'.countdown-seconds',s);
2633if(diff===0){
2634var redirect=node.getAttribute('data-redirect');
2635var inFrame=false;try{inFrame=window!==window.top;}catch(e){inFrame=true;}
2636if(redirect&&!inFrame){try{window.location.href=redirect;}catch(e){}}
2637return true;
2638}
2639return false;
2640}
2641function endMsFor(node){
2642var mode=node.getAttribute('data-htmlpub-countdown');
2643if(mode==='fixed'){
2644var target=node.getAttribute('data-target');
2645var t=target?Date.parse(target):NaN;
2646return isNaN(t)?0:t;
2647}
2648if(mode==='evergreen'){
2649var dur=parseInt(node.getAttribute('data-duration')||'0',10);
2650if(!dur)return 0;
2651var idx=Array.prototype.indexOf.call(nodes,node);
2652var base='htmlpub_cd_'+(node.getAttribute('data-persist-countdown-id')||node.id||(location.pathname+'|'+dur+'|'+idx));
2653var ver=parseInt(node.getAttribute('data-persist-countdown-reset')||'0',10)||0;
2654var key=ver>0?base+'|v'+ver:base;
2655if(ver>0){try{for(var li=localStorage.length-1;li>=0;li--){var lk=localStorage.key(li);if(!lk)continue;if(lk===base){localStorage.removeItem(lk);continue;}if(lk.indexOf(base+'|v')===0){var lv=lk.slice(base.length+2);if(/^[0-9]+$/.test(lv)&&parseInt(lv,10)<ver){localStorage.removeItem(lk);}}}}catch(e){}}
2656var first=null;
2657try{first=localStorage.getItem(key);}catch(e){}
2658if(!first){first=String(Date.now());try{localStorage.setItem(key,first);}catch(e){}}
2659return parseInt(first,10)+dur*1000;
2660}
2661return 0;
2662}
2663for(var i=0;i<nodes.length;i++){(function(node){
2664var endMs=endMsFor(node);
2665if(!endMs)return;
2666if(tick(node,endMs))return;
2667var iv=setInterval(function(){if(tick(node,endMs))clearInterval(iv);},1000);
2668})(nodes[i]);}
2669})();
2670</script>
2670<script data-text-anim-runtime>
2671(function(){
2672var nodes=document.querySelectorAll('[data-persist-text-anim]');
2673if(!nodes.length)return;
2674try{if(window.matchMedia('(prefers-reduced-motion: reduce)').matches)return;}catch(e){}
2675var KEYFRAMES={
2676fadeIn:[{opacity:0},{opacity:1}],
2677slideUp:[{opacity:0,transform:'translateY(32px)'},{opacity:1,transform:'translateY(0)'}],
2678slideDown:[{opacity:0,transform:'translateY(-32px)'},{opacity:1,transform:'translateY(0)'}],
2679slideLeft:[{opacity:0,transform:'translateX(32px)'},{opacity:1,transform:'translateX(0)'}],
2680slideRight:[{opacity:0,transform:'translateX(-32px)'},{opacity:1,transform:'translateX(0)'}],
2681zoomIn:[{opacity:0,transform:'scale(0.85)'},{opacity:1,transform:'scale(1)'}],
2682blurIn:[{opacity:0,filter:'blur(8px)'},{opacity:1,filter:'blur(0)'}],
2683bounceIn:[{offset:0,opacity:0,transform:'translateY(40px)'},{offset:0.55,opacity:1,transform:'translateY(-8px)'},{offset:0.8,transform:'translateY(4px)'},{offset:1,transform:'translateY(0)'}]
2684};
2685var DEFAULTS={duration:600,delay:0,easing:'ease-out'};
2686function readInt(el,attr,fallback,min){
2687var n=parseInt(el.getAttribute(attr)||'',10);
2688if(!isFinite(n)||n<min)return fallback;
2689return n;
2690}
2691function arm(el){
2692var type=el.getAttribute('data-persist-text-anim')||'fadeIn';
2693var frames=KEYFRAMES[type];
2694if(!frames)return null;
2695var duration=readInt(el,'data-persist-text-anim-duration',DEFAULTS.duration,1);
2696var delay=readInt(el,'data-persist-text-anim-delay',DEFAULTS.delay,0);
2697var easing=el.getAttribute('data-persist-text-anim-easing')||DEFAULTS.easing;
2698try{
2699var anim=el.animate(frames,{duration:duration,delay:delay,easing:easing,fill:'both'});
2700anim.pause();
2701anim.currentTime=0;
2702return anim;
2703}catch(e){return null;}
2704}
2705if('IntersectionObserver' in window){
2706var io=new IntersectionObserver(function(entries){
2707for(var i=0;i<entries.length;i++){
2708var en=entries[i];
2709if(!en.isIntersecting)continue;
2710io.unobserve(en.target);
2711var armed=en.target._hpTextAnim;
2712if(!armed)continue;
2713en.target._hpTextAnim=null;
2714try{armed.play();}catch(e){}
2715}
2716},{threshold:0.15});
2717for(var j=0;j<nodes.length;j++){
2718var pending=arm(nodes[j]);
2719if(!pending)continue;
2720nodes[j]._hpTextAnim=pending;
2721io.observe(nodes[j]);
2722}
2723}else{
2724for(var k=0;k<nodes.length;k++){
2725var immediate=arm(nodes[k]);
2726if(immediate)try{immediate.play();}catch(e){}
2727}
2728}
2729})();
2730</script>
2730<script data-htmlpub-nav>
2731(function(){
2732"use strict";
2733
2734function closeAllSubmenus(except) {
2735  var panels = document.querySelectorAll('[data-persist-nav-submenu-panel]');
2736  for (var i = 0; i < panels.length; i++) {
2737    var panel = panels[i];
2738    if (panel === except) continue;
2739    panel.classList.remove('is-open');
2740    var trigger = panel.__hpNavTrigger;
2741    if (trigger) trigger.setAttribute('aria-expanded', 'false');
2742  }
2743}
2744
2745function wireSubmenus() {
2746  var triggers = document.querySelectorAll('[data-persist-nav-submenu]');
2747  if (!triggers.length) return;
2748  for (var i = 0; i < triggers.length; i++) {
2749    (function(trigger) {
2750      if (trigger.__hpNavWired) return;
2751      trigger.__hpNavWired = true;
2752      var panelId = trigger.getAttribute('data-persist-nav-submenu');
2753      var panel = panelId ? document.getElementById(panelId) : (trigger.parentElement ? trigger.parentElement.querySelector('[data-persist-nav-submenu-panel]') : null);
2754      if (!panel) return;
2755      panel.__hpNavTrigger = trigger;
2756      trigger.setAttribute('aria-haspopup', 'true');
2757      trigger.setAttribute('aria-expanded', 'false');
2758      trigger.addEventListener('click', function(e) {
2759        var isOpen = panel.classList.contains('is-open');
2760        e.preventDefault();
2761        e.stopPropagation();
2762        closeAllSubmenus(isOpen ? null : panel);
2763        panel.classList.toggle('is-open', !isOpen);
2764        trigger.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
2765      });
2766    })(triggers[i]);
2767  }
2768  document.addEventListener('click', function(e) {
2769    var t = e.target;
2770    if (t && t.closest && t.closest('[data-persist-nav-submenu-panel], [data-persist-nav-submenu]')) return;
2771    closeAllSubmenus(null);
2772  });
2773}
2774
2775function wireMobileMenu() {
2776  var toggles = document.querySelectorAll('[data-persist-nav-mobile-toggle], [data-persist-nav-tablet-toggle]');
2777  if (!toggles.length) return;
2778  for (var i = 0; i < toggles.length; i++) {
2779    (function(toggle) {
2780      if (toggle.__hpNavWired) return;
2781      toggle.__hpNavWired = true;
2782      var isTablet = toggle.hasAttribute('data-persist-nav-tablet-toggle');
2783      var targetId = isTablet ? toggle.getAttribute('data-persist-nav-tablet-toggle') : toggle.getAttribute('data-persist-nav-mobile-toggle');
2784      var panelSelector = isTablet ? '[data-persist-nav-tablet-panel]' : '[data-persist-nav-mobile-panel]';
2785      var panel = targetId ? document.getElementById(targetId) : (toggle.parentElement ? toggle.parentElement.querySelector(panelSelector) : null);
2786      if (!panel) return;
2787      toggle.setAttribute('aria-expanded', 'false');
2788      toggle.addEventListener('click', function(e) {
2789        var isOpen = panel.classList.contains('is-open');
2790        e.preventDefault();
2791        e.stopPropagation();
2792        if (isOpen) panel.classList.remove('is-open'); else panel.classList.add('is-open');
2793        toggle.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
2794      });
2795    })(toggles[i]);
2796  }
2797}
2798
2799function wireScrollBackground() {
2800  // Every nav this system manages, not just ones with scroll-bg/blur
2801  // currently set — a page published while an OLDER build still applied
2802  // blur directly to nav carries that inline backdrop-filter baked into
2803  // its served HTML even after blur is later turned off, so the self-heal
2804  // below has to run regardless of the CURRENT attribute state.
2805  var navs = document.querySelectorAll('[data-persist-nav-id]');
2806  if (!navs.length) return;
2807  for (var i = 0; i < navs.length; i++) {
2808    (function(nav) {
2809      // Self-heal: strip a stale inline backdrop-filter baked into a page
2810      // saved before this fix (HP-3558 round 15 follow-up) — see the
2811      // dedicated blur-layer comment below for why it must never live on
2812      // nav itself.
2813      if (nav.style.getPropertyValue('backdrop-filter') || nav.style.getPropertyValue('-webkit-backdrop-filter')) {
2814        nav.style.removeProperty('backdrop-filter');
2815        nav.style.removeProperty('-webkit-backdrop-filter');
2816      }
2817      // "Transparent" (data-persist-nav-always-transparent, HP-3558 round
2818      // 16) — the non-sticky counterpart of scroll-bg above. "Useful over a
2819      // hero image" only holds if nav visually overlaps whatever follows
2820      // it, so lift it out of flow the same way Sticky's own
2821      // position:fixed/sticky already does, rather than leaving it
2822      // position:static (which just reveals blank page background above
2823      // the hero, not the hero itself — see iframe-script/utils.ts's
2824      // ensureNavScrollBackground for the matching editor-side comment).
2825      var alwaysTransparent = nav.hasAttribute('data-persist-nav-always-transparent');
2826      if (alwaysTransparent) {
2827        var navCsForOverlay = window.getComputedStyle(nav);
2828        if (!navCsForOverlay || (navCsForOverlay.position !== 'fixed' && navCsForOverlay.position !== 'sticky')) {
2829          nav.style.position = 'absolute';
2830          nav.style.top = '0px';
2831          nav.style.left = '0px';
2832          nav.style.right = '0px';
2833          if (!nav.style.zIndex) nav.style.zIndex = '1000';
2834        }
2835      }
2836      // Blur needs no scroll-reactivity — a blurred OPAQUE background
2837      // looks identical to an unblurred one, so it's simplest and correct
2838      // to apply it once, unconditionally, regardless of scroll state.
2839      // Applied to a dedicated decorative layer BEHIND nav's real
2840      // content, never to nav itself — backdrop-filter promotes whatever
2841      // element carries it into a new containing block for any
2842      // position:fixed descendant, which silently shrank the full-screen/
2843      // bottom-drawer hamburger panel (both position:fixed) down to nav's
2844      // own small box instead of the viewport when it lived on nav
2845      // directly (HP-3558 round 15 follow-up: "the full-screen hamburger
2846      // menu... ended up being covered by the next section").
2847      var blur = nav.getAttribute('data-persist-nav-scroll-blur');
2848      if (blur) {
2849        var blurLayer = nav.querySelector('[data-persist-nav-scroll-blur-layer]');
2850        if (!blurLayer) {
2851          blurLayer = document.createElement('div');
2852          blurLayer.setAttribute('data-persist-nav-scroll-blur-layer', '');
2853          // z-index:-1 required — without it, backdrop-filter's own
2854          // compositing samples nav's LATER siblings (logo/links/CTA) too,
2855          // visibly blurring them despite painting after this layer in
2856          // plain DOM order. Safe from escaping behind nav's own box into
2857          // the page below because Sticky header always sets an explicit,
2858          // non-auto nav.style.zIndex — position:fixed/sticky + a non-auto
2859          // z-index already makes nav its own stacking context.
2860          blurLayer.style.cssText = 'position:absolute;inset:0;pointer-events:none;z-index:-1;';
2861          nav.insertBefore(blurLayer, nav.firstChild);
2862          var navCs = window.getComputedStyle(nav);
2863          if (!navCs || navCs.position === 'static' || !navCs.position) nav.style.position = 'relative';
2864        }
2865        // Self-heal a layer saved before the z-index fix landed (see
2866        // iframe-script/utils.ts's ensureNavScrollBackground for the
2867        // matching editor-side comment).
2868        if (blurLayer.style.zIndex !== '-1') blurLayer.style.zIndex = '-1';
2869        blurLayer.style.setProperty('backdrop-filter', 'blur(' + blur + ')', 'important');
2870        blurLayer.style.setProperty('-webkit-backdrop-filter', 'blur(' + blur + ')', 'important');
2871      }
2872      if (alwaysTransparent) {
2873        nav.style.setProperty('background-color', 'transparent', 'important');
2874        return;
2875      }
2876      var color = nav.getAttribute('data-persist-nav-scroll-bg');
2877      if (!color || nav.__hpNavScrollWired) return;
2878      nav.__hpNavScrollWired = true;
2879      function sync() {
2880        nav.style.setProperty('background-color', window.scrollY > 24 ? color : 'transparent', 'important');
2881      }
2882      window.addEventListener('scroll', sync, { passive: true });
2883      sync();
2884    })(navs[i]);
2885  }
2886}
2887
2888function boot() {
2889  wireSubmenus();
2890  wireMobileMenu();
2891  wireScrollBackground();
2892}
2893
2894if (document.readyState === "loading") {
2895  document.addEventListener("DOMContentLoaded", boot);
2896} else {
2897  boot();
2898}
2899
2900})();
2901</script>
2901<script data-htmlpub-dtr>
2902(function(){
2903if(false)return;
2904var params=new URLSearchParams(location.search);
2905if(!params.toString())return;
2906function run(){
2907var MAX=500;
2908var replacements=[];
2909function strip(s){return s.replace(/<[^>]*>/g,"");}
2910function clean(s){try{s=decodeURIComponent(s);}catch(e){}s=strip(s);if(s.length>MAX)s=s.slice(0,MAX);return s;}
2911function replace(el,val,param){
2912if(!el||!val)return;
2913var orig=el.textContent||"";
2914var cleaned=clean(val);
2915if(cleaned===orig)return;
2916el.setAttribute("data-dtr-original",orig);
2917el.textContent=cleaned;
2918replacements.push({param:param,target:el.tagName.toLowerCase()+(el.id?"#"+el.id:""),original:orig,replacement:cleaned});
2919}
2920// 1. Explicit data-dtr attributes
2921var dtrMap={headline:"utm_headline",subheadline:"utm_subheadline",cta:"utm_cta"};
2922var explicitFound={};
2923for(var key in dtrMap){
2924var pv=params.get(dtrMap[key]);
2925if(pv){
2926var els=document.querySelectorAll('[data-dtr="'+key+'"]');
2927if(els.length>0){
2928explicitFound[key]=true;
2929for(var i=0;i<els.length;i++)replace(els[i],pv,dtrMap[key]);
2930}
2931}
2932}
2933// 2. Custom data-dtr-param attributes
2934var customEls=document.querySelectorAll("[data-dtr-param]");
2935for(var j=0;j<customEls.length;j++){
2936var pn=customEls[j].getAttribute("data-dtr-param");
2937if(pn){var cv=params.get(pn);if(cv)replace(customEls[j],cv,pn);}
2938}
2939// 3. Auto-detect fallback for standard params (only if no explicit match)
2940if(!explicitFound.headline){
2941var hv=params.get("utm_headline");
2942if(hv){var h1=document.querySelector("h1");if(h1)replace(h1,hv,"utm_headline");}
2943}
2944if(!explicitFound.subheadline){
2945var sv=params.get("utm_subheadline");
2946if(sv){var sub=document.querySelector("h2")||document.querySelector(".subtitle")||document.querySelector(".subheadline");if(sub)replace(sub,sv,"utm_subheadline");}
2947}
2948if(!explicitFound.cta){
2949var cv2=params.get("utm_cta");
2950if(cv2){var btn=document.querySelector('a.cta,button.cta,a[class*="cta"],button[class*="cta"]')||document.querySelector("a.btn,button.btn,a.button,button[type=submit]")||document.querySelector('a[href]:not([href^="#"]):not([href^="mailto"]):not([href^="tel"])>span,a[href]:not([href^="#"]):not([href^="mailto"]):not([href^="tel"])')||document.querySelector("button");if(btn)replace(btn,cv2,"utm_cta");}
2951}
2952// 4. Generic dtr_* prefix params
2953params.forEach(function(val,key){
2954if(key.indexOf("dtr_")===0){
2955var slug=key.slice(4);
2956var targets=document.querySelectorAll('[data-dtr-param="'+key+'"]');
2957if(targets.length>0){for(var k=0;k<targets.length;k++)replace(targets[k],val,key);}
2958}
2959});
2960// Fire analytics event
2961if(replacements.length===0)return;
2962var PID="rm90klif4ys3o04jw9zgjfpw";
2963var API="https://htmlpub.com"+"/api/analytics/collect";
2964function vid(){try{return localStorage.getItem("_hp_vid")||""}catch(e){return""}}
2965function sid(){try{return sessionStorage.getItem("_hp_sid")||""}catch(e){return""}}
2966var ev={pageId:PID,eventType:"dtr_applied",visitorId:vid(),sessionId:sid(),payload:{replacements:replacements},timestamp:Date.now()};
2967var body=JSON.stringify({events:[ev]});
2968try{if(navigator.sendBeacon){navigator.sendBeacon(API,new Blob([body],{type:"text/plain"}));}
2969else{fetch(API,{method:"POST",headers:{"Content-Type":"application/json"},body:body,keepalive:true}).catch(function(){});}}catch(e){}
2970}
2971if(document.readyState==="loading"){document.addEventListener("DOMContentLoaded",run);}else{run();}
2972})();
2973</script>
2973<script>(function(){function c(){var b=a.contentDocument||(a.contentWindow&&a.contentWindow.document);if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'a45d5c691d8d65f6',t:'MTc5MTIxMzEwOA=='};var a=document.createElement('script');a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script>
2973</body></html>

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.