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&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&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 & 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><article></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 > Protect > Preserve > 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.