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