PageSourceSearch

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

html 4responsible.ai collected 2026-10-05 15:11:48 UTC 177,432 bytes, 3,002 lines download raw bytes

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