PageSourceSearch

https://kmu-bibliothek.com/nur-5-euro

html kmu-bibliothek.com collected 2026-10-05 23:03:49 UTC 182,427 bytes, 2,983 lines download raw bytes

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&amp;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&amp;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&#58;//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&#58;//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&#58;//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 &amp; 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&#58;//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.