PageSourceSearch

https://mindrebel.eu/datenschutz

html mindrebel.eu collected 2026-10-07 10:18:37 UTC 217,402 bytes, 2,985 lines download raw bytes

1<!DOCTYPE html><html lang="de" style=""><head><meta name="description" content="Datenschutzerklärung von Mindrebel – Informationen zur Verarbeitung personenbezogener Daten, Ihre Rechte und die eingesetzten Dienste gemäß DSGVO.">
2<meta property="og:title" content="Datenschutz – Mindrebel">
3<meta property="og:description" content="Datenschutzerklärung von Mindrebel – Informationen zur Verarbeitung personenbezogener Daten, Ihre Rechte und die eingesetzten Dienste gemäß DSGVO.">
4<meta property="og:image" content="https://htmlpub.com/api/og/screenshot?id=v8s3dfzxmk03xqctda3738k8&amp;v=1789651194965">
5<meta property="og:image:width" content="1200">
6<meta property="og:image:height" content="630">
7<meta property="og:url" content="https://mindrebel.eu/datenschutz">
8<meta name="twitter:url" content="https://mindrebel.eu/datenschutz">
9<meta property="og:type" content="website">
10<meta property="og:site_name" content="mindrebel.eu">
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="Datenschutz – Mindrebel">
15<meta name="twitter:description" content="Datenschutzerklärung von Mindrebel – Informationen zur Verarbeitung personenbezogener Daten, Ihre Rechte und die eingesetzten Dienste gemäß DSGVO.">
16<meta name="twitter:image" content="https://htmlpub.com/api/og/screenshot?id=v8s3dfzxmk03xqctda3738k8&amp;v=1789651194965">
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://mindrebel.eu/datenschutz">
21<title>Datenschutz – Mindrebel</title>
22<meta charset="UTF-8">
23<meta name="viewport" content="width=device-width, initial-scale=1.0">
24
25
26<style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:hidden;max-width:100%}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;background:#0d0d0d;color:#e8e2d6;line-height:1.8}a{text-decoration:none;color:inherit}nav{padding:22px 40px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #888888;position:sticky;top:0;background:rgba(13,13,13,0.95);backdrop-filter:blur(12px);z-index:100}.back{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:#ED187E;transition:opacity .2s}.back:hover{opacity:.7}.content{max-width:760px;margin:0 auto;padding:80px 24px 100px}.eyebrow{font-size:11px;letter-spacing:3px;text-transform:uppercase;color:#ED187E;margin-bottom:16px}h1{font-family:Georgia,serif;font-size:clamp(2rem,4vw,3rem);color:#f5f0e8;font-weight:700;letter-spacing:-.5px;margin:0 0 40px;line-height:1.15}h2{font-family:Georgia,serif;font-size:1.3rem;color:#f5f0e8;font-weight:700;margin:40px 0 12px}p{color:#aaaaaa;font-size:15px;margin:0 0 12px;overflow-wrap:break-word;word-break:break-word}.content div,.content li,.hint{overflow-wrap:break-word;word-break:break-word}.content div{min-width:0;max-width:100%}h1,h2{overflow-wrap:break-word}.hint{color:#666666;font-size:13px;font-style:italic;margin-top:36px;padding-top:20px;border-top:1px solid rgba(255,255,255,0.08)}footer{padding:32px 40px;border-top:1px solid #888888;display:flex;justify-content:space-between;align-items:center;font-size:12px;color:#888888;flex-wrap:wrap;gap:12px}footer a{color:#aaaaaa;transition:color .2s}footer a:hover{color:#ED187E}@media (max-width:900px){nav{padding:18px 24px;flex-direction:column;align-items:flex-start;gap:10px}.back{white-space:nowrap}.content{padding:60px 24px 80px}footer{padding:24px;flex-direction:column;text-align:center}}</style>
27<style id="htmlpub-breakpoint-visibility">@media (max-width: 767px) { .hp-hide-mobile { display: none !important; } }
28@media (min-width: 768px) and (max-width: 1199px) { .hp-hide-tablet { display: none !important; } }
29@media (min-width: 1200px) { .hp-hide-desktop { display: none !important; } }</style><style id="nova-responsive-styles">@media (max-width: 767px){
30[data-nova-id="n-gt59lvu"]{font-size:29px!important}
31}
32</style>
32<script id="Cookiebot" src="https://consent.cookiebot.com/uc.js" data-cbid="638e2267-350e-47e1-9c00-9210069d2c94" data-blockingmode="auto" type="text/javascript"></script>
32
33</head>
34<body style="" class=""><a href="https://mindrebel.eu/cdn-cgi/content?id=4cz.3AchtMlMb3KAGdlkgj_LuI52KVhxmRInQW0wRtQ-1791368311.248302-1.2.1.1-Fz.sLZl8IptihdxLEUi860ifRngM4ViVeRL4vNS25k7sNzLMFHf7lnGkvIqgcU_N" aria-hidden="true" rel="nofollow noopener" style="display: none !important; visibility: hidden !important"></a>
35<nav>
36  <a href="/CLfUUZCIIn" style="display: flex; align-items: center;"><img src="/api/library/ts1n1equqyxv4hw5vbfa84g6?w=1200&fit=scale-down&format=auto&quality=82" alt="Mindrebel Logo" style="height: 36px; width: auto; display: block;" fetchpriority="high"></a>
37  <a href="/CLfUUZCIIn" class="back">← Zurück zur Startseite</a>
38</nav>
39<div class="content" style="">
40  <div class="eyebrow">Rechtliches</div>
41  <h1 style="" data-nova-id="n-gt59lvu">Datenschutzerklärung</h1>
42  <h2 style="font-size: 30px;">1. Datenschutz auf einen Blick</h2><h2>Allgemein</h2>
43  <p style="">Die folgenden Hinweise geben einen einfachen Überblick darüber, was mit Ihren personenbezogenen Daten passiert, wenn Sie diese Website besuchen. Personenbezogene Daten sind alle Daten, mit denen Sie persönlich identifiziert werden können. Ausführliche Informationen zum Thema Datenschutz entnehmen Sie unserer unter diesem Text aufgeführten Datenschutzerklärung.<br></p>
44  <h2 style="font-size: 25px;">Datenerfassung auf dieser Website</h2><h2>Wer ist verantwortlich für die Datenerfassung auf dieser Website?</h2>
45  <p>Die Datenverarbeitung auf dieser Website erfolgt durch den Websitebetreiber. Dessen Kontaktdaten können Sie dem Abschnitt „Hinweis zur Verantwortlichen Stelle“ in dieser Datenschutzerklärung entnehmen.<br></p>
46  <h2>Wie erfassen wir Daten?</h2>
47  <p>Ihre Daten werden zum einen dadurch erhoben, dass Sie uns diese mitteilen. Hierbei kann es sich z. B. um Daten handeln, die Sie in ein Kontaktformular eingeben.<br>Andere Daten werden automatisch oder nach Ihrer Einwilligung beim Besuch der Website durch unsere IT-Systeme erfasst. Das sind vor allem technische Daten (z. B. Internetbrowser, Betriebssystem oder Uhrzeit des Seitenaufrufs). Die Erfassung dieser Daten erfolgt automatisch, sobald Sie diese Website betreten.<br></p><p></p>
48  <h2>Wofür nutzen wir Daten?</h2><p>Ein Teil der Daten wird erhoben, um eine fehlerfreie Bereitstellung der Website zu gewährleisten. Andere Daten können zur Analyse Ihres Nutzerverhaltens verwendet werden.<br></p><h2>Welche Rechte haben Sie bezüglich Ihrer Daten?</h2>
49  
50  <p>Sie haben jederzeit das Recht, unentgeltlich Auskunft über Herkunft, Empfänger und Zweck Ihrer gespeicherten personenbezogenen Daten zu erhalten. Sie haben außerdem ein Recht, die Berichtigung oder Löschung dieser Daten zu verlangen. Wenn Sie eine Einwilligung zur Datenverarbeitung erteilt haben, können Sie diese Einwilligung jederzeit für die Zukunft widerrufen. Außerdem haben Sie das Recht, unter bestimmten Umständen die Einschränkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen. Des Weiteren steht Ihnen ein Beschwerderecht bei der zuständigen Aufsichtsbehörde zu.<br>Hierzu sowie zu weiteren Fragen zum Thema Datenschutz können Sie sich jederzeit an uns wenden.<br></p><h2>Analyse-Tools und Tools von Drittanbietern</h2>
51  <p>Beim Besuch dieser Website kann Ihr Surf-Verhalten statistisch ausgewertet werden. Das geschieht vor allem mit sogenannten Analyseprogrammen.<br>Detaillierte Informationen zu diesen Analyseprogrammen finden Sie in der folgenden Datenschutzerklärung.<br></p>
52  <h2 style="font-size: 30px;">2. Hosting</h2>
53  <p>Wir hosten die Inhalte unserer Website bei folgendem Anbieter:</p>
54  <h2>Ionos</h2><p>Anbieter ist die Raidboxes GmbH, Hafenstraße 32<br>48153 Münster (nachfolgend Raidboxes).</p><div><br>Details entnehmen Sie der Datenschutzerklärung von Raidboxes: https://raidboxes.io/legal/privacy/<br>Die Verwendung von Raidboxes erfolgt auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO. Wir haben ein berechtigtes Interesse an einer möglichst zuverlässigen Darstellung unserer Website. Sofern eine entsprechende Einwilligung abgefragt wurde, erfolgt die Verarbeitung ausschließlich auf Grundlage von Art. 6 Abs. 1 lit. a DSGVO und § 25 Abs. 1 TTDSG, soweit die Einwilligung die Speicherung von Cookies oder den Zugriff auf Informationen im Endgerät des Nutzers (z. B. Device-Fingerprinting) im Sinne des TTDSG umfasst. Die Einwilligung ist jederzeit widerrufbar.<br><br><b>Auftragsverarbeitung</b><br>Wir haben einen Vertrag über Auftragsverarbeitung (AVV) zur Nutzung des oben genannten Dienstes geschlossen. Hierbei handelt es sich um einen datenschutzrechtlich vorgeschriebenen Vertrag, der gewährleistet, dass dieser die personenbezogenen Daten unserer Websitebesucher nur nac
54h unseren Weisungen und unter Einhaltung der DSGVO verarbeitet.<br></div><p></p><h2 style="font-size: 30px;">3. Allgemeine Hinweise und Pflicht­informationen</h2><h2 style="font-size: 21px;">Datenschutz</h2><p>Die Betreiber dieser Seiten nehmen den Schutz Ihrer persönlichen Daten sehr ernst. Wir behandeln Ihre personenbezogenen Daten vertraulich und entsprechend den gesetzlichen Datenschutzvorschriften sowie dieser Datenschutzerklärung.<br>Wenn Sie diese Website benutzen, werden verschiedene personenbezogene Daten erhoben. Personenbezogene Daten sind Daten, mit denen Sie persönlich identifiziert werden können. Die vorliegende Datenschutzerklärung erläutert, welche Daten wir erheben und wofür wir sie nutzen. Sie erläutert auch, wie und zu welchem Zweck das geschieht.<br>Wir weisen darauf hin, dass die Datenübertragung im Internet (z. B. bei der Kommunikation per E-Mail) Sicherheitslücken aufweisen kann. Ein lückenloser Schutz der Daten vor dem Zugriff durch Dritte ist nicht möglich.<br></p>
55<h2 style="font-size: 21px;">Hinweis zur verantwortlichen Stelle</h2><p>Die verantwortliche Stelle für die Datenverarbeitung auf dieser Website ist:<br>Mindrebel KG<br>Bahnhofstraße 23<br>5201 Seekirchen am Wallersee<br>E-Mail: support (at) mindrebel.eu<br>Verantwortliche Stelle ist die natürliche oder juristische Person, die allein oder gemeinsam mit anderen über die Zwecke und Mittel der Verarbeitung von personenbezogenen Daten (z. B. Namen, E-Mail-Adressen o. Ä.) entscheidet.<br></p><h2 style="font-size: 21px;">Datenschutz</h2><p>Soweit innerhalb dieser Datenschutzerklärung keine speziellere Speicherdauer genannt wurde, verbleiben Ihre personenbezogenen Daten bei uns, bis der Zweck für die Datenverarbeitung entfällt. Wenn Sie ein berechtigtes Löschersuchen geltend machen oder eine Einwilligung zur Datenverarbeitung widerrufen, werden Ihre Daten gelöscht, sofern wir keine anderen rechtlich zulässigen Gründe für die Speicherung Ihrer personenbezogenen Daten haben (z. B. steuer- oder handelsrechtliche Aufbewahrungsfristen); im letztgenannten Fall erfolgt die Löschung nach Fortfall dieser Gründe.<br></p><h2 style="font-size: 21px;">Allgemeine Hinweise zu den Rechtsgrundlagen der Datenverarbeitung auf dieser Website</h2><p>Sofern Sie in die Datenverarbeitung eingewilligt haben, verarbeiten wir Ihre personenbezogenen Daten auf Grundlage von Art. 6 Abs. 1 lit. a DSGVO bzw. Art. 9 Abs. 2 lit. a DSGVO, sofern besondere Datenkategorien nach Art. 9 Abs. 1 DSGVO verarbeitet werden. Im Falle einer ausdrücklichen Einwilligung in die Übertragung personenbezogener Daten in Drittstaaten erfolgt die Datenverarbeitung außerdem auf Grundlage von Art. 49 Abs. 1 lit. a DSGVO. Sofern Sie in die Speicherung von Cookies oder in den Zugriff auf Informationen in Ihr Endgerät (z. B. via Device-Fingerprinting) eingewilligt haben, erfolgt die Datenverarbeitung zusätzlich auf Grundlage von § 25 Abs. 1 TTDSG. Die Einwilligung ist jederzeit widerrufbar. Sind Ihre Daten zur Vertragserfüllung oder zur Durchführung vorvertraglicher Maßnahmen erforderlich, verarbeiten wir Ihre Daten auf Grundlage des Art. 6 Abs. 1 lit. b DSGVO. Des Weiteren verarbeiten wir Ihre Daten, sofern diese zur Erfüllung einer rechtlichen Verpflichtung erforderlich sind auf Grundlage von Art. 6 Abs. 1 lit. c DSGVO. Die Datenverarbeitung kann ferner auf Grundlage unseres berechtigten Interesses nach Art. 6 Abs. 1 lit. f DSGVO erfolgen. Über die jeweils im Einzelfall einschlägigen Rechtsgrundlagen wird in den folgenden Absätzen dieser Datenschutzerklärung informiert.<br></p><h2 style="font-size: 21px;">Hinweis zur Datenweitergabe in die USA und sonstige Drittstaaten</h2><p>Wir verwenden unter anderem Tools von Unternehmen mit Sitz in den USA oder sonstigen datenschutzrechtlich nicht sicheren Drittstaaten. Wenn diese Tools aktiv sind, können Ihre personenbezogene Daten in diese Drittstaaten übertragen und dort verarbeitet werden. Wir weisen darauf hin, dass in diesen Ländern kein mit der EU vergleichbares Datenschutzniveau garantiert werden kann. Beispielsweise sind US-Unternehmen dazu verpflichtet, personenbezogene Daten an Sicherheitsbehörden herauszugeben, ohne dass Sie als Betroffener hiergegen gerichtlich vorgehen könnten. Es kann daher nicht ausgeschlossen werden, dass US-Behörden (z. B. Geheimdienste) Ihre auf US-Servern befindlichen Daten zu Überwachungszwecken verarbeiten, auswerten und dauerhaft speichern. Wir haben auf diese Verarbeitungstätigkeiten keinen Einfluss.<br></p><h2 style="font-size: 21px;">Widerruf Ihrer Einwilligung zur Datenverarbeitung</h2><p>Viele Datenverarbeitungsvorgänge sind nur mit Ihrer ausdrücklichen Einwilligung möglich. Sie können eine bereits erteilte Einwilligung jederzeit widerrufen. Die Rechtmäßigkeit der bis zum Widerruf erfolgten Datenverarbeitung bleibt vom Widerruf unberührt.<br></p><h2 style="font-size: 21px;">Widerspruchsrecht gegen die Datenerhebung in besonderen Fällen sowie gegen Direktwerbung (Art. 21 DSGVO)</h2><p>WENN DIE DATENVERARBEITUNG AUF GRUNDLAGE VON ART. 6 ABS. 1 LIT. E ODER F DSGVO ERFOLGT, HABEN SIE JEDERZEIT DAS RECHT, AUS GRÜNDEN, DIE SICH AUS IHRER BESONDEREN SITUATION ERGEBEN, GEGEN DIE VERARBEITUNG IHRER PERSONENBEZOGENEN DATEN WIDERSPRUCH EINZULEGEN; DIES GILT AUCH FÜR EIN AUF DIESE BESTIMMUNGEN GESTÜTZTES PROFILING. DIE JEWEILIGE RECHTSGRUNDLAG
55E, AUF DENEN EINE VERARBEITUNG BERUHT, ENTNEHMEN SIE DIESER DATENSCHUTZERKLÄRUNG. WENN SIE WIDERSPRUCH EINLEGEN, WERDEN WIR IHRE BETROFFENEN PERSONENBEZOGENEN DATEN NICHT MEHR VERARBEITEN, ES SEI DENN, WIR KÖNNEN ZWINGENDE SCHUTZWÜRDIGE GRÜNDE FÜR DIE VERARBEITUNG NACHWEISEN, DIE IHRE INTERESSEN, RECHTE UND FREIHEITEN ÜBERWIEGEN ODER DIE VERARBEITUNG DIENT DER GELTENDMACHUNG, AUSÜBUNG ODER VERTEIDIGUNG VON RECHTSANSPRÜCHEN (WIDERSPRUCH NACH ART. 21 ABS. 1 DSGVO).<br>WERDEN IHRE PERSONENBEZOGENEN DATEN VERARBEITET, UM DIREKTWERBUNG ZU BETREIBEN, SO HABEN SIE DAS RECHT, JEDERZEIT WIDERSPRUCH GEGEN DIE VERARBEITUNG SIE BETREFFENDER PERSONENBEZOGENER DATEN ZUM ZWECKE DERARTIGER WERBUNG EINZULEGEN; DIES GILT AUCH FÜR DAS PROFILING, SOWEIT ES MIT SOLCHER DIREKTWERBUNG IN VERBINDUNG STEHT. WENN SIE WIDERSPRECHEN, WERDEN IHRE PERSONENBEZOGENEN DATEN ANSCHLIESSEND NICHT MEHR ZUM ZWECKE DER DIREKTWERBUNG VERWENDET (WIDERSPRUCH NACH ART. 21 ABS. 2 DSGVO).<br></p><h2 style="font-size: 21px;">Beschwerde­recht bei der zuständigen Aufsichts­behörde</h2><p>Im Falle von Verstößen gegen die DSGVO steht den Betroffenen ein Beschwerderecht bei einer Aufsichtsbehörde, insbesondere in dem Mitgliedstaat ihres gewöhnlichen Aufenthalts, ihres Arbeitsplatzes oder des Orts des mutmaßlichen Verstoßes zu. Das Beschwerderecht besteht unbeschadet anderweitiger verwaltungsrechtlicher oder gerichtlicher Rechtsbehelfe.WERDEN IHRE PERSONENBEZOGENEN DATEN VERARBEITET, UM DIREKTWERBUNG ZU BETREIBEN, SO HABEN SIE DAS RECHT, JEDERZEIT WIDERSPRUCH GEGEN DIE VERARBEITUNG SIE BETREFFENDER PERSONENBEZOGENER DATEN ZUM ZWECKE DERARTIGER WERBUNG EINZULEGEN; DIES GILT AUCH FÜR DAS PROFILING, SOWEIT ES MIT SOLCHER DIREKTWERBUNG IN VERBINDUNG STEHT. WENN SIE WIDERSPRECHEN, WERDEN IHRE PERSONENBEZOGENEN DATEN ANSCHLIESSEND NICHT MEHR ZUM ZWECKE DER DIREKTWERBUNG VERWENDET (WIDERSPRUCH NACH ART. 21 ABS. 2 DSGVO).<br></p><h2 style="font-size: 21px;">Recht auf Daten­übertrag­barkeit</h2><p>Sie haben das Recht, Daten, die wir auf Grundlage Ihrer Einwilligung oder in Erfüllung eines Vertrags automatisiert verarbeiten, an sich oder an einen Dritten in einem gängigen, maschinenlesbaren Format aushändigen zu lassen. Sofern Sie die direkte Übertragung der Daten an einen anderen Verantwortlichen verlangen, erfolgt dies nur, soweit es technisch machbar ist.<br></p><h2 style="font-size: 21px;">Auskunft, Berichtigung und Löschung</h2><p>Sie haben im Rahmen der geltenden gesetzlichen Bestimmungen jederzeit das Recht auf unentgeltliche Auskunft über Ihre gespeicherten personenbezogenen Daten, deren Herkunft und Empfänger und den Zweck der Datenverarbeitung und ggf. ein Recht auf Berichtigung oder Löschung dieser Daten. Hierzu sowie zu weiteren Fragen zum Thema personenbezogene Daten können Sie sich jederzeit an uns wenden.<br></p><h2 style="font-size: 21px;">Recht auf Einschränkung der Verarbeitung</h2><p>Sie haben das Recht, die Einschränkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen. Hierzu können Sie sich jederzeit an uns wenden. Das Recht auf Einschränkung der Verarbeitung besteht in folgenden Fällen:<br></p><ul><li style="color: rgb(170, 170, 170);">Wenn Sie die Richtigkeit Ihrer bei uns gespeicherten personenbezogenen Daten bestreiten, benötigen wir in der Regel Zeit, um dies zu überprüfen. Für die Dauer der Prüfung haben Sie das Recht, die Einschränkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen.</li><li style="color: rgb(170, 170, 170);">Wenn die Verarbeitung Ihrer personenbezogenen Daten unrechtmäßig geschah/geschieht, können Sie statt der Löschung die Einschränkung der Datenverarbeitung verlangen.</li><li style="color: rgb(170, 170, 170);">Wenn wir Ihre personenbezogenen Daten nicht mehr benötigen, Sie sie jedoch zur Ausübung, Verteidigung oder Geltendmachung von Rechtsansprüchen benötigen, haben Sie das Recht, statt der Löschung die Einschränkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen.</li><li style="color: rgb(170, 170, 170);">Wenn Sie einen Widerspruch nach Art. 21 Abs. 1 DSGVO eingelegt haben, muss eine Abwägung zwischen Ihren und unseren Interessen vorgenommen werden. Solange noch nicht feststeht, wessen Interessen überwiegen, haben Sie das Recht, die Einschränkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen.</li></ul><p>Wenn Sie die Verarbeitung Ihrer personenbezogenen Daten eingeschränkt haben, dürfen die
55se Daten – von ihrer Speicherung abgesehen – nur mit Ihrer Einwilligung oder zur Geltendmachung, Ausübung oder Verteidigung von Rechtsansprüchen oder zum Schutz der Rechte einer anderen natürlichen oder juristischen Person oder aus Gründen eines wichtigen öffentlichen Interesses der Europäischen Union oder eines Mitgliedstaats verarbeitet werden.</p><h2 style="font-size: 21px;">SSL- bzw. TLS-Verschlüsselung</h2><p>Diese Seite nutzt aus Sicherheitsgründen und zum Schutz der Übertragung vertraulicher Inhalte, wie zum Beispiel Bestellungen oder Anfragen, die Sie an uns als Seitenbetreiber senden, eine SSL- bzw. TLS-Verschlüsselung. Eine verschlüsselte Verbindung erkennen Sie daran, dass die Adresszeile des Browsers von „http://“ auf „https://“ wechselt und an dem Schloss-Symbol in Ihrer Browserzeile.<br>Wenn die SSL- bzw. TLS-Verschlüsselung aktiviert ist, können die Daten, die Sie an uns übermitteln, nicht von Dritten mitgelesen werden.<br></p><h2 style="font-size: 30px;">4. Datenerfassung auf dieser Website</h2><h2 style="font-size: 21px;">Cookies</h2><p>Unsere Internetseiten verwenden so genannte „Cookies“. Cookies sind kleine Datenpakete und richten auf Ihrem Endgerät keinen Schaden an. Sie werden entweder vorübergehend für die Dauer einer Sitzung (Session-Cookies) oder dauerhaft (permanente Cookies) auf Ihrem Endgerät gespeichert. Session-Cookies werden nach Ende Ihres Besuchs automatisch gelöscht. Permanente Cookies bleiben auf Ihrem Endgerät gespeichert, bis Sie diese selbst löschen oder eine automatische Löschung durch Ihren Webbrowser erfolgt.<br>Cookies können von uns (First-Party-Cookies) oder von Drittunternehmen stammen (sog. Third-Party-Cookies). Third-Party-Cookies ermöglichen die Einbindung bestimmter Dienstleistungen von Drittunternehmen innerhalb von Webseiten (z. B. Cookies zur Abwicklung von Zahlungsdienstleistungen).<br>Cookies haben verschiedene Funktionen. Zahlreiche Cookies sind technisch notwendig, da bestimmte Webseitenfunktionen ohne diese nicht funktionieren würden (z. B. die Warenkorbfunktion oder die Anzeige von Videos). Andere Cookies können zur Auswertung des Nutzerverhaltens oder zu Werbezwecken verwendet werden.<br>Cookies, die zur Durchführung des elektronischen Kommunikationsvorgangs, zur Bereitstellung bestimmter, von Ihnen erwünschter Funktionen (z. B. für die Warenkorbfunktion) oder zur Optimierung der Website (z. B. Cookies zur Messung des Webpublikums) erforderlich sind (notwendige Cookies), werden auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO gespeichert, sofern keine andere Rechtsgrundlage angegeben wird. Der Websitebetreiber hat ein berechtigtes Interesse an der Speicherung von notwendigen Cookies zur technisch fehlerfreien und optimierten Bereitstellung seiner Dienste. Sofern eine Einwilligung zur Speicherung von Cookies und vergleichbaren Wiedererkennungstechnologien abgefragt wurde, erfolgt die Verarbeitung ausschließlich auf Grundlage dieser Einwilligung (Art. 6 Abs. 1 lit. a DSGVO und § 25 Abs. 1 TTDSG); die Einwilligung ist jederzeit widerrufbar.<br>Sie können Ihren Browser so einstellen, dass Sie über das Setzen von Cookies informiert werden und Cookies nur im Einzelfall erlauben, die Annahme von Cookies für bestimmte Fälle oder generell ausschließen sowie das automatische Löschen der Cookies beim Schließen des Browsers aktivieren. Bei der Deaktivierung von Cookies kann die Funktionalität dieser Website eingeschränkt sein.<br>Welche Cookies und Dienste auf dieser Website eingesetzt werden, können Sie dieser Datenschutzerklärung entnehmen.<br></p><h2 style="font-size: 21px;">Einwilligung mit Cookiebot Cookie</h2><p>Unsere Website nutzt die Consent-Technologie von Cookiebot, um Ihre Einwilligung zur Speicherung bestimmter Cookies in Ihrem Browser oder zum Einsatz bestimmter Technologien einzuholen und diese datenschutzkonform zu dokumentieren. Anbieter dieser Technologie ist die Usercentrics A/S, Uavnegade 39, 1058 Kopenhagen<br>Dänemark (im Folgenden Cookiebot).<br>Wenn Sie unsere Website betreten, wird ein Cookiebot Cookie in Ihrem Browser gespeichert, in dem die von Ihnen erteilten Einwilligungen oder der Widerruf dieser Einwilligungen gespeichert werden. Diese Daten werden nicht an den Anbieter von Cookiebot weitergegeben.<br>Die erfassten Daten werden gespeichert, bis Sie uns zur Löschung auffordern bzw. das Cookiebot-Cookie selbst löschen oder der Zweck für die Datenspeicherung entfällt. Zwingende gesetzliche Aufbewahrungsfristen bleiben unberührt. Details zur Datenverarbeitung von Cookiebot Cookie finden Sie unter https://www.cookiebot.com/de/dsgvo-cookies/<br>Der Einsatz der Cookiebot-Cookie-Consent-Technologie erfolgt, um die gesetzlich vorgeschriebenen Einwilligungen für den Einsatz von Cookies einzuholen. Rechtsgrundlage hierfür ist Art. 6 Abs. 1 lit. c DSGVO.<br></p><h2 style="font-size: 21px;">Kommunikation via WhatsApp</h2><p>Für die Kommunikation mit unseren Kunden und sonstigen Dritten nutzen wir unter anderem den Instant-Messaging-Dienst WhatsApp. Anbieter ist die WhatsApp Ireland Limited, 4 Grand Canal Square, Grand Canal Harbour, Dublin 2, Irland.<br>Die Kommunikation erfolgt über eine Ende-zu-Ende-Verschlüsselung (Peer-to-Peer), die verhindert, dass WhatsApp oder sonstige Dritte Zugriff auf die Kommunikationsinhalte erlangen können. WhatsApp erhält jedoch Zugriff auf Metadaten, die im Zuge des Kommunikationsvorgangs entstehen (z. B. Absender, Empfänger und Zeitpunkt). Wir weisen ferner darauf hin, dass WhatsApp nach eigener Aussage, personenbezogene Daten seiner Nutzer mit seiner in den USA ansässigen Konzernmutter Meta teilt. Weitere Details zur Datenverarbeitung finden Sie in der Datenschutzrichtlinie von WhatsApp unter: https://www.whatsapp.com/legal/#privacy-policy.<br>Der Einsatz von WhatsApp erfolgt auf Grundlage unseres berechtigten Interesses an einer möglichst schnellen und effektiven Kommunikation mit Kunden, Interessenten und sonstigen Geschäfts- und Vertragspartnern (Art. 6 Abs. 1 lit. f DSGVO). Sofern eine entsprechende Einwilligung abgefragt wurde, erfolgt die Datenverarbeitung ausschließlich auf Grundlage der Einwilligung; diese ist jederzeit mit Wirkung für die Zukunft widerrufbar.<br>Die zwischen und auf WhatsApp ausgetauschten Kommunikationsinhalte verbleiben bei uns, bis Sie uns zur Löschung auffordern, Ihre Einwilligung zur Speicherung widerrufen oder der Zweck für die Datenspeicherung entfällt (z. B. nach abgeschlossener Bearbeitung Ihrer Anfrage). Zwingende gesetzliche Bestimmungen – insbesondere Aufbewahrungsfristen – bleiben unberührt.<br>Wir nutzen WhatsApp in der Variante „WhatsApp“.<br>Die Datenübertragung in die USA wird auf die Standardvertragsklauseln der EU-Kommission gestützt. Details finden Sie hier: https://www.whatsapp.com/legal/business-data-transfer-addendum.<br></p><h2 style="font-size: 21px;">Typeform</h2><p>Wir haben Typeform auf dieser Website eingebunden. Anbieter ist die TYPEFORM S.L., Carrer Bac de Roda, 163, 08018 Barcelona, Spanien (nachfolgend Typeform).<br>Typeform ermöglicht es uns, Online-Formulare zu erstellen und sie auf unserer Website einzubinden. Die von Ihnen in unsere Typeform-Formulare eingegebenen Daten werden auf den Servern von Typeform gespeichert, bis Sie uns zur Löschung auffordern, eine abgegebene Einwilligung zur Speicherung widerrufen oder der Zweck für die Datenspeicherung entfällt (z. B. nach abgeschlossener Bearbeitung Ihrer Anfrage). Zwingende gesetzliche Bestimmungen – insbesondere Aufbewahrungsfristen – bleiben unberührt.<br>Die Verwendung von Typeform erfolgt auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO. Der Websitebetreiber hat ein berechtigtes Interesse an funktionierenden Online-Formularen. Sofern eine entsprechende Einwilligung abgefragt wurde, erfolgt die Verarbeitung ausschließlich auf Grundlage von Art. 6 Abs. 1 lit. a DSGVO und § 25 Abs. 1 TTDSG, soweit die Einwilligung die Speicherung von Cookies oder den Zugriff auf Informationen im Endgerät des Nutzers (z. B. Device-Fingerprinting) im Sinne des TTDSG umfasst. Die Einwilligung ist jederzeit widerrufbar.<br><br><b>Auftragsverarbeitung</b><br>Wir haben einen Vertrag über Auftragsverarbeitung (AVV) zur Nutzung des oben genannten Dienstes geschlossen. Hierbei handelt es sich um einen datenschutzrechtlich vorgeschriebenen Vertrag, der gewährleistet, dass dieser die personenbezogenen Daten unserer Websitebesucher nur nac
55h unseren Weisungen und unter Einhaltung der DSGVO verarbeitet.<br></p><h2 style="font-size: 21px;">Google Forms</h2><p>Wir haben Google Forms auf dieser Website eingebunden. Anbieter ist die die Google Ireland Limited, Gordon House, Barrow Street, Dublin 4, Irland (nachfolgend Google).<br>Google Forms ermöglicht es uns, Online-Formulare zu erstellen um Nachrichten, Anfragen und sonstige Eingaben unserer Websitebesucher strukturiert zu erfassen. Alle von Ihnen getätigten Eingaben werden auf den Servern von Google verarbeitet. Google Forms speichert ein Cookie in Ihrem Browser, das eine eindeutige ID enthält (NID-Cookie). Dieses Cookie speichert verschiedene Informationen wie beispielsweise Ihre Spracheinstellungen.<br>Der Einsatz von Google Forms erfolgt auf Grundlage unseres berechtigten Interesses an einer möglichst nutzerfreundlichen Ermittlung Ihres Anliegens (Art. 6 Abs. 1 lit. f DSGVO). Sofern eine entsprechende Einwilligung abgefragt wurde, erfolgt die Verarbeitung ausschließlich auf Grundlage von Art. 6 Abs. 1 lit. a DSGVO und § 25 Abs. 1 TTDSG, soweit die Einwilligung die Speicherung von Cookies oder den Zugriff auf Informationen im Endgerät des Nutzers (z. B. Device-Fingerprinting) im Sinne des TTDSG umfasst. Die Einwilligung ist jederzeit widerrufbar.<br>Die von Ihnen im Formular eingegebenen Daten verbleiben bei uns, bis Sie uns zur Löschung auffordern, Ihre Einwilligung zur Speicherung widerrufen oder der Zweck für die Datenspeicherung entfällt (z. B. nach abgeschlossener Bearbeitung Ihrer Anfrage). Zwingende gesetzliche Bestimmungen – insbesondere Aufbewahrungsfristen – bleiben unberührt.<br>Weitere Informationen entnehmen Sie der Datenschutzerklärung von Google unter https://policies.google.com/.<br><br><b>Auftragsverarbeitung</b><br>Wir haben einen Vertrag über Auftragsverarbeitung (AVV) zur Nutzung des oben genannten Dienstes geschlossen. Hierbei handelt es sich um einen datenschutzrechtlich vorgeschriebenen Vertrag, der gewährleistet, dass dieser die personenbezogenen Daten unserer Websitebesucher nur nach unseren Weisungen und unter Einhaltung der DSGVO verarbeitet.<br></p><h2 style="font-size: 21px;">Calendly</h2><p>Auf unserer Website haben Sie die Möglichkeit, Termine mit uns zu vereinbaren. Für die Terminbuchung nutzen wir das Tool „Calendly“. Anbieter ist die Calendly LLC, 271 17th St NW, 10th Floor, Atlanta, Georgia 30363, USA (nachfolgend „Calendly“).<br>Zum Zweck der Terminbuchung geben Sie die abgefragten Daten und den Wunschtermin in die dafür vorgesehene Maske ein. Die eingegebenen Daten werden für die Planung, Durchführung und ggf. für die Nachbereitung des Termins verwendet. Die Termindaten werden für uns auf den Servern von Calendly gespeichert, dessen Datenschutzerklärung Sie hier einsehen können: https://calendly.com/de/pages/privacy.<br>Die von Ihnen eingegebenen Daten verbleiben bei uns, bis Sie uns zur Löschung auffordern, Ihre Einwilligung zur Speicherung widerrufen oder der Zweck für die Datenspeicherung entfällt. Zwingende gesetzliche Bestimmungen – insbesondere Aufbewahrungsfristen – bleiben unberührt.<br>Rechtsgrundlage für die Datenverarbeitung ist Art. 6 Abs. 1 lit. f DSGVO. Der Websitebetreiber hat ein berechtigtes Interesse an einer möglichst unkomplizierten Terminvereinbarung mit Interessenten und Kunden. Sofern eine entsprechende Einwilligung abgefragt wurde, erfolgt die Verarbeitung ausschließlich auf Grundlage von Art. 6 Abs. 1 lit. a DSGVO und § 25 Abs. 1 TTDSG, soweit die Einwilligung die Speicherung von Cookies oder den Zugriff auf Informationen im Endgerät des Nutzers (z. B. Device-Fingerprinting) im Sinne des TTDSG umfasst. Die Einwilligung ist jederzeit widerrufbar.<br>Die Datenübertragung in die USA wird auf die Standardvertragsklauseln der EU-Kommission gestützt. Details finden Sie hier: https://calendly.com/pages/dpa.<br><br><b>Auftragsverarbeitung</b><br>Wir haben einen Vertrag über Auftragsverarbeitung (AVV) zur Nutzung des oben genannten Dienstes geschlossen. Hierbei handelt es sich um einen datenschutzrechtlich vorgeschriebenen Vertrag, der gewährleistet, dass dieser die personenbezogenen Daten unserer Websitebesucher nur nac
55h unseren Weisungen und unter Einhaltung der DSGVO verarbeitet.<br></p><h2 style="font-size: 30px;">5. Analyse-Tools und Werbung</h2><h2 style="font-size: 21px;">Google Tag Manager</h2><p>Wir setzen den Google Tag Manager ein. Anbieter ist die Google Ireland Limited, Gordon House, Barrow Street, Dublin 4, Irland.<br>Der Google Tag Manager ist ein Tool, mit dessen Hilfe wir Tracking- oder Statistik-Tools und andere Technologien auf unserer Website einbinden können. Der Google Tag Manager selbst erstellt keine Nutzerprofile, speichert keine Cookies und nimmt keine eigenständigen Analysen vor. Er dient lediglich der Verwaltung und Ausspielung der über ihn eingebundenen Tools. Der Google Tag Manager erfasst jedoch Ihre IP-Adresse, die auch an das Mutterunternehmen von Google in die Vereinigten Staaten übertragen werden kann.<br>Der Einsatz des Google Tag Managers erfolgt auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO. Der Websitebetreiber hat ein berechtigtes Interesse an einer schnellen und unkomplizierten Einbindung und Verwaltung verschiedener Tools auf seiner Website. Sofern eine entsprechende Einwilligung abgefragt wurde, erfolgt die Verarbeitung ausschließlich auf Grundlage von Art. 6 Abs. 1 lit. a DSGVO und § 25 Abs. 1 TTDSG, soweit die Einwilligung die Speicherung von Cookies oder den Zugriff auf Informationen im Endgerät des Nutzers (z. B. Device-Fingerprinting) im Sinne des TTDSG umfasst. Die Einwilligung ist jederzeit widerrufbar.<br></p><h2 style="font-size: 21px;">Google Analytics</h2><p>Diese Website nutzt Funktionen des Webanalysedienstes Google Analytics. Anbieter ist die Google Ireland Limited („Google“), Gordon House, Barrow Street, Dublin 4, Irland.<br>Google Analytics ermöglicht es dem Websitebetreiber, das Verhalten der Websitebesucher zu analysieren. Hierbei erhält der Websitebetreiber verschiedene Nutzungsdaten, wie z. B. Seitenaufrufe, Verweildauer, verwendete Betriebssysteme und Herkunft des Nutzers. Diese Daten werden in einer User-ID zusammengefasst und dem jeweiligen Endgerät des Websitebesuchers zugeordnet.<br>Des Weiteren können wir mit Google Analytics u. a. Ihre Maus- und Scrollbewegungen und Klicks aufzeichnen. Ferner verwendet Google Analytics verschiedene Modellierungsansätze, um die erfassten Datensätze zu ergänzen und setzt Machine-Learning-Technologien bei der Datenanalyse ein.<br>Google Analytics verwendet Technologien, die die Wiedererkennung des Nutzers zum Zwecke der Analyse des Nutzerverhaltens ermöglichen (z. B. Cookies oder Device-Fingerprinting). Die von Google erfassten Informationen über die Benutzung dieser Website werden in der Regel an einen Server von Google in den USA übertragen und dort gespeichert.<br>Die Nutzung dieses Dienstes erfolgt auf Grundlage Ihrer Einwilligung nach Art. 6 Abs. 1 lit. a DSGVO und § 25 Abs. 1 TTDSG. Die Einwilligung ist jederzeit widerrufbar.<br>Die Datenübertragung in die USA wird auf die Standardvertragsklauseln der EU-Kommission gestützt. Details finden Sie hier: https://privacy.google.com/businesses/controllerterms/mccs/.<br></p><h2 style="font-size: 21px;">Browser Plugin</h2><p>Sie können die Erfassung und Verarbeitung Ihrer Daten durch Google verhindern, indem Sie das unter dem folgenden Link verfügbare Browser-Plugin herunterladen und installieren: https://tools.google.com/dlpage/gaoptout?hl=de.<br>Mehr Informationen zum Umgang mit Nutzerdaten bei Google Analytics finden Sie in der Datenschutzerklärung von Google: https://support.google.com/analytics/answer/6004245?hl=de.<br></p><h2 style="font-size: 21px;">Google Signale</h2><p>Wir nutzen Google-Signale. Wenn Sie unsere Website besuchen, erfasst Google Analytics u. a. Ihren Standort, Suchverlauf und YouTube-Verlauf sowie demografische Daten (Besucherdaten). Diese Daten können mit Hilfe von Google-Signal für personalisierte Werbung verwendet werden. Wenn Sie über ein Google-Konto verfügen, werden die Besucherdaten von Google-Signal mit Ihrem Google-Konto verknüpft und für personalisierte Werbebotschaften verwendet. Die Daten werden außerdem für die Erstellung anonymisierter Statistiken zum Nutzerverhalten unserer User verwendet.<br><br><b>Auftragsverarbeitung</b><br>Wir haben mit Google einen Vertrag zur Auftragsverarbeitung abgeschlossen und setzen die strengen Vorgaben der deutschen Datenschutzbehörden bei der Nutzung von Google Analytics vollständig um.<br></p><h2 style="font-size: 21px;">Google Ads</h2><p>Der Websitebetreiber verwendet Google Ads. Google Ads ist ein Online-Werbeprogramm der Google Ireland Limited („Google“), Gordon House, Barrow Street, Dublin 4, Irland.<br>Google Ads ermöglicht es uns Werbeanzeigen in der Google-Suchmaschine oder auf Drittwebseiten auszuspielen, wenn der Nutzer bestimmte Suchbegriffe bei Google eingibt (Keyword-Targeting). Ferner können zielgerichtete Werbeanzeigen anhand der bei Google vorhandenen Nutzerdaten (z. B. Standortdaten und Interessen) ausgespielt werden (Zielgruppen-Targeting). Wir als Websitebetreiber können diese Daten quantitativ auswerten, indem wir beispielsweise analysieren, welche Suchbegriffe zur Ausspielung unserer Werbeanzeigen geführt haben und wie viele Anzeigen zu entsprechenden Klicks geführt haben.<br>Die Nutzung dieses Dienstes erfolgt auf Grundlage Ihrer Einwilligung nach Art. 6 Abs. 1 lit. a DSGVO und § 25 Abs. 1 TTDSG. Die Einwilligung ist jederzeit widerrufbar.<br>Die Datenübertragung in die USA wird auf die Standardvertragsklauseln der EU-Kommission gestützt. Details finden Sie hier: https://policies.google.com/privacy/frameworks und https://privacy.google.com/businesses/controllerterms/mccs/.<br></p><h2 style="font-size: 21px;">Google Ads Remarking</h2><p>Diese Website nutzt die Funktionen von Google Ads Remarketing. Anbieter ist die Google Ireland Limited („Google“), Gordon House, Barrow Street, Dublin 4, Irland.<br>Mit Google Ads Remarketing können wir Personen, die mit unserem Online-Angebot interagieren bestimmten Zielgruppen zuordnen, um ihnen anschließend interessenbezogene Werbung im Google-Werbenetzwerk anzeigen zu lassen (Remarketing bzw. Retargeting).<br>Des Weiteren können die mit Google Ads Remarketing erstellten Werbe-Zielgruppen mit den geräteübergreifenden Funktionen von Google verknüpft werden. Auf diese Weise können interessenbezogene, personalisierte Werbebotschaften, die in Abhängigkeit Ihres früheren Nutzungs- und Surfverhaltens auf einem Endgerät (z. B. Handy) an Sie angepasst wurden auch auf einem anderen Ihrer Endgeräte (z. B. Tablet oder PC) angezeigt werden.<br>Wenn Sie über einen Google-Account verfügen, können Sie der personalisierten Werbung unter folgendem Link widersprechen: https://www.google.com/settings/ads/onweb/.<br>Die Nutzung dieses Dienstes erfolgt auf Grundlage Ihrer Einwilligung nach Art. 6 Abs. 1 lit. a DSGVO und § 25 Abs. 1 TTDSG. Die Einwilligung ist jederzeit widerrufbar.<br>Weitergehende Informationen und die Datenschutzbestimmungen finden Sie in der Datenschutzerklärung von Google unter: https://policies.google.com/technologies/ads?hl=de.<br></p><h2 style="font-size: 21px;">Google Conversion Tracking</h2><p>Diese Website nutzt Google Conversion Tracking. Anbieter ist die Google Ireland Limited („Google“), Gordon House, Barrow Street, Dublin 4, Irland.<br>Mit Hilfe von Google-Conversion-Tracking können Google und wir erkennen, ob der Nutzer bestimmte Aktionen durchgeführt hat. So können wir beispielsweise auswerten, welche Buttons auf unserer Website wie häufig geklickt und welche Produkte besonders häufig angesehen oder gekauft wurden. Diese Informationen dienen dazu, Conversion-Statistiken zu erstellen. Wir erfahren die Gesamtanzahl der Nutzer, die auf unsere Anzeigen geklickt haben und welche Aktionen sie durchgeführt haben. Wir erhalten keine Informationen, mit denen wir den Nutzer persönlich identifizieren können. Google selbst nutzt zur Identifikation Cookies oder vergleichbare Wiedererkennungstechnologien.<br>Die Nutzung dieses Dienstes erfolgt auf Grundlage Ihrer Einwilligung nach Art. 6 Abs. 1 lit. a DSGVO und § 25 Abs. 1 TTDSG. Die Einwilligung ist jederzeit widerrufbar.<br>Mehr Informationen zu Google Conversion-Tracking finden Sie in den Datenschutzbestimmungen von Google: https://policies.google.com/privacy?hl=de.<br></p><h2 style="font-size: 21px;">META Conversion API</h2><p>Wir haben Facebook Conversion API auf dieser Website eingebunden. Anbieter dieses Dienstes ist die Meta Platforms Ireland Limited, 4 Grand Canal Square, Dublin 2, Irland. Die erfassten Daten werden nach Aussage von Facebook jedoch auch in die USA und in andere Drittländer übertragen.<br>Facebook Conversion API ermöglicht es uns, die Interaktionen des Websitebesuchers mit unserer Website zu erfassen und an Facebook weiterzugeben, um die Werbeperformance bei Facebook zu verbessern.<br>Hierzu werden insbesondere Zeitpunkt des Aufrufes, die aufgerufene Webseite, Ihre IP-Adresse und Ihr User-Agent sowie ggf. weitere spezifische Daten (z. B. gekaufte Produkte, Wert des Warenkorbs und Währung) erfasst. Eine vollständige Übersicht über die erfassbaren Daten finden Sie hier: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters.<br>Die Nutzung dieses Dienstes erfolgt auf Grundlage Ihrer Einwilligung nach Art. 6 Abs. 1 lit. a DSGVO und § 25 Abs. 1 TTDSG. Die Einwilligung ist jederzeit widerrufbar.<br>Soweit mit Hilfe des hier beschriebenen Tools personenbezogene Daten auf unserer Website erfasst und an Facebook weitergeleitet werden, sind wir und die Meta Platforms Ireland Limited, 4 Grand Canal Square, Grand Canal Harbour, Dublin 2, Irland gemeinsam für diese Datenverarbeitung verantwortlich (Art. 26 DSGVO). Die gemeinsame Verantwortlichkeit beschränkt sich dabei ausschließlich auf die Erfassung der Daten und deren Weitergabe an Facebook. Die nach der Weiterleitung erfolgende Verarbeitung durch Facebook ist nicht Teil der gemeinsamen Verantwortung. Die uns gemeinsam obliegenden Verpflichtungen wurden in einer Vereinbarung über gemeinsame Verarbeitung festgehalten. Den Wortlaut der Vereinbarung finden Sie unter: https://www.facebook.com/legal/controller_addendum. Laut dieser Vereinbarung sind wir für die Erteilung der Datenschutzinformationen beim Einsatz des Facebook-Tools und für die datenschutzrechtlich sichere Implementierung des Tools auf unserer Website verantwortlich. Für die Datensicherheit der Facebook-Produkte ist Facebook verantwortlich. Betroffenenrechte (z. B. Auskunftsersuchen) hinsichtlich der bei Facebook verarbeiteten Daten können Sie direkt bei Facebook geltend machen. Wenn Sie die Betroffenenrechte bei uns geltend machen, sind wir verpflichtet, diese an Facebook weiterzuleiten.<br>Die Datenübertragung in die USA wird auf die Standardvertragsklauseln der EU-Kommission gestützt. Details finden Sie hier: https://www.facebook.com/legal/EU_data_transfer_addendum und https://de-de.facebook.com/help/566994660333381.<br>In den Datenschutzhinweisen von Facebook finden Sie weitere Hinweise zum Schutz Ihrer Privatsphäre: https://de-de.facebook.com/about/privacy/.<br><br><b>Auftragsverarbeitung</b><br>Wir haben einen Vertrag über Auftragsverarbeitung (AVV) zur Nutzung des oben genannten Dienstes geschlossen. Hierbei handelt es sich um einen datenschutzrechtlich vorgeschriebenen Vertrag, der gewährleistet, dass dieser die personenbezogenen Daten unserer Websitebesucher nur nac
55h unseren Weisungen und unter Einhaltung der DSGVO verarbeitet.<br></p><h2 style="font-size: 21px;">META Custom Audience</h2><p>Wir nutzen Facebook Custom Audiences. Anbieter dieses Dienstes ist die Meta Platforms Ireland Limited, 4 Grand Canal Square, Dublin 2, Irland.<br>Wenn Sie unsere Webseiten und Apps besuchen bzw. nutzen, unsere kostenlosen oder kostenpflichtigen Angebote in Anspruch nehmen, Daten an uns übermitteln oder mit den Facebook-Inhalten unseres Unternehmens interagieren, erfassen wir hierbei Ihre personenbezogenen Daten. Sofern Sie uns eine Einwilligung zur Nutzung von Facebook Custom Audiences erteilen, werden wir diese Daten an Facebook übermitteln, mit denen Facebook Ihnen passende Werbung ausspielen kann. Des Weiteren können mit Ihren Daten Zielgruppen definiert werden (Lookalike Audiences).<br>Facebook verarbeitet diese Daten als unser Auftragsverarbeiter. Details sind der Nutzungsvereinbarung von Facebook zu entnehmen: https://www.facebook.com/legal/terms/customaudience.<br>Die Nutzung dieses Dienstes erfolgt auf Grundlage Ihrer Einwilligung nach Art. 6 Abs. 1 lit. a DSGVO und § 25 Abs. 1 TTDSG. Die Einwilligung ist jederzeit widerrufbar.<br>Die Datenübertragung in die USA wird auf die Standardvertragsklauseln der EU-Kommission gestützt. Details finden Sie hier: https://www.facebook.com/legal/terms/customaudience und https://www.facebook.com/legal/terms/dataprocessing<br></p><h2 style="font-size: 30px;">6. Newsletter</h2><h2 style="font-size: 21px;">Newsletterdaten</h2><p>
55Wenn Sie den auf der Website angebotenen Newsletter beziehen möchten, benötigen wir von Ihnen eine E-Mail-Adresse sowie Informationen, welche uns die Überprüfung gestatten, dass Sie der Inhaber der angegebenen E-Mail-Adresse und mit dem Empfang des Newsletters einverstanden sind. Weitere Daten werden nicht bzw. nur auf freiwilliger Basis erhoben. Für die Abwicklung der Newsletter nutzen wir Newsletterdiensteanbieter, die nachfolgend beschrieben werden.<br></p><h2 style="font-size: 21px;">Klicktipp</h2><p>Für den Versand unseres Newsletters nutzen wir Klick-Tipp.&nbsp;</p><div>Anbieter ist die Klick-Tipp Limited, 15 Cambridge Court,<br>210 Shepherd’s Bush Road, London W6 7NJ, Vereinigtes<br>Königreich (nachfolgend Klick-Tipp).<br>Wenn Sie sich für unseren Newsletter anmelden, erheben wir Ihre E-Mail-Adresse sowie Ihren Vornamen. Die Anmeldung erfolgt im Double-Opt-in-Verfahren: Nach Ihrer Eintragung erhalten Sie eine E-Mail, in der Sie die Anmeldung bestätigen müssen. Zum Nachweis Ihrer Einwilligung speichern wir zusätzlich Ihre IP Adresse sowie Datum und Uhrzeit von Anmeldung und Bestätigung. Klick-Tipp ermöglicht es uns, Newsletter-Empfänger anhand verschiedener Merkmale zu unterteilen (sogenanntes Tagging) und auszuwerten, ob eine Nachricht geöffnet und welche Links darin angeklickt wurden. So können wir unsere<br>Newsletter besser auf die Interessen unserer Empfänger abstimmen. Rechtsgrundlage für die Verarbeitung ist Ihre Einwilligung nach Art. 6 Abs. 1 lit. a DSGVO. Sie können diese Einwilligung jederzeit mit Wirkung für die Zukunft widerrufen, indem Sie den Abmeldelink in jeder E-Mail nutzen oder uns eine Nachricht senden. Die Rechtmäßigkeit der bereits erfolgten Verarbeitung bleibt vom Widerruf unberührt. Ihre Daten verbleiben bei uns und auf den Servern von Klick-Tipp, bis Sie sich vom Newsletter abmelden. Nach der Abmeldung werden sie gelöscht. Daten, die zu anderen Zwecken bei uns gespeichert sind, bleiben hiervon unberührt. Zum Nachweis der ursprünglich erteilten Einwilligung können wir die Anmeldedaten bis zum Ablauf möglicher Verjährungsfristen aufbewahren. Das Vereinigte Königreich ist ein Drittstaat im Sinne der DSGVO. Die Europäische Kommission hat mit Angemessenheitsbeschluss nach Art. 45 DSGVO festgestellt, dass dort ein angemessenes Datenschutzniveau besteht; der Datentransfer ist deshalb zulässig. Details zur Datenverarbeitung entnehmen Sie der Datenschutzerklärung von Klick-Tipp: https://www.klicktipp.com/datenschutzerklarung/<br><br><b>Auftragsverarbeitung</b><br>Wir haben einen Vertrag über Auftragsverarbeitung (AVV) zur Nutzung des oben genannten Dienstes geschlossen. Hierbei handelt es sich um einen datenschutzrechtlich vorgeschriebenen Vertrag, der gewährleistet, dass dieser die personenbezogenen Daten unserer Websitebesucher nur nach unseren Weisungen und unter Einhaltung der DSGVO verarbeitet.<br></div><p></p><h2 style="font-size: 30px;">7. Plugins und Tools</h2><h2 style="font-size: 21px;">Wistia</h2><div>Diese Website bindet Videos der Videoplattform Wistia ein.<br>Anbieter ist die Wistia, Inc., 17 Tudor Street, Cambridge, MA 02139, USA (nachfolgend Wistia).<br>Sobald Sie eine Seite mit einem eingebundenen Wistia-Video<br>aufrufen, wird eine Verbindung zu den Servern von Wistia hergestellt. Dabei wird dem Wistia-Server unter anderem mitgeteilt, welche unserer Seiten Sie besucht haben, und Ihre IP-Adresse übermittelt. Dies geschieht unabhängig davon, ob Sie das Video tatsächlich abspielen.<br>Wistia erfasst darüber hinaus Informationen über Ihre Nutzung der Videos – etwa welche Videos Sie ansehen, wann Sie die Wiedergabe starten und stoppen und wie oft Sie ein Video aufrufen. Zu diesem Zweck kann Wistia Cookies auf Ihrem Endgerät speichern oder vergleichbare<br>Wiedererkennungstechnologien einsetzen. Diese Daten dienen uns zur Auswertung und Verbesserung unserer Videoinhalte. Sind Sie in einem Wistia-Konto eingeloggt, kann Wistia Ihr Nutzungsverhalten Ihrem Profil zuordnen; dies können Sie verhindern, indem Sie sich vor dem Besuch unserer Website aus Ihrem Wistia-Konto abmelden.<br>Die Nutzung dieses Dienstes erfolgt ausschließlich auf Grundlage Ihrer Einwilligung nach Art. 6 Abs. 1 lit. a DSGVO und § 25 Abs. 1 TTDSG. Die Einwilligung ist jederzeit<br>mit Wirkung für die Zukunft widerrufbar. Solange Sie keineEinwilligung erteilt haben, werden keine Daten an Wistia<br>übermittelt.<br>Wistia verarbeitet Ihre Daten in den USA. Sie willigen mit Ihrer Zustimmung im Cookie-Banner<br>ausdrücklich auch in diese Datenübermittlung ein<br>(Art. 49 Abs. 1 lit. a DSGVO).<br>Weitere Informationen zum Umgang mit Nutzerdaten finden Sie<br>in der Datenschutzerklärung von Wistia unter:<br>https://wistia.com/privacy<br><br><b>Auftragsverarbeitung</b><br>Wir haben einen Vertrag über Auftragsverarbeitung (AVV) zur Nutzung des oben genannten Dienstes geschlossen. Hierbei handelt es sich um einen datenschutzrechtlich vorgeschriebenen Vertrag, der gewährleistet, dass dieser die personenbezogenen Daten unserer Websitebesucher nur nac
55h unseren Weisungen und unter Einhaltung der DSGVO verarbeitet.<br></div><h2 style="font-size: 21px;">Google Font (lokales Hosting)</h2><div>Diese Seite nutzt zur einheitlichen Darstellung von Schriftarten so genannte Google Fonts, die von Google bereitgestellt werden. Die Google Fonts sind lokal installiert. Eine Verbindung zu Servern von Google findet dabei nicht statt.<br>Weitere Informationen zu Google Fonts finden Sie unter https://developers.google.com/fonts/faq und in der Datenschutzerklärung von Google: https://policies.google.com/privacy?hl=de.<br></div><h2 style="font-size: 21px;">Font Awesome (lokales Hosting)</h2><div>Diese Seite nutzt zur einheitlichen Darstellung von Schriftarten Font Awesome. Font Awesome ist lokal installiert. Eine Verbindung zu Servern von Fonticons, Inc. findet dabei nicht statt.<br>Weitere Informationen zu Font Awesome finden Sie in der Datenschutzerklärung für Font Awesome unter: https://fontawesome.com/privacy.<h2 style="font-size: 21px;">Font Awesome (lokales Hosting)</h2><br></div><div>Diese Seite nutzt zur einheitlichen Darstellung von Schriftarten Font Awesome. Font Awesome ist lokal installiert. Eine Verbindung zu Servern von Fonticons, Inc. findet dabei nicht statt.<br>Weitere Informationen zu Font Awesome finden Sie in der Datenschutzerklärung für Font Awesome unter: https://fontawesome.com/privacy.<br><h2 style="font-size: 21px;">Webinarjam</h2><div>Wir nutzen WebinarJam zur Veranstaltung und Abwicklung von Online-Webinaren. Anbieter dieses Dienstes ist die Genesis LLC, 7660 Fay Ave #H184, La Jolla, Kalifornien (USA).<br>Wenn Sie an einem unserer Webinare teilnehmen, werden Ihre personenbezogenen Daten auf den Servern von WebinarJam gespeichert. Dies umfasst insbesondere Ihre IP-Adresse, sowie alle Inhalte, die Sie selbst bei WebinarJam eingeben (z. B. Ihre E-Mail-Adresse oder Chatnachrichten).<br>Rechtsgrundlage für die Nutzung von WebinarJam ist Art. 6 Abs. 1 lit. b DSGVO (Vertragserfüllung) und unser berechtigtes Interesse an einer professionellen Abwicklung unserer Webinare (Art. 6 Abs. 1 lit. f DSGVO). Sofern eine entsprechende Einwilligung abgefragt wurde, erfolgt die Verarbeitung ausschließlich auf Grundlage von Art. 6 Abs. 1 lit. a DSGVO und § 25 Abs. 1 TTDSG, soweit die Einwilligung die Speicherung von Cookies oder den Zugriff auf Informationen im Endgerät des Nutzers (z. B. Device-Fingerprinting) im Sinne des TTDSG umfasst. Die Einwilligung ist jederzeit widerrufbar.<br>Die Datenübertragung in die USA wird auf die Standardvertragsklauseln der EU-Kommission gestützt. Details finden Sie hier: https://home.webinarjam.com/dpa.<br>Details zur Datenverarbeitung entnehmen Sie der Datenschutzerklärung von WebinarJam: https://home.webinarjam.com/privacypolicy.<br><br><b>Auftragsverarbeitung</b><br>Wir haben einen Vertrag über Auftragsverarbeitung (AVV) zur Nutzung des oben genannten Dienstes geschlossen. Hierbei handelt es sich um einen datenschutzrechtlich vorgeschriebenen Vertrag, der gewährleistet, dass dieser die personenbezogenen Daten unserer Websitebesucher nur nach unseren Weisungen und unter Einhaltung der DSGVO verarbeitet.<br></div><br><h2 style="font-size: 30px;">8. Audio- und Video-Konferenzen</h2><h2 style="font-size: 21px;">Datenverarbeitung</h2></div><div>Für die Kommunikation mit unseren Kunden setzen wir unter anderen Online-Konferenz-Tools ein. Die im Einzelnen von uns genutzten Tools sind unten aufgelistet. Wenn Sie mit uns per Video- oder Audiokonferenz via Internet kommunizieren, werden Ihre personenbezogenen Daten von uns und dem Anbieter des jeweiligen Konferenz-Tools erfasst und verarbeitet.<br>Die Konferenz-Tools erfassen dabei alle Daten, die Sie zur Nutzung der Tools bereitstellen/einsetzen (E-Mail-Adresse und/oder Ihre Telefonnummer). Ferner verarbeiten die Konferenz-Tools die Dauer der Konferenz, Beginn und Ende (Zeit) der Teilnahme an der Konferenz, Anzahl der Teilnehmer und sonstige „Kontextinformationen“ im Zusammenhang mit dem Kommunikationsvorgang (Metadaten).<br>Des Weiteren verarbeitet der Anbieter des Tools alle technischen Daten, die zur Abwicklung der Online-Kommunikation erforderlich sind. Dies umfasst insbesondere IP-Adressen, MAC-Adressen, Geräte-IDs, Gerätetyp, Betriebssystemtyp und -version, Client-Version, Kameratyp, Mikrofon oder Lautsprecher sowie die Art der Verbindung.<br>
55Sofern innerhalb des Tools Inhalte ausgetauscht, hochgeladen oder in sonstiger Weise bereitgestellt werden, werden diese ebenfalls auf den Servern der Tool-Anbieter gespeichert. Zu solchen Inhalten zählen insbesondere Cloud-Aufzeichnungen, Chat-/ Sofortnachrichten, Voicemails hochgeladene Fotos und Videos, Dateien, Whiteboards und andere Informationen, die während der Nutzung des Dienstes geteilt werden.<br>Bitte beachten Sie, dass wir nicht vollumfänglich Einfluss auf die Datenverarbeitungsvorgänge der verwendeten Tools haben. Unsere Möglichkeiten richten sich maßgeblich nach der Unternehmenspolitik des jeweiligen Anbieters. Weitere Hinweise zur Datenverarbeitung durch die Konferenztools entnehmen Sie den Datenschutzerklärungen der jeweils eingesetzten Tools, die wir unter diesem Text aufgeführt haben.<br></div><h2 style="font-size: 21px;">Zweck und Rechtsgrundlagen</h2><div>Die Konferenz-Tools werden genutzt, um mit angehenden oder bestehenden Vertragspartnern zu kommunizieren oder bestimmte Leistungen gegenüber unseren Kunden anzubieten (Art. 6 Abs. 1 lit. b DSGVO). Des Weiteren dient der Einsatz der Tools der allgemeinen Vereinfachung und Beschleunigung der Kommunikation mit uns bzw. unserem Unternehmen (berechtigtes Interesse im Sinne von Art. 6 Abs. 1 lit. f DSGVO). Soweit eine Einwilligung abgefragt wurde, erfolgt der Einsatz der betreffenden Tools auf Grundlage dieser Einwilligung; die Einwilligung ist jederzeit mit Wirkung für die Zukunft widerrufbar.<br></div><h2 style="font-size: 21px;">Speicherdauer</h2><div>Die unmittelbar von uns über die Video- und Konferenz-Tools erfassten Daten werden von unseren Systemen gelöscht, sobald Sie uns zur Löschung auffordern, Ihre Einwilligung zur Speicherung widerrufen oder der Zweck für die Datenspeicherung entfällt. Gespeicherte Cookies verbleiben auf Ihrem Endgerät, bis Sie sie löschen. Zwingende gesetzliche Aufbewahrungsfristen bleiben unberührt.<br>Auf die Speicherdauer Ihrer Daten, die von den Betreibern der Konferenz-Tools zu eigenen Zwecken gespeichert werden, haben wir keinen Einfluss. Für Einzelheiten dazu informieren Sie sich bitte direkt bei den Betreibern der Konferenz-Tools.<br></div><h2 style="font-size: 21px;">Eingesetzte Konferenz-Tools:</h2><div>Wir setzen folgende Konferenz-Tools ein:<br></div><h2 style="font-size: 21px;">Zoom</h2><div>Wir nutzen Zoom. Anbieter dieses Dienstes ist die Zoom Communications Inc., San Jose, 55 Almaden Boulevard, 6th Floor, San Jose, CA 95113, USA. Details zur Datenverarbeitung entnehmen Sie der Datenschutzerklärung von Zoom: https://zoom.us/de-de/privacy.html.<br>Die Datenübertragung in die USA wird auf die Standardvertragsklauseln der EU-Kommission gestützt. Details finden Sie hier: https://zoom.us/de-de/privacy.html.<br><br><b>Auftragsverarbeitung</b><br>Wir haben einen Vertrag über Auftragsverarbeitung (AVV) zur Nutzung des oben genannten Dienstes geschlossen. Hierbei handelt es sich um einen datenschutzrechtlich vorgeschriebenen Vertrag, der gewährleistet, dass dieser die personenbezogenen Daten unserer Websitebesucher nur nach unseren Weisungen und unter Einhaltung der DSGVO verarbeitet.<br></div><div><br>Quelle: <a href="https://www.e-recht24.de" target="_blank" rel="noopener noreferrer" style="color: inherit; text-decoration-color: currentcolor; text-underline-offset: 0.2em;">https://www.e-recht24.de</a><br></div></div>
56<footer>
57  <span>© 2026 Mindrebel. Alle Rechte vorbehalten.</span>
58  <span><a href="https://mindrebel.eu/impressum">Impressum</a> · <a href="https://mindrebel.eu/datenschutz">Datenschutz</a> · <a href="https://mindrebel.eu/agb">AGB</a></span>
59</footer>
60
61<script data-htmlpub-analytics>
62(function(){
63var PID="v8s3dfzxmk03xqctda3738k8";
64var EXP_ID="";
65var EXP_VARIANT="";
66var API="/collect";
67var APP_ORIGIN="https://htmlpub.com";
68var FLUSH_INTERVAL=5000;
69var FLUSH_MAX=20;
70
71// HP-4568 consent gate. Runs onGranted() at most once, only when analytics
72// storage is permitted. See app/src/lib/analytics-consent-gate.ts.
73function hpConsent(onGranted){
74var started=false;
75function start(){if(started)return;started=true;try{onGranted();}catch(e){}}
76
77// Browser-level objection (Global Privacy Control / legacy Do-Not-Track) is
78// terminal: it is the visitor speaking directly, so no later CMP "accept all"
79// re-enables tracking for this page view. Mirrors the Customers.ai pixel gate.
80try{
81if(navigator.globalPrivacyControl===true)return;
82var dnt=navigator.doNotTrack||window.doNotTrack||navigator.msDoNotTrack;
83if(dnt==="1"||dnt===1||dnt===true)return;
84}catch(e){}
85
86// Analytics/statistics consent per whichever CMP the page carries:
87//    1 granted / 0 denied / -1 present but undecided / null no CMP detected
88function hpConsentState(){
89// Google consent mode v2 first: vendor-neutral, so a CMP writing it (directly
90// or via GTM) is authoritative even before its own JS API initialises. Last
91// write wins, matching gtag's 'default' then 'update'.
92try{
93var dl=window.dataLayer;
94if(dl&&dl.length){
95var seen=null;
96for(var i=0;i<dl.length;i++){
97var entry=dl[i];
98if(!entry||entry[0]!=="consent"||!entry[2])continue;
99var mode=entry[2].analytics_storage;
100if(mode==="granted")seen=1;else if(mode==="denied")seen=0;
101}
102if(seen!==null)return seen;
103}
104}catch(e){}
105// Cookiebot. Its object appears only once uc.js has run, so fall back to the
106// script tag it is installed as to tell "undecided" from "absent".
107try{
108var cb=window.Cookiebot||window.CookieConsent;
109if(cb&&cb.consent)return cb.consent.statistics?1:(cb.hasResponse?0:-1);
110if(document.getElementById("Cookiebot")||document.querySelector("script[src*='consent.cookiebot.com'],script[src*='consentcdn.cookiebot.com']"))return -1;
111}catch(e){}
112// OneTrust: statistics is the reserved C0002 group. OneTrust comma-pads the
113// list itself, so the delimited match cannot hit C00020. Every runtime signal
114// it exposes appears only after otSDKStub.js runs, so without the script-tag
115// fallback a blocked or async stub reads as "no CMP" and we would track — the
116// exact bug, on the pages most likely to care.
117try{
118var groups=window.OptanonActiveGroups||window.OnetrustActiveGroups;
119if(typeof groups==="string"&&groups)return groups.indexOf(",C0002,")>-1?1:0;
120if(window.OneTrust||window.Optanon||document.getElementById("onetrust-consent-sdk")||document.querySelector("script[src*='cookielaw.org'],script[src*='onetrust.com']"))return -1;
121}catch(e){}
122// CookieYes
123try{
124if(typeof window.getCkyConsent==="function"){
125var cky=window.getCkyConsent();
126if(cky&&cky.categories)return cky.categories.analytics?1:0;
127}
128if(document.querySelector("script[src*='cookieyes.com'],script[src*='cookie-script.com']"))return -1;
129}catch(e){}
130// Iubenda: purpose 4 is measurement.
131try{
132var iub=window._iub;
133if(iub&&iub.cs&&iub.cs.consent&&iub.cs.consent.purposes)return iub.cs.consent.purposes[4]?1:0;
134if(iub||document.querySelector("script[src*='iubenda.com']"))return -1;
135}catch(e){}
136return null;
137}
138
139var state=hpConsentState();
140if(state===null||state===1){start();return;}
141
142// CMP present, analytics not granted: write nothing, send nothing, and listen
143// for a later grant. The poll is a catch-all for CMPs whose event names we do
144// not know; it stops after 60s rather than running forever.
145function recheck(){if(hpConsentState()===1)start();}
146var EVENTS=["CookiebotOnAccept","CookiebotOnConsentReady","CookieConsentDeclaration","cookieyes-consent-update","consent.onetrust","OneTrustGroupsUpdated","iubenda-consent-given","hp-analytics-consent"];
147for(var evt=0;evt<EVENTS.length;evt++){try{window.addEventListener(EVENTS[evt],recheck,false);}catch(e){}}
148var polls=0;
149var timer=setInterval(function(){recheck();if(started||++polls>=120)clearInterval(timer);},500);
150}
151
152// Everything below mints an identifier or sends a beacon, so all of it waits
153// for consent. window.__hpupPushEvent is the exception: it binds synchronously
154// here and only forwards once consent arrives. Binding it late would let the
155// popup embed bundle's fallback shim (app/src/lib/popups/embed-bundle.ts,
156// registered under the same "if not already defined" test) claim the name first
157// and reroute pop-up events. Pre-consent calls are dropped, not queued — they
158// have no visitorId. SYNC with app/src/lib/analytics-tracking-script.ts.
159var hpPush=null;
160if(!window.__hpupPushEvent){window.__hpupPushEvent=function(t,p){try{if(hpPush)hpPush(t,p);}catch(e){}};}
161hpConsent(function(){
162
163function 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)});}
164function 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();}}
165
166// Full visits from a customer's own site (offsite.js, lib/funnels/external-sessions.ts)
167// arrive with _cvsid: adopt it as this tab's session when there is none yet, so
168// the visit that started there continues here as one session, then drop it from
169// the address bar. SYNC with the other copy of this script.
170try{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){}
171var visitorId=getOrSet(localStorage,"_hp_vid");
172var sessionId=getOrSet(sessionStorage,"_hp_sid");
173
174var params=new URLSearchParams(location.search);
175var utmSource=params.get("utm_source")||undefined;
176var utmMedium=params.get("utm_medium")||undefined;
177var utmCampaign=params.get("utm_campaign")||undefined;
178// Ad-platform click IDs (conversion feedback loop) — captured like the UTMs.
179// SYNC with canonical at app/src/lib/analytics-tracking-script.ts.
180var gclid=params.get("gclid")||undefined;
181var fbclid=params.get("fbclid")||undefined;
182var msclkid=params.get("msclkid")||undefined;
183
184var vw=window.innerWidth||0;
185var vh=window.innerHeight||0;
186var ua=navigator.userAgent||"";
187var isMobileUA=/Android|iPhone|iPod|iPad|webOS|BlackBerry|IEMobile|Opera Mini/i.test(ua);
188var maxScreenDim=Math.max(screen.width||0,screen.height||0);
189var deviceType=isMobileUA?(maxScreenDim<=812||vw<768?"mobile":"tablet"):"desktop";
190
191function detectBrowser(){
192if(ua.indexOf("Edg/")>-1)return"Edge";
193if(ua.indexOf("Chrome/")>-1)return"Chrome";
194if(ua.indexOf("Safari/")>-1&&ua.indexOf("Chrome/")===-1)return"Safari";
195if(ua.indexOf("Firefox/")>-1)return"Firefox";
196return"Other";
197}
198function detectOS(){
199if(ua.indexOf("iPhone")>-1||ua.indexOf("iPad")>-1)return"iOS";
200if(ua.indexOf("Android")>-1)return"Android";
201if(ua.indexOf("Mac OS")>-1)return"macOS";
202if(ua.indexOf("Windows")>-1)return"Windows";
203if(ua.indexOf("Linux")>-1)return"Linux";
204return"Other";
205}
206var browserFamily=detectBrowser();
207var osFamily=detectOS();
208
209var buffer=[];
210function makeEvent(type,payload){
211return{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
211ight:vh,experimentId:EXP_ID||undefined,experimentVariant:EXP_VARIANT||undefined,timestamp:Date.now()};
212}
213function pushEvent(type,payload){buffer.push(makeEvent(type,payload));if(buffer.length>=FLUSH_MAX)flush();}
214
215// HP-1557 — expose a single-arg helper so the popups runtime (separate IIFE,
216// no shared closure with analytics) can feed events into the same buffer.
217// Defensive: only set if not already defined so a future second analytics
218// injection doesn't clobber an in-flight handler. HP-4568: the name is bound
219// above, outside the consent gate; this is where it starts forwarding.
220hpPush=pushEvent;
221// NOTE (HP-3461): this copy deliberately does NOT register window.__hpTrack, even
222// though the app copy registers both names. checkout-script.ts guards on
223// __hpTrack, and inject.ts appends that script to every edge-served page, so
224// adding the name here would start writing checkout_started for the entire edge
225// fleet (pubhtml.com + all custom domains) where that beacon has always been a
226// no-op — beginning an ecommerce event series mid-history, and moving the derived
227// checkout_abandoned with it, from a change about page engagement. It is a real
228// gap and wants its own change with a backfill story.
229// (No backticks anywhere in this file: it is one big template literal, so a
230// backtick in a comment terminates the string. This exact comment did it once.)
231
232function readMmuid(){try{var v=localStorage.getItem("mmuid");return v&&v.length<=128?v:undefined;}catch(e){return undefined;}}
233
234function flush(){
235if(buffer.length===0)return;
236var batch=buffer.splice(0,FLUSH_MAX);
237var payload={events:batch};
238var mmuid=readMmuid();
239if(mmuid)payload.mmuid=mmuid;
240try{navigator.sendBeacon(API,JSON.stringify(payload));}catch(e){}
241}
242
243// Engaged time-on-page (HP-3592) — count a second only while the tab is
244// visible AND the visitor was active in the last 30s; emit the accrued DELTA
245// every 30 engaged seconds and on hide/pagehide. Registered BEFORE the flush
246// handlers below so the final delta lands in the buffer before the last
247// beacon fires (listener registration order = firing order).
248// SYNC with canonical at app/src/lib/analytics-tracking-script.ts.
249var dwellSec=0,dwellSent=0,dwellLastActive=Date.now();
250var DWELL_IDLE_MS=30000,DWELL_EMIT_SEC=30,DWELL_CAP_SEC=1800;
251function dwellActive(){dwellLastActive=Date.now();}
252document.addEventListener("scroll",dwellActive,{passive:true});
253document.addEventListener("mousemove",dwellActive,{passive:true});
254document.addEventListener("keydown",dwellActive,true);
255document.addEventListener("touchstart",dwellActive,{passive:true});
256document.addEventListener("click",dwellActive,true);
257function dwellEmit(){var d=dwellSec-dwellSent;if(d>0){dwellSent=dwellSec;pushEvent("dwell",{seconds:d});}}
258var dwellTimer=setInterval(function(){
259if(document.visibilityState!=="visible")return;
260if(Date.now()-dwellLastActive>DWELL_IDLE_MS)return;
261dwellSec++;
262if(dwellSec-dwellSent>=DWELL_EMIT_SEC)dwellEmit();
263if(dwellSec>=DWELL_CAP_SEC){dwellEmit();clearInterval(dwellTimer);}
264},1000);
265document.addEventListener("visibilitychange",function(){if(document.visibilityState==="hidden")dwellEmit();});
266window.addEventListener("pagehide",dwellEmit);
267
268setInterval(flush,FLUSH_INTERVAL);
269document.addEventListener("visibilitychange",function(){if(document.visibilityState==="hidden")flush();});
270window.addEventListener("pagehide",flush);
271
272pushEvent("page_view");
273
274// On-platform conversion goal ("a thank-you page I own"): report the current
275// pageId plus the visitor's experiment assignments (_hp_exp_<id> cookies) so the
276// server can count this page-view as a conversion. Fires once per session per
277// page. HP-2925 Layer 1: must fire on EVERY page, not just experiment
278// participants — a standalone "Offsite event" goal has no experiment cookie, so
279// gating on cookies means the server never gets to attribute it. The server
280// no-ops cheaply when the page is neither an experiment nor a goal target.
281// SYNC with canonical at app/src/lib/analytics-tracking-script.ts.
282(function(){
283try{
284if(!APP_ORIGIN)return;
285var guard="_hp_goal:"+PID;
286if(sessionStorage.getItem(guard))return;
287sessionStorage.setItem(guard,"1");
288var exps=[];var ck=document.cookie.split(";");
289for(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())});}}
290var body=JSON.stringify({pageId:PID,visitorId:visitorId,sessionId:sessionId,experiments:exps});
291var url=APP_ORIGIN+"/api/experiments/onsite-conversion";
292if(navigator.sendBeacon){navigator.sendBeacon(url,new Blob([body],{type:"text/plain"}));}
293else{fetch(url,{method:"POST",headers:{"Content-Type":"application/json"},body:body,keepalive:true}).catch(function(){});}
294}catch(e){}
295})();
296
297// HP-3461 — milestones reached WITHOUT the visitor scrolling carry {auto:1}.
298// checkScroll runs on a 1s timer regardless of input and at scrollY=0 pct is
299// just winH/docH, so any page under ~4 screens tall banked scroll_depth:25 from
300// every visitor who did nothing (a page fitting the viewport banked 100). Those
301// rows mark the session engaged in the rollup, which pushed the bounce rate of
302// every short page toward zero and inflated avgScrollDepth the same way. The
303// events still fire (that depth WAS visible, and the scroll funnel wants it);
304// the stamp lets the bounce query demand a real interaction.
305// SYNC with app/src/lib/analytics-tracking-script.ts.
306var scrollMilestones={25:false,50:false,75:false,100:false};
307var scrollUserMoved=false;
308function checkScroll(){
309var docH=Math.max(document.body.scrollHeight,document.documentElement.scrollHeight);
310var scrollY=window.scrollY||window.pageYOffset;
311var winH=window.innerHeight;
312if(docH<=winH){if(!scrollMilestones[100]){scrollMilestones[100]=true;pushEvent("scroll_depth",{depth:100,auto:1});}return;}
313var pct=Math.round(((scrollY+winH)/docH)*100);
314[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});}});
315}
316window.addEventListener("scroll",function(){scrollUserMoved=true;checkScroll();},{passive:true});
317setTimeout(checkScroll,1000);
318
319// HP-1557 — when a click/submit happens inside a pop-up, add the popup's id +
320// name to the payload. Customers track conversions on the standard form_submit /
321// element_click types; this is just attribution metadata so the analytics surface
322// can say "this conversion came from the Welcome offer pop-up". Matches the
323// canonical data-persist-popup marker and the legacy data-htmlpub-popup one.
324function popupAttribution(node){
325var p=node&&node.closest&&node.closest("[data-persist-popup],[data-htmlpub-popup]");
326if(!p)return null;
327var nm=p.getAttribute("data-persist-popup-name");if(nm===null)nm=p.getAttribute("data-htmlpub-popup-name");
328return{popupId:p.getAttribute("id")||"",popupName:(nm||"").replace(/^\s+|\s+$/g,"")};
329}
330
331document.addEventListener("click",function(e){
332var el=e.target;
333if(!el||!el.tagName)return;
334var tag=el.tagName.toLowerCase();
335if(!(tag==="a"||tag==="button"||tag==="input"||tag==="select"||tag==="textarea"||el.getAttribute("role")==="button"||el.closest("a,button,[role=button]")))return;
336var docH=Math.max(document.body.scrollHeight,document.documentElement.scrollHeight,1);
337var rect=el.getBoundingClientRect();
338var 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};
339// HP-3227 — toggling "count clicks as conversions" on a button stamps
340// data-lp-conversion="<configId>" on that element, and both the rollup worker
341// and the dashboard read match a click goal by that id. This tracker never
342// reported it, so a goal whose stored selector is the placeholder
343// a[data-lp-conversion] had nothing to match on and counted zero conversions
344// forever, no matter how many buttons were toggled on. Read from the closest
345// stamped ancestor, since the click target is often an inner <span> of the CTA.
346// SYNC with app/src/lib/analytics-tracking-script.ts.
347var lpConvEl=el.closest?el.closest("[data-lp-conversion]"):null;
348var lpConvId=lpConvEl?lpConvEl.getAttribute("data-lp-conversion"):null;
349if(lpConvId)payload.lpConversion=lpConvId;
350// HP-1557 — only attribute element_click to a pop-up when the actual
351// click target is a button-shaped CTA. A click on the email input
352// inside a lead-capture popup would otherwise inflate the popup's
353// "Button clicks" count on the dashboard.
354var isCtaClick=(tag==="a"||tag==="button"||el.getAttribute("role")==="button"||(el.closest&&el.closest("a,button,[role=button]")));
355if(isCtaClick){var pa=popupAttribution(el);if(pa){payload.popupId=pa.popupId;payload.popupName=pa.popupName;}}
356// HP-3461 — two NON-IDENTIFYING facts about the clicked anchor: the host it leaves
357// for (external only) and the file extension. The selector above records only "#id"
358// or "tag.class", so a PDF link and any other button were indistinguishable and no
359// past click can be reclassified. The full href is NOT stored: pathnames carry
360// customer identity and this is the highest-volume table (see absUrl).
361// SYNC with app/src/lib/analytics-tracking-script.ts.
362var hrefEl=tag==="a"?el:(el.closest&&el.closest("a[href]"));
363var clickUrl=hrefEl?absUrl(hrefEl):null;
364var clickFacts=clickUrl?linkFacts(clickUrl):null;
365if(clickFacts&&clickFacts.external)payload.linkHost=clickFacts.host;
366if(clickFacts&&clickFacts.ext)payload.linkExt=clickFacts.ext;
367var clickKey=clickUrl?linkClickKey(clickUrl):null;
368if(clickKey)payload.linkKey=clickKey;
369pushEvent("element_click",payload);
370// Classify into a micro-conversion. Emitted ALONGSIDE the element_click, never
371// instead of it, so a classification we later decide was wrong can be re-derived.
372if(hrefEl)classifyLinkClick(hrefEl,clickUrl);
373// HP-2811 — same-tab anchor navigation races the page unload before the 5s
374// flush timer; visibilitychange/pagehide flushes are unreliable (esp. iOS
375// Safari, same-tab nav), so element_click goals for outbound CTAs (e.g. a
376// HubSpot Meetings link) see 0 conversions despite real clicks. Mirror the
377// form_submit flush pattern for anchors that will navigate this tab.
378var navAnchor=tag==="a"?el:(el.closest&&el.closest("a[href]"));
379if(navAnchor&&navAnchor.getAttribute("target")!=="_blank"&&!e.ctrlKey&&!e.metaKey&&!e.shiftKey){
380var navHref=(navAnchor.getAttribute("href")||"").replace(/^\s+/,"");
381if(navHref&&navHref.charAt(0)!=="#"&&navHref.slice(0,11).toLowerCase()!=="javascript:")flush();
382}
383},true);
384
385document.addEventListener("submit",function(e){
386var f=e.target;
387if(!f||f.tagName!=="FORM")return;
388var payload={formIdentifier:f.id||f.getAttribute("name")||"form-"+([].indexOf.call(document.forms,f))};
389var pa=popupAttribution(f);
390if(pa){payload.popupId=pa.popupId;payload.popupName=pa.popupName;}
391pushEvent("form_submit",payload);
392flush();// send now — a thank-you-page redirect may navigate away before the 5s timer, dropping the experiment conversion
393},true);
394
395// ─── HP-3461 on-page micro-conversions ──────────────────────────────────────
396// Kinds + classification rules mirror app/src/lib/analytics/page-engagement-kinds.ts
397// (the server-side reference). Literal here because this compiles to an inline
398// string and cannot import; page-engagement-kinds.test.ts asserts both script
399// copies only ever emit registered kinds.
400// SYNC with app/src/lib/analytics-tracking-script.ts.
401
402// Resolve an anchor for RULE MATCHING only — the url itself is never sent. Only
403// host + extension are reported (linkFacts): customer delivery links routinely put
404// identity in the PATH (/thank-you/jane-doe, /download/lead/8842/Invoice-Jane-Doe.pdf),
405// and AnalyticsEvent is the highest-volume table with no redaction pass and no
406// consent gate, so a stored pathname is PII a DSAR erase cannot reach.
407function absUrl(a){
408try{
409var raw=(a.getAttribute("href")||"").replace(/^\s+/,"");
410if(!raw||raw.charAt(0)==="#"||raw.slice(0,11).toLowerCase()==="javascript:")return null;
411var u=new URL(raw,location.href);
412if(u.protocol!=="http:"&&u.protocol!=="https:")return null;
413return u;
414}catch(e){return null;}
415}
416// Host + extension, derived from the WHOLE url (never a truncated copy) so a long
417// signed URL cannot lose its extension to a length cap and read as plain navigation.
418function linkFacts(u){
419var p=u.pathname.toLowerCase();
420var dot=p.lastIndexOf(".");
421var ext=dot<0?"":p.slice(dot+1);
422if(ext.length>8)ext="";
423return{host:u.hostname.toLowerCase(),ext:ext,external:u.hostname!==location.hostname};
424}
425// Stable join key for WHICH link was clicked: FNV-1a hash of the canonicalized
426// URL (lowercase host, no fragment, no trailing slash, query kept minus our own
427// runtime decoration params). The raw href is still never stored (HP-3461) —
428// the dashboard re-derives each key from the page's OWN html and joins on the
429// hash (app/src/lib/analytics/link-click-key.ts, the reference implementation;
430// its test extracts this copy and asserts they agree).
431// SYNC with app/src/lib/analytics-tracking-script.ts and lib/analytics/link-click-key.ts.
432function linkClickKey(u){
433try{
434var c=new URL(u.toString());
435c.hash="";
436c.hostname=c.hostname.toLowerCase();
437if(c.pathname.length>1&&c.pathname.slice(-1)==="/")c.pathname=c.pathname.slice(0,-1);
438if(c.search){
439var strip=["lp_click_id","lp_page_id","lp_ts","_exp","_var","_vid","_pvar","client_reference_id"];
440var kept=new URLSearchParams();
441c.searchParams.forEach(function(v,k){if(strip.indexOf(k)<0)kept.append(k,v);});
442var q=kept.toString();
443c.search=q?"?"+q:"";
444}
445var t=c.toString();
446var h=0x811c9dc5;
447for(var i=0;i<t.length;i++){h^=t.charCodeAt(i);h=Math.imul(h,0x01000193);}
448var x=(h>>>0).toString(16);
449while(x.length<8)x="0"+x;
450return x;
451}catch(e){return null;}
452}
453
454var DL_EXT=["pdf","epub","zip","csv","xlsx","xls","docx","doc","pptx","ppt","mp3","wav"];
455// calendar.google.com is deliberately ABSENT: the standard add-to-calendar button
456// is not a booking, and host-only matching cannot tell them apart, so including it
457// reported bookings on pages where nobody booked. Host entries only — this copy
458// matches on hostname, so a host/path entry could never match here.
459var BOOK_HOSTS=["calendly.com","cal.com","acuityscheduling.com"];
460
461// Every kind counts at most once PER PAGE LOAD: three clicks on the same PDF is one
462// download, and a scrubbed video cannot re-bank a quartile.
463//
464// IN-MEMORY, and it must stay that way. This used to consult sessionStorage and
465// return true from its catch block, so wherever storage throws (sandboxed iframe,
466// "block all cookies", in-app webviews, full quota) every timeupdate re-emitted
467// three quartiles — 12+/sec, which trips the collect route's 30-req/min-per-IP
468// limit and then 429s that visitor's REAL page_view / form_submit / conversion
469// beacons. A session-scoped key also outlived the page load, so a batch lost to a
470// 429 or a closed tab could never be re-reported. The server counts DISTINCT
471// SESSIONS per kind, so duplicates across page views are deduped there anyway.
472var microFired={};
473function latch(kind,id){
474var k=kind+":"+(id||"");
475if(microFired[k])return false;
476microFired[k]=true;
477return true;
478}
479function microEvent(kind,id){
480if(!latch(kind,id))return;
481pushEvent("page_engagement",id?{kind:kind,id:id}:{kind:kind});
482}
483
484function classifyLinkClick(a,u){
485try{
486if(!u)return;
487var f=linkFacts(u);
488if((a.hasAttribute&&a.hasAttribute("download"))||(f.ext&&DL_EXT.indexOf(f.ext)>-1)){
489// id is the EXTENSION, not the file name: "Invoice-Jane-Doe.pdf" is as
490// identifying as the path we refuse to store, and phase 1 renders only an
491// aggregate count.
492microEvent("file_download",f.ext||undefined);
493return;
494}
495if(!f.external)return;
496for(var i=0;i<BOOK_HOSTS.length;i++){
497var b=BOOK_HOSTS[i];
498if(f.host===b||f.host.slice(-(b.length+1))==="."+b){microEvent("booking_open",b);return;}
499}
500}catch(e){}
501}
502
503// <video> only. YouTube/Vimeo/Loom/Wistia are cross-origin iframes whose
504// playback is invisible without the provider's JS API plus an embed-URL rewrite,
505// both deferred out of phase 1 — a page whose only video is an embed reports NO
506// video kinds, which the read side must treat as "not measurable", not as 0%.
507function videoId(v){
508try{
509if(v.id)return v.id.slice(0,128);
510var src=v.currentSrc||v.getAttribute("src")||"";
511if(!src)return undefined;
512var seg=src.split("?")[0].split("/");
513return (seg[seg.length-1]||"").slice(0,128)||undefined;
514}catch(e){return undefined;}
515}
516// Autoplay policy only lets a video start by itself while MUTED, so "muted and the
517// visitor has never touched the page" is the signature of decorative playback.
518// A <video autoplay muted loop playsinline> hero fires 'play' on every load, so
519// counting it would report "100% played the video" on exactly the pages most likely
520// to have a hero video. Unmuted playback always counts: it needs a gesture.
521// mousedown as well as pointerdown: a path delivering only mouse events would
522// otherwise never set this, silently disabling the feature with nothing in the logs.
523var userGestured=false;
524function markGesture(){userGestured=true;}
525document.addEventListener("pointerdown",markGesture,true);
526document.addEventListener("mousedown",markGesture,true);
527document.addEventListener("keydown",markGesture,true);
528document.addEventListener("touchstart",markGesture,{passive:true,capture:true});
529// Decide ONCE at 'play' and LATCH on the element. Re-checking the live flag on every
530// timeupdate was a real bug found in Chrome: a decorative muted autoplay correctly
531// suppressed video_start, then one unrelated trusted keystroke flipped userGestured
532// and every later timeupdate passed — banking p25/p50/p75 for a video nobody chose
533// to watch, with NO video_start, leaving the read side dividing by a zero
534// denominator. Unmuted playback always counts: it cannot start without a gesture.
535function decidePlay(v){
536var intent=(!v.muted)||userGestured;
537try{v.__hpIntent=intent?1:0;}catch(e){}
538return intent;
539}
540// Undefined = never saw this element's 'play' (script injected mid-playback): fail
541// closed, since crediting is the direction that corrupts the headline percentage.
542function countedPlay(v){return v.__hpIntent===1;}
543// Unmuting a decorative video IS intent, and no 'play' fires for it, so re-decide.
544// Emitting video_start keeps the stream coherent: a start precedes its quartiles.
545function onVideoVolume(e){
546var v=e.target;
547if(!v||v.tagName!=="VIDEO")return;
548if(v.muted||v.paused||v.__hpIntent===1)return;
549v.__hpIntent=1;
550microEvent("video_start",videoId(v));
551}
552function onVideoTime(e){
553var v=e.target;
554if(!v||v.tagName!=="VIDEO")return;
555// Consult the LATCHED decision, never the live flag — see decidePlay.
556if(!countedPlay(v))return;
557var d=v.duration;
558// Live streams / metadata not yet loaded report 0, NaN or Infinity — a quartile
559// of an unknown duration is meaningless, so report the play and nothing more.
560if(!isFinite(d)||d<=0)return;
561var pct=(v.currentTime/d)*100;
562var id=videoId(v);
563if(pct>=25)microEvent("video_p25",id);
564if(pct>=50)microEvent("video_p50",id);
565if(pct>=75)microEvent("video_p75",id);
566// p100 comes from 'ended', not a threshold: the last timeupdate can land a
567// fraction short of duration, so a full watch would never bank it.
568}
569document.addEventListener("play",function(e){var v=e.target;if(v&&v.tagName==="VIDEO"&&decidePlay(v))microEvent("video_start",videoId(v));},true);
570document.addEventListener("timeupdate",onVideoTime,true);
571document.addEventListener("volumechange",onVideoVolume,true);
572// SYNC-START embed-video-listeners (app/edge twins pinned by page-engagement-kinds.test.ts)
573// ─── Embedded video: Vimeo (HP-3461) ────────────────────────────────────────
574// <video> is handled above by reading the element directly. A Vimeo embed is a
575// CROSS-ORIGIN iframe, so its state is unreadable — but the player reports its own
576// progress over postMessage, and needs NO change to the embed URL. A plainly pasted
577// Vimeo embed becomes measurable as-is. (YouTube is different: it is silent unless
578// enablejsapi=1 is on the URL. Handled separately.)
579//
580// Verified against live players, and two details cost an attempt each:
581//   1. SUBSCRIBE ON THE PLAYER'S ready MESSAGE, not the iframe's load event.
582//      Subscribing on load races the player's own init: the addEventListener calls
583//      are silently dropped, no ACK comes back, and no timeupdate ever arrives — it
584//      looks exactly like "Vimeo does not work".
585//   2. ATTRIBUTE BY e.source, not player_id. Vimeo only echoes a player_id if the
586//      embed URL carries one, and a customer's pasted embed has none.
587//
588// Vimeo sends OBJECTS (YouTube sends JSON strings), and timeupdate carries
589// data.percent as a 0..1 fraction, so quartiles need no arithmetic of our own.
590function vimeoHostOk(origin){
591try{
592var h=new URL(origin).hostname.toLowerCase();
593return h==="player.vimeo.com"||h==="vimeo.com"||h.slice(-10)===".vimeo.com";
594}catch(e){return false;}
595}
596// Low-cardinality, non-identifying label: the numeric video id off the embed URL,
597// prefixed so it cannot collide with a <video> filename in the tally.
598function vimeoLabel(f){
599try{
600var m=(f.getAttribute("src")||"").match(/vimeo\.com\/video\/(\d+)/);
601return m?"vimeo:"+m[1]:"vimeo";
602}catch(e){return "vimeo";}
603}
604function vimeoFrameFor(src){
605try{
606var fr=document.querySelectorAll("iframe");
607for(var i=0;i<fr.length;i++){if(fr[i].contentWindow===src)return fr[i];}
608}catch(e){}
609return null;
610}
611function vimeoSubscribe(f){
612try{
613var w=f.contentWindow;
614var evs=["play","timeupdate","ended"];
615for(var i=0;i<evs.length;i++)w.postMessage({method:"addEventListener",value:evs[i]},"*");
616}catch(e){}
617}
618window.addEventListener("message",function(e){
619try{
620if(!vimeoHostOk(e.origin))return;
621var d=e.data;
622if(!d||typeof d!=="object")return;
623var f=vimeoFrameFor(e.source);
624if(!f)return;
625if(d.event==="ready"){vimeoSubscribe(f);return;}
626var id=vimeoLabel(f);
627if(d.event==="play"){
628// Same intent rule as <video>, minus the muted check we cannot read through an
629// iframe: browser policy only permits unprompted autoplay while MUTED, so
630// "playback began before the visitor touched anything" is the decorative case.
631if(!userGestured)return;
632f.__hpEmbedCounted=1;
633microEvent("video_start",id);
634return;
635}
636if(f.__hpEmbedCounted!==1)return;
637if(d.event==="timeupdate"){
638var pct=d.data&&typeof d.data.percent==="number"?d.data.percent*100:-1;
639if(pct<0)return;
640if(pct>=25)microEvent("video_p25",id);
641if(pct>=50)microEvent("video_p50",id);
642if(pct>=75)microEvent("video_p75",id);
643// p100 comes from 'ended', as with <video>: the last timeupdate lands short.
644}else if(d.event==="ended"){
645microEvent("video_p25",id);microEvent("video_p50",id);microEvent("video_p75",id);microEvent("video_p100",id);
646}
647}catch(err){}
648});
649
650// ─── Embedded video: YouTube (HP-3461) ──────────────────────────────────────
651// YouTube is SILENT unless the embed URL carries enablejsapi=1 — verified live: a
652// plain embed answers nothing, even if we send it the handshake. So this listener
653// only hears players whose URL has that flag; adding it to customer HTML at serve
654// time is a separate commit, deliberately, because that mutates what visitors load.
655//
656// With the flag, the protocol is: post a listening message, then the player streams
657// infoDelivery with playerState, currentTime and duration.
658//
659// The handshake repeats on a timer, and the honest reason is that it catches frames
660// that appear after load (a tab, an accordion, a script-mounted embed). Whether a
661// player also STOPS streaming when we stop asking was not established: repeats are
662// answered "alreadyInitialized", which suggests the first one is what arms it. The
663// polling stays because the version with polling is the one verified to work end to
664// end, and trading that for an unverified optimisation would risk silently losing
665// every embed metric. The steady-state cost is one postMessage per api-enabled
666// frame every 2s.
667//
668// Unlike Vimeo, YouTube sends JSON STRINGS, so the payload needs parsing before it
669// looks like anything.
670//
671// playerState is YouTube's enum: -1 unstarted, 0 ended, 1 playing, 2 paused,
672// 3 buffering, 5 cued.
673function ytHostOk(origin){
674try{
675var h=new URL(origin).hostname.toLowerCase();
676return h==="www.youtube.com"||h==="youtube.com"||h==="www.youtube-nocookie.com"||h==="youtube-nocookie.com";
677}catch(e){return false;}
678}
679function ytLabel(f){
680try{
681var m=(f.getAttribute("src")||"").match(/\/embed\/([A-Za-z0-9_-]{11})/);
682return m?"youtube:"+m[1]:"youtube";
683}catch(e){return "youtube";}
684}
685function ytFrameFor(src){
686try{
687var fr=document.querySelectorAll("iframe");
688for(var i=0;i<fr.length;
688i++){if(fr[i].contentWindow===src)return fr[i];}
689}catch(e){}
690return null;
691}
692// Ask every api-enabled YouTube frame to start reporting, and keep asking. Cheap: a
693// postMessage to a frame that is not a YouTube player is simply ignored.
694function ytListen(){
695try{
696var fr=document.querySelectorAll('iframe[src*="enablejsapi=1"]');
697for(var i=0;i<fr.length;i++){
698try{fr[i].contentWindow.postMessage('{"event":"listening","id":"hp","channel":"widget"}',"*");}catch(e){}
699}
700}catch(e){}
701}
702setTimeout(ytListen,1200);
703setInterval(ytListen,2000);
704window.addEventListener("message",function(e){
705try{
706if(!ytHostOk(e.origin))return;
707var d=e.data;
708// YouTube sends strings; anything else is not its API talking.
709if(typeof d!=="string")return;
710try{d=JSON.parse(d);}catch(x){return;}
711if(!d||typeof d!=="object"||d.event!=="infoDelivery")return;
712var info=d.info;
713if(!info||typeof info!=="object")return;
714var f=ytFrameFor(e.source);
715if(!f)return;
716var id=ytLabel(f);
717var state=info.playerState;
718// Same intent rule as Vimeo and <video>, but YouTube hands us no discrete 'play'
719// event: infoDelivery repeats playerState 1 for the WHOLE of playback. So the
720// decision is made on the TRANSITION into playing and latched there, never
721// re-read. Re-reading it is precisely the bug decidePlay documents, and it is
722// reachable here in a way it is not for <video>: a muted autoplay is correctly
723// suppressed, then one unrelated trusted click (the form, alt-tab, a screenshot)
724// flips userGestured and the next state-1 update promotes a video nobody chose
725// into a counted play, quartiles and all. Latching on the transition also keeps
726// the <video> semantics that a chosen REPLAY after a decorative autoplay counts,
727// because that arrives as 2 then 1.
728var prev=f.__hpYtPrev;
729f.__hpYtPrev=state;
730if(state===1&&prev!==1){
731f.__hpEmbedCounted=userGestured?1:0;
732if(f.__hpEmbedCounted===1)microEvent("video_start",id);
733}
734if(f.__hpEmbedCounted!==1)return;
735if(state===0){
736// Ended implies every quartile, even if a throttled tab skipped an update.
737microEvent("video_p25",id);microEvent("video_p50",id);microEvent("video_p75",id);microEvent("video_p100",id);
738return;
739}
740var dur=info.duration,cur=info.currentTime;
741// A live stream reports duration 0, and a quartile of an unknown length is
742// meaningless — report the play and nothing more.
743if(typeof dur!=="number"||typeof cur!=="number"||!isFinite(dur)||dur<=0)return;
744var pct=(cur/dur)*100;
745if(pct>=25)microEvent("video_p25",id);
746if(pct>=50)microEvent("video_p50",id);
747if(pct>=75)microEvent("video_p75",id);
748// p100 comes from playerState 0, not a threshold: the final update lands short.
749}catch(err){}
750});
751// SYNC-END embed-video-listeners
752document.addEventListener("ended",function(e){
753var v=e.target;
754if(!v||v.tagName!=="VIDEO")return;
755if(!countedPlay(v))return;
756var id=videoId(v);
757// Watched to the end implies every quartile, even if a throttled background tab
758// skipped a timeupdate.
759microEvent("video_p25",id);microEvent("video_p50",id);microEvent("video_p75",id);microEvent("video_p100",id);
760},true);
761
762// ─── Scheduler widgets (HP-3461 phase 3) ────────────────────────────────────
763// An inline Calendly/Cal.com embed is a CROSS-ORIGIN iframe, so we cannot see a
764// click inside it — which is why booking_open (a click on an outbound scheduling
765// link) is absent on exactly the pages most likely to take bookings. The widget
766// broadcasts its own progress to the parent window, so we get told instead of
767// guessing. No SDK needed; Calendly posts these regardless.
768//
769// SECURITY: any frame can post a message claiming to be a booking, and this ships
770// on every published page, so the sender's ORIGIN must be a recognised scheduling
771// host AND the message must match the provider's shape. Everything else is ignored.
772// PRIVACY: only WHICH STEP is read. event.data.payload (invitee details on
773// event_scheduled) is never touched.
774// SYNC with app/src/lib/analytics-tracking-script.ts.
775function schedulerOrigin(origin){
776try{
777var h=new URL(origin).hostname.toLowerCase();
778for(var i=0;i<BOOK_HOSTS.length;i++){
779var b=BOOK_HOSTS[i];
780if(h===b||h.slice(-(b.length+1))==="."+b)return b;
781}
782}catch(e){}
783return null;
784}
785// calendly.profile_page_viewed is deliberately ignored: it fires for a profile
786// listing rather than a bookable calendar, so it would inflate the funnel's top.
787window.addEventListener("message",function(e){
788try{
789var host=schedulerOrigin(e.origin);
790if(!host)return;
791var d=e.data;
792if(!d||typeof d!=="object")return;
793// Calendly's vocabulary reads off d.event and is prefixed; Cal.com's reads off
794// d.type and is not. Verified against live widgets on both, plus Cal.com's own
795// EventDataMap in packages/embeds/embed-core/src/sdk-action-manager.ts.
796// calendly.profile_page_viewed is ignored: it fires for a profile listing rather
797// than a bookable calendar, so it would inflate the top of the funnel.
798//
799// Cal.com HAS NO time-selection event. Its full enum (27 keys) contains nothing for
800// picking a date or slot -- availabilityLoaded and slotsFetched are the widget
801// LOADING options, not a visitor choosing one. So Cal.com reports 2 of the 3 steps,
802// and booking_time_selected is left ABSENT for it rather than synthesised from the
803// booking. Fabricating it would make every Cal.com page show a flat 100% from
804// "selected" to "completed" -- a number that looks measured and is not. Absent
805// already means "not measurable here" everywhere else in this feature (see the
806// video quartiles), and the read side honours that.
807//
808// Emissions are LITERALS, one branch per step, so the sync seam in
809// page-engagement-kinds.test.ts can see them -- it caught booking_completed going
810// unverifiable when this was table-driven.
811var name=typeof d.event==="string"?d.event:(typeof d.type==="string"?d.type:null);
812if(!name)return;
813if(host==="calendly.com"){
814if(name==="calendly.event_scheduled"){
815microEvent("booking_viewed",host);
816microEvent("booking_time_selected",host);
817microEvent("booking_completed",host);
818// The booking is the outcome this exists to catch and the visitor usually leaves
819// for the confirmation, so flush rather than wait out the 5s timer.
820flush();
821}else if(name==="calendly.date_and_time_selected"){
822microEvent("booking_viewed",host);
823microEvent("booking_time_selected",host);
824}else if(name==="calendly.event_type_viewed"){
825microEvent("booking_viewed",host);
826}
827}else if(host==="cal.com"){
828// bookingSuccessfulV2 deprecates bookingSuccessful and both are still in the enum,
829// so accept either -- customers sit on different embed versions.
830if(name==="bookingSuccessfulV2"||name==="bookingSuccessful"){
831// NOTE: no booking_time_selected here. Cal.com cannot report it.
832microEvent("booking_viewed",host);
833microEvent("booking_completed",host);
834flush();
835}else if(name==="bookerViewed"){
836microEvent("booking_viewed",host);
837}
838}
839// No acuityscheduling.com branch, and there never can be one. Its embed script
840// exchanges only sizing/load/scrollTo/scrollBy/gaMeasurementId with the parent and
841// takes a Google Analytics client id INWARD — there is no booking message to hear.
842// Acuity reports outward only through its webhooks (HP-4041's inbound conversion
843// webhook is the route). A branch here would be dead code. See lib/scheduling-hosts.
844// Its origin stays accepted by the gate above, which costs nothing: an origin that
845// never speaks simply never matches.
846}catch(err){}
847});
848
849// Consent usually arrives seconds after load, so a bare load listener would
850// never fire and rum_timing would vanish from every CMP-gated page. The
851// navigation/paint entries report the same absolute numbers whenever read.
852function hpRumTiming(){
853setTimeout(function(){
854try{
855var perf=performance.getEntriesByType("navigation")[0];
856if(perf){
857var ev=makeEvent("rum_timing",{loadTimeMs:Math.round(perf.loadEventEnd-perf.startTime),ttfbMs:Math.round(perf.responseStart-perf.startTime),fcpMs:undefined});
858var paintEntries=performance.getEntriesByType("paint");
859for(var i=0;i<paintEntries.length;i++){if(paintEntries[i].name==="first-contentful-paint"){ev.payload.fcpMs=Math.round(paintEntries[i].startTime);break;}}
860buffer.push(ev);
861}
862}catch(e){}
863},2000);
864}
865if(document.readyState==="complete")hpRumTiming();else window.addEventListener("load",hpRumTiming);
866});
867})();
868</script>
868<script data-page-id="v8s3dfzxmk03xqctda3738k8" data-page-slug="datenschutz" data-api-url="https://htmlpub.com" data-turnstile-key="0x4AAAAAAD7Sp6X7R_qRx-Cz" data-htmlpub-forms>
869(function(){
870var s=document.querySelector("script[data-htmlpub-forms]");
871var pageId=s.getAttribute("data-page-id");
872var slug=s.getAttribute("data-page-slug");
873var api=s.getAttribute("data-api-url")+"/api/forms/submit";
874// Owner-mode: editor previews and the in-app shell load this script with
875// data-owner="1". We still listen for submits (the editor surfaces success
876// state to the user via the toolbar), but we MUST NOT fire conversion
877// pixels — that would pollute the customer's Meta / TikTok pixel data
878// with editor test traffic indistinguishable from real visitors.
879var _hpIsOwner=s.getAttribute("data-owner")==="1";
880// Preview-mode: the editor's Preview loads owner-gated (draft access + no pixel
881// pollution) but must show the true visitor experience. _hpIsPreview lets visitor-UX
882// blocks (multi-step paging) run even though _hpIsOwner is set; pixel/submission gates
883// stay owner-only.
884var _hpIsPreview=s.getAttribute("data-preview")==="1";
885var _hpAutomaticConversionEventsEnabled=s.getAttribute("data-automatic-conversion-events")!=="0";
886var _hpTurnstileKey=s.getAttribute("data-turnstile-key")||"";
887// data-persist-hidden: fields hidden via the inspector — hidden from visitors on
888// the live page but still submitted (attribution/UTM). The edit canvas (owner, NOT
889// preview) skips the rule so the inspector keeps them visible with a "Hidden" chip;
890// Preview applies it so it matches the published page.
891if(!_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){}}
892// Multiple-choice radio/checkbox width normalization is NOT injected at runtime.
893// The editor bakes an inline width:auto onto each option input (idempotently), so the
894// fix persists in the saved HTML and follows the normal draft -> publish flow instead
895// of overriding every served form's checkboxes/radios on every page load.
896// A field hidden from visitors (data-persist-hidden -> display:none) that ALSO carries native
897// required cannot be focused, so on a single-step form native constraint validation would
898// silently abort submit on it (the multi-step path already skips hidden fields via
899// _hpValidatable). Drop the required attribute from hidden controls on the served page so
900// submission is never blocked by a field the visitor can't see; the field still submits its value.
901if(!_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){}}
902// The form's real submit button — never one of the authored multi-step nav buttons
903// (data-persist-ms-back/next), which may be type-less if hand-authored.
904function _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])");}
905// A checkbox group marked required-as-a-group (data-persist-group-required) is satisfied when
906// AT LEAST ONE member is checked — native required on each box would wrongly demand ALL.
907// Returns true if every such group within the given scope is satisfied; otherwise reports validity on
908// the first empty group (clearing on the next change) and returns false.
909function _hpGroupReqOK(scope){
910var boxes=scope.querySelectorAll('input[type="checkbox"][data-persist-group-required]');
911var groups={};
912for(var i=0;i<boxes.length;i++){var n=boxes[i].name||("__hp"+i);if(!groups[n])groups[n]=[];groups[n].push(boxes[i]);}
913for(var nm in groups){if(!Object.prototype.hasOwnProperty.call(groups,nm))continue;
914var g=groups[nm],any=false,first=null;
915for(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;}
916if(first&&!any){try{first.setCustomValidity("Please select at least one option.");
916if(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;}}
917return true;
918}
919// Pick a Turnstile theme (light/dark) from the first opaque background behind the form, so the
920// widget matches the PAGE rather than defaulting to the visitor's system theme ("auto"). Parses
921// the rgb() string without a regex escape (template-literal safe). Falls back to "auto".
922function _hpPickTheme(el){
923try{
924var n=el;
925while(n&&n.nodeType===1){
926var bg=window.getComputedStyle(n).backgroundColor||"";
927if(bg.indexOf("rgb")===0){
928var p=bg.replace(/[^0-9.,]/g,"").split(",");
929var a=p.length>3?parseFloat(p[3]):1;
930if(a>0.1){var lum=(0.299*+p[0]+0.587*+p[1]+0.114*+p[2])/255;return lum>0.5?"light":"dark";}
931}
932n=n.parentElement;
933}
934}catch(e){}
935return "auto";
936}
937// Inject the Turnstile widget on a spam-protected form via the platform-origin bridge iframe.
938// The widget CANNOT render inline on a customer's custom domain: Turnstile binds a widget to a
939// hostname allowlist (Enterprise caps at 200, no wildcards), so customer domains can't be listed.
940// Instead we embed /turnstile-bridge served from data-api-url (a platform origin that IS on the
941// allowlist); it renders the widget there and postMessages the token back (see the listener below).
942// Placed ABOVE the submit's outermost row so on a multi-step footer nav it sits on its own line.
943// Starts 0px (interaction-only is invisible); the bridge posts a resize only if a challenge appears.
944// Idempotent; no-op without the feature flag or inside a sandboxed iframe (the /p shell renders its
945// own widget on the app origin).
946function _hpInjectCaptcha(f){
947if(!_hpTurnstileKey||inSandbox||!f||f.querySelector("iframe[data-hp-turnstile]"))return;
948var base=(s.getAttribute("data-api-url")||"");
949if(base&&base.charAt(base.length-1)==="/")base=base.slice(0,-1);
950if(!base)return;
951var fr=document.createElement("iframe");
952fr.setAttribute("data-hp-turnstile","");
953fr.setAttribute("title","Spam protection");
954fr.setAttribute("scrolling","no");
955fr.style.cssText="width:100%;max-width:320px;height:0;border:0;display:block;overflow:hidden;";
956fr.src=base+"/turnstile-bridge?theme="+encodeURIComponent(_hpPickTheme(f));
957var sb=_hpFindSubmit(f);
958if(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);
959}
960// Receive the Turnstile token from the platform-origin bridge iframe. Origin-checked against
961// data-api-url. Stores the token on the owning form for the submit path; resizes the iframe when
962// Managed mode shows an actual challenge (invisible/auto-pass stays 0px). One listener serves
963// every captcha form on the page.
964(function(){
965var _hpBO="";try{_hpBO=new URL(s.getAttribute("data-api-url")||"",location.href).origin;}catch(e){}
966window.addEventListener("message",function(e){
967if(!_hpBO||e.origin!==_hpBO)return;
968var d=e.data;if(!d||d.source!=="hp-turnstile")return;
969var frs=document.querySelectorAll("iframe[data-hp-turnstile]"),host=null;
970for(var i=0;i<frs.length;i++){if(frs[i].contentWindow===e.source){host=frs[i];break;}}
971if(!host)return;
972var form=host.closest?host.closest("form"):null;
973if(d.type==="token"){if(form){form.__hpCaptchaToken=(d.token||"");form.__hpCaptchaError=false;}}
974else if(d.type==="expired"){if(form)form.__hpCaptchaToken="";}
975// A terminal bridge error (misconfigured/unreachable widget, keys mismatch) means no token can
976// ever be produced; flag it so the submit gate fails OPEN rather than trapping the visitor forever
977// (the server still fails open for the same unconfigured case). Distinct from expired (retr
977yable).
978else if(d.type==="error"){if(form){form.__hpCaptchaToken="";form.__hpCaptchaError=true;}}
979else 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");}
980});
981})();
982// Bounded wait for a Turnstile token, with a single re-challenge (decided fail policy #1/#2).
983// The bridge delivers the token via postMessage, which can land slightly AFTER the visitor
984// clicks submit (invisible challenge still resolving). Give it a bounded window; if none
985// arrives, force ONE fresh challenge (reload the bridge iframe) and wait a final window; if
986// STILL none, proceed anyway — the server accepts the lead and flags it unverified rather
987// than dropping it. done() runs exactly once: as soon as a token appears, the bridge reports
988// a terminal error, or the budget is spent.
989function _hpWaitForCaptcha(f,done){
990var WAIT=3000,STEP=100,called=false;
991function finish(){if(called)return;called=true;done();}
992function poll(deadline,onExpire){
993if((f.__hpCaptchaToken||"")!==""||f.__hpCaptchaError){finish();return;}
994if(Date.now()>=deadline){onExpire();return;}
995setTimeout(function(){poll(deadline,onExpire);},STEP);
996}
997poll(Date.now()+WAIT,function(){
998if((f.__hpCaptchaToken||"")!==""||f.__hpCaptchaError){finish();return;}
999try{var fr=f.querySelector("iframe[data-hp-turnstile]");if(fr)fr.src=fr.src;}catch(e){}
1000poll(Date.now()+WAIT,finish);
1001});
1002}
1003// reCAPTCHA enforcement (HP-1475): only pages whose owner opted in emit this
1004// flag. It gates the client-side submit block below so pages that merely carry
1005// a decorative widget (the pre-HP-1475 status quo) keep working unchanged.
1006var _hpRecaptchaEnforced=s.getAttribute("data-recaptcha-enforced")==="1";
1007var corrId=0;
1008var pendingCallbacks={};
1009
1010// Detect if we're in a sandboxed iframe
1011var inSandbox=false;
1012try{inSandbox=window!==window.parent;}catch(e){inSandbox=true;}
1013
1014function cleanKey(s){return s.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_|_$/g,"").substring(0,30);}
1015// SYNC: mirror worker-pages/src/scripts/forms.ts and app/src/lib/extract-form-fields.ts
1016function 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);}
1017function fieldKey(el,idx){
1018if(el.name&&!looksLikeRawId(el.name))return el.name;
1019if(el.id&&!looksLikeRawId(el.id))return el.id;
1020var df=el.getAttribute("data-field")||el.getAttribute("data-name");
1021if(df&&!looksLikeRawId(df))return df;
1022// Type-based semantic hint. <input type="email"> without an explicit name
1023// should still be identified as "email" so the lead-capture extractor can
1024// find the address. Same for tel/url. Critical when the page builder
1025// (HTMLPub editor / Sacca's offers.leadpages.com pages) emits inputs
1026// without name attributes — without this, the email field gets keyed off
1027// its placeholder ("alex_at_youragency_com") and no lead is extracted.
1028if(el.type==="email")return "email";
1029if(el.type==="tel")return "phone";
1030if(el.type==="url")return "url";
1031// Sibling label pattern: <label>Foo</label><input ...> inside a wrapper.
1032// Common in modern form layouts. The existing closest(label) check below
1033// only catches WRAPPING labels, not sibling ones.
1034var prev=el.previousElementSibling;
1035if(prev&&prev.tagName==="LABEL"&&prev.textContent){var pk=cleanKey(prev.textContent);if(pk)return pk;}
1036if(el.placeholder){var ck=cleanKey(el.placeholder);if(ck)return ck;}
1037var al=el.getAttribute("aria-label");
1038if(al){var ck2=cleanKey(al);if(ck2)return ck2;}
1039if(el.id){var lbl=document.querySelector('label[for="'+el.id+'"]');if(lbl&&lbl.textContent){var ck3=cleanKey(lbl.textContent);if(ck3)return ck3;}}
1040var par=el.closest?el.closest("label"):null;
1041if(par&&par.textContent){var ck4=cleanKey(par.textContent);if(ck4)return ck4;}
1042return (el.type||"field")+"_"+idx;
1043}
1044// Known third-party tracking field patterns to ignore
1045var _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;
1046function _isInjected(el){
1047// Skip hidden inputs not present in the original page HTML.
1048// Third-party scripts (Customers.ai, HubSpot, etc.) inject hidden fields
1049// at runtime. We detect these by checking if they were present at DOMContentLoaded.
1050if(el.type==="hidden"&&!el.hasAttribute("data-hp-original"))return true;
1051return false;
1052}
1053function collect(f){
1054var d={};
1055var els=f.elements||f.querySelectorAll("input,textarea,select");
1056var used={};
1057var hasVisibleValue=false;
1058for(var i=0;i<els.length;i++){
1059var el=els[i];
1060if(el.type==="file"||el.type==="submit"||el.type==="button")continue;
1061if((el.type==="checkbox"||el.type==="radio")&&!el.checked)continue;
1062if(_isInjected(el))continue;
1063// International-phone controls: the visible national input (data-hp-phone-nat) and
1064// the injected country <select> (data-hp-phone-cc) are NOT collected — the hidden
1065// E.164 companion carries the value under the field's name. The national input still
1066// counts toward hasVisibleValue so a phone-only form isn't treated as empty.
1067if(el.hasAttribute&&el.hasAttribute("data-hp-phone-nat")){if(el.value&&el.value.trim())hasVisibleValue=true;continue;}
1068if(el.hasAttribute&&el.hasAttribute("data-hp-phone-cc"))continue;
1069// Empty URL-param inputs are dropped from the payload so direct-traffic
1070// submissions from a form with data-persist-capture-utms (or hand-authored
1071// data-persist-url-param inputs) don't ship 7 blank attribution columns
1072// into every submission, integration webhook, CSV export, and CAPI event.
1073// Author-provided value="direct" (or any non-empty default) still ships.
1074if(el.type==="hidden"&&!(el.value||"").trim()&&el.hasAttribute("data-persist-url-param"))continue;
1075var key=fieldKey(el,i);
1076if(_blocklist.test(key))continue;
1077if(used[key]){key=key+"_"+i;}
1078used[key]=true;
1079d[key]=el.value;
1080if(el.type!=="hidden"&&el.value&&el.value.trim())hasVisibleValue=true;
1081}
1082// Require at least one visible field with a value — pure hidden-field submissions are noise
1083if(!hasVisibleValue)return {};
1084return d;
1085}
1086
1087// Generate a UUID v4 (browser-side). Used as the eventID that ties the
1088// fbq/ttq browser event to the matching CAPI event the server sends via
1089// the integration queue → provider workflow, so pixel platforms can dedup.
1090function _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);});}
1091
1092// Fire the canonical "Lead" event on every installed pixel platform. Only
1093// platforms that booted (window.fbq, window.ttq, window.gtag, window.dataLayer)
1094// receive a call — others no-op cleanly. The eventID is threaded into the
1095// provider-specific dedup parameter (Meta: eventID, TikTok: event_id).
1096function _hpFireLead(formIdent,data,eventId){
1097  if(_hpIsOwner||!_hpAutomaticConversionEventsEnabled)return;
1098  var em=null,ph=null,fn=null,ln=null;
1099  for(var k in data){
1100    if(!Object.prototype.hasOwnProperty.call(data,k))continue;
1101    var v=(data[k]||"").toString().trim();
1102    if(!v)continue;
1103    var kl=k.toLowerCase();
1104    if(!em&&/email/.test(kl)&&/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v))em=v;
1105    else if(!ph&&/phone|tel|mobile/.test(kl))ph=v;
1106    else if(!fn&&/first.?name|fname/.test(kl))fn=v;
1107    else if(!ln&&/last.?name|lname/.test(kl))ln=v;
1108  }
1109  var custom={};if(em)custom.em=em;if(ph)custom.ph=ph;if(fn)custom.fn=fn;if(ln)custom.ln=ln;
1110  try{if(window.fbq)window.fbq("track","Lead",custom,{eventID:eventId});}catch(e){}
1111  try{if(window.ttq&&window.ttq.track)window.ttq.track("SubmitForm",custom,{event_id:eventId});}catch(e){}
1112  try{if(window.gtag)window.gtag("event","generate_lead",{event_id:eventId,lp_form_id:formIdent});}catch(e){}
1113  try{if(window.dataLayer&&window.dataLayer.push)window.dataLayer.push({event:"lp_form_submit",lp_event_id:eventId,lp_form_id:formIdent});}catch(e){}
1114}
1115
1116// First-interaction hook: when the visitor focuses any form field for the
1117// first time on this page, fire the platform's "started a form" event.
1118// Once per form per page (debounced via _hpFiredInitFocus).
1119var _hpFiredInitFocus={};
1120function _hpFireInitiateCheckout(formIdent){
1121  if(_hpIsOwner||!_hpAutomaticConversionEventsEnabled)return;
1122  if(_hpFiredInitFocus[formIdent])return;
1123  _hpFiredInitFocus[formIdent]=true;
1124  var eid=_hpUuid();
1125  try{if(window.fbq)window.fbq("track","InitiateCheckout",{}
1125,{eventID:eid});}catch(e){}
1126  try{if(window.ttq&&window.ttq.track)window.ttq.track("InitiateCheckout",{},{event_id:eid});}catch(e){}
1127  try{if(window.gtag)window.gtag("event","form_start",{event_id:eid,lp_form_id:formIdent});}catch(e){}
1128  try{if(window.dataLayer&&window.dataLayer.push)window.dataLayer.push({event:"lp_form_start",lp_event_id:eid,lp_form_id:formIdent});}catch(e){}
1129}
1130// HP-3461 — the same first-interaction moment, reported to OUR analytics. It has
1131// been detected here all along and told to Facebook, TikTok, Google and GTM
1132// (above) while our own funnel never heard about it, so "169 started the form, 17
1133// finished" was unanswerable from first-party data.
1134//
1135// Deliberately NOT inside _hpFireInitiateCheckout: that is gated on
1136// _hpAutomaticConversionEventsEnabled, the customer's AD-PIXEL preference, and
1137// switching off automatic ad conversion events must not blind our own analytics.
1138//
1139// Requires a real visitor gesture. A focusin alone is not evidence of intent:
1140// <input autofocus>, a popup that focuses its email field on open, and browser
1141// autofill all fire it with nobody touching the keyboard, which would report a
1142// form_start for ~every session and make the abandonment number this feature
1143// exists to answer ("169 started, 17 finished") read as a 100% start rate on a
1144// page nobody typed into. _hpFireInitiateCheckout above has always had this bug;
1145// it only ever fed ad pixels, so nobody noticed.
1146//
1147// Owner previews self-gate: analyticsTrackingScript returns on data-owner="1"
1148// BEFORE registering window.__hpupPushEvent, so this finds no bridge and no-ops.
1149// (The edge serves only public traffic, so the same code is correct on both
1150// hosts. Known gap, pre-existing and not widened by form_start: the Nova page
1151// switcher omits ?token=, so isOwnerForAnalytics misses it and page_view /
1152// scroll_depth / element_click already report from that path.)
1153// SYNC with worker-pages/src/scripts/forms.ts — these files must stay byte-identical.
1154var _hpFormStartGesture=false;
1155function _hpMarkFormGesture(){_hpFormStartGesture=true;}
1156document.addEventListener("pointerdown",_hpMarkFormGesture,true);
1157document.addEventListener("keydown",_hpMarkFormGesture,true);
1158document.addEventListener("touchstart",_hpMarkFormGesture,{passive:true,capture:true});
1159// Object.create(null), not {}: the key is formId(), i.e. the author-controlled
1160// id/name attribute. With a bare object a form named "constructor" or "toString"
1161// reads as already-fired on its very first focus and never reports at all, and
1162// "__proto__" cannot even be latched — the page would show submissions with no
1163// starts whatsoever.
1164var _hpFiredFormStart=Object.create(null);
1165function _hpReportFormStart(formIdent){
1166  if(!_hpFormStartGesture)return;
1167  var k=String(formIdent||"");
1168  if(_hpFiredFormStart[k])return;
1169  _hpFiredFormStart[k]=true;
1170  try{if(window.__hpupPushEvent)window.__hpupPushEvent("page_engagement",{kind:"form_start",id:k.slice(0,128)});}catch(e){}
1171}
1172document.addEventListener("focusin",function(e){
1173  var el=e.target;
1174  if(!el||(el.tagName!=="INPUT"&&el.tagName!=="TEXTAREA"&&el.tagName!=="SELECT"))return;
1175  if(el.type==="hidden"||el.type==="submit"||el.type==="button")return;
1176  var f=el.closest&&(el.closest("form")||el.closest("[data-htmlpub-fake-form]"));
1177  if(!f)return;
1178  _hpReportFormStart(formId(f));
1179  _hpFireInitiateCheckout(formId(f));
1180},true);
1181
1182// Set _hp_email cookie when we find an email in form data (for CRM personalization)
1183function setEmailCookie(data){
1184var email=null;
1185for(var k in data){
1186if(!data.hasOwnProperty(k))continue;
1187var v=(data[k]||"").toString();
1188if(/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim())){email=v.trim();break;}
1189}
1190if(email){
1191try{var enc=btoa(email.toLowerCase().trim());document.cookie="_hp_email="+encodeURIComponent(enc)+";path=/;max-age=31536000;SameSite=Lax;Secure";}catch(e){}
1192}
1193}
1194
1195// Read _hpid from URL and set cookie for future visits
1196(function(){
1197var p=new URLSearchParams(window.location.search);
1198var hpid=p.get("_hpid");
1199if(hpid){
1200try{var decoded=atob(hpid);if(decoded.indexOf("@")!==-1){document.cookie="_hp_email="+encodeURIComponent(hpid)+";path=/;max-age=31536000;SameSite=Lax;Secure";}}catch(e){}
1201}
1202})();
1203
1204// URL-parameter auto-populate. Any input/textarea/select with
1205// data-persist-url-param="<name>" gets its value set from
1206// URLSearchParams(location.search).get(<name>) on script boot. Enables UTM /
1207// gclid / fbclid / any-custom-param capture into form submissions without a
1208// custom head script. An author (or the AI builder) drops
1209//   <input type="hidden" name="utm_source" data-persist-url-param="utm_source">
1210// and the runtime fills it in from ?utm_source=... on page load. Runs
1211// BEFORE the data-hp-original tagging pass below so the populated hidden
1212// inputs get stamped and pass _isInjected. utm_* names are not on
1213// _blocklist. Missing params leave any pre-existing value intact so authors
1214// can specify a fallback via a plain value="" on the input. Selector is
1215// scoped to form fields so a stray attribute on a non-form element is a
1216// no-op rather than a silent property write. Repeated params
1217// (?utm_source=a&utm_source=b) resolve to the first value ("a") per the
1218// URLSearchParams.get() spec.
1219try{
1220var _hpUrlParams=new URLSearchParams(window.location.search);
1221document.querySelectorAll("input[data-persist-url-param],textarea[data-persist-url-param],select[data-persist-url-param]").forEach(function(el){
1222var pname=el.getAttribute("data-persist-url-param");
1223if(!pname)return;
1224var pv=_hpUrlParams.get(pname);
1225if(pv!==null&&pv!=="")el.value=pv;
1226});
1227}catch(e){}
1228
1229// Standard-UTM auto-capture. A form with data-persist-capture-utms gets the
1230// seven standard attribution params (utm_source, utm_medium, utm_campaign,
1231// utm_term, utm_content, gclid, fbclid, msclkid) auto-injected as hidden inputs +
1232// populated from URLSearchParams if the URL carries them. Editor UI toggles
1233// the attribute; runtime does the injection. Idempotent by name, so a form
1234// with a pre-existing named hidden input (author-written or AI-authored)
1235// keeps its own definition and is only value-populated by the URL-param
1236// pass above. Runs BEFORE the data-hp-original tagging pass below so the
1237// injected inputs get stamped and pass _isInjected on submission.
1238try{
1239var _HP_STD_UTMS=["utm_source","utm_medium","utm_campaign","utm_term","utm_content","gclid","fbclid","msclkid"];
1240document.querySelectorAll("form[data-persist-capture-utms]").forEach(function(f){
1241_HP_STD_UTMS.forEach(function(n){
1242if(f.querySelector('input[name="'+n+'"]'))return;
1243var inp=document.createElement("input");
1244inp.type="hidden";
1245inp.name=n;
1246inp.setAttribute("data-persist-url-param",n);
1247var v=_hpUrlParams.get(n);
1248if(v!==null&&v!=="")inp.value=v;
1249f.appendChild(inp);
1250});
1251});
1252}catch(e){}
1253
1254// Cloudflare Turnstile CAPTCHA. A form with data-persist-captcha gets the bridge iframe
1255// injected before its submit control when the feature flag is set. TOP-LEVEL path only — in
1256// the sandboxed /p/ iframe the parent shell renders the widget (opaque origin can't run
1257// Turnstile). Owner/editor previews skip it. The bridge posts the token back (listener near
1258// _hpInjectCaptcha), stashed on the form and read by submitData as the top-level captchaToken.
1259if(!_hpIsOwner&&!inSandbox){try{
1260document.querySelectorAll("form[data-persist-captcha]").forEach(function(f){
1261// Multi-step forms defer injection to the LAST step (see the multi-step render), so the
1262// widget renders fresh + visible next to the submit and never appears on an earlier step.
1263if(f.hasAttribute("data-persist-multistep"))return;
1264_hpInjectCaptcha(f);
1265});
1266}catch(e){}}
1267
1268// Authored multi-step progress indicator — a real [data-persist-ms-progress] element the
1269// editor builds as page content (variant "text" | "bar" | "dots"), so the author restyles it
1270// like anything else on the page. Only its STATE is written here; the structure is authored
1271// markup and is never rebuilt, or a restyle would be thrown away on every step. MIRROR of
1272// syncMsProgressState in visual-editor/iframe-script/utils.ts, which does the same for the
1273// edit canvas — that file cannot be imported from here (this script is a self-contained twin
1274// of a separate Cloudflare-worker package; see form-scripts-sync.test.ts), so the two copies
1275// must agree on every attribute name below. Not to be confused with the TRANSIENT
1276// data-htmlpub-ms-progress line further down, injected only for a form with no authored nav.
1277function _hpMsProgress(f,cur,total){
1278var p=f.querySelector("[data-persist-ms-progress]");
1279if(!p)return;
1280var t=p.querySelector("[data-persist-ms-progress-text]");
1281if(t)t.textContent="Step "+(cur+1)+" of "+total;
1282var fill=p.querySelector("[data-persist-ms-progress-fill]");
1283if(fill)fill.style.width=(total>0?((cur+1)/total)*100:100)+"%";
1284var dots=p.querySelectorAll("[data-persist-ms-progress-dot]");
1285for(var i=0;i<dots.length;i++)dots[i].style.opacity=(i<=cur?"1":"0.25");
1286// A connector counts as travelled only once the step AFTER it is reached.
1287var lines=p.querySelectorAll("[data-persist-ms-progress-line]");
1288for(var j=0;j<lines.length;j++)lines[j].style.opacity=(j<cur?"1":"0.25");
1289}
1290
1291// ── Multi-step forms ── a form[data-persist-multistep] shows one [data-persist-step]
1292// container at a time, and validates the current step's required fields before advancing.
1293// Navigation uses the AUTHORED, page-styled buttons the editor creates —
1294// [data-persist-ms-next] (advance) + [data-persist-ms-back] (retreat) — with the real submit
1295// shown only on the last step. Legacy / hand-authored forms with no such buttons get a
1296// transient injected Back/Next + progress line (data-htmlpub-*, never saved) so they still
1297// work. The controller is stashed on the form as __hpMultiStep so the submit handler advances
1298// instead of submitting until the last step. The edit canvas (owner, NOT preview) skips this
1299// so every field stays visible for editing; Preview (data-preview="1") runs it.
1300if(!_hpIsOwner||_hpIsPreview){try{
1301document.querySelectorAll("form[data-persist-multistep]").forEach(function(f){
1302var steps=[].slice.call(f.querySelectorAll("[data-persist-step]"));
1303if(steps.length<2){
1304// A single-step form has no navigation — hide any authored Back/Next so they don't render
1305// as dead buttons; the submit stays as the only action.
1306var _hpDead=f.querySelectorAll("[data-persist-ms-back],[data-persist-ms-next]");
1307for(var _hpd=0;_hpd<_hpDead.length;_hpd++)_hpDead[_hpd].style.display="none";
1308// Same reasoning for an authored progress indicator: with no steps to advance through it
1309// would sit there reading "Step 1 of 1".
1310var _hpProg1=f.querySelector("[data-persist-ms-progress]");
1311if(_hpProg1)_hpProg1.style.display="none";
1312// A degenerate multi-step form (0 or 1 real steps) behaves like a plain form, but the boot-time
1313// captcha pass skipped it (it carries data-persist-multistep) and the last-step inject below
1314// never runs. Inject the widget here — otherwise the server enforces a token this page never
1315// produces and every submission is rejected (100% lead loss), while the editor still shows the
1316// placeholder. Mirrors the non-multistep boot path (owner/preview skip via !_hpIsOwner).
1317if(!_hpIsOwner&&f.hasAttribute("data-persist-captcha"))_hpInjectCaptcha(f);
1318return;
1319}
1320steps.sort(function(a,b){return (parseInt(a.getAttribute("data-persist-step"),10)||0)-(parseInt(b.getAttribute("data-persist-step"),10)||0);});
1321// Suppress native whole-form constraint validation: a required field in a
1322// not-yet-reached (display:none) step would otherwise abort submission and block
1323// Enter-to-advance. We validate each step in JS instead (validateStep on every hop
1324// + the last step on final submit), so required fields are still enforced.
1325f.noValidate=true;
1326// Exclude the authored nav buttons so a type-less [data-persist-ms-*] button is never picked as the submit.
1327var submit=_hpFindSubmit(f);
1328var submitDisp=submit?submit.style.display:"";
1329var cur=0;
1330// Prefer the authored nav buttons; fall back to injecting them for legacy forms.
1331var next=f.querySelector("[data-persist-ms-next]");
1332var back=f.querySelector("[data-persist-ms-back]");
1333var prog=null;
1334if(!next){
1335// Skipped when the form carries an AUTHORED indicator: the transient one exists only to
1336// stand in for a form that has none, and rendering both reads as a duplicate.
1337if(!f.querySelector("[data-persist-ms-progress]")){
1338prog=document.createElement("div");
1339prog.setAttribute("data-htmlpub-ms-progress","");
1340prog.style.cssText="font-family:system-ui,sans-serif;font-size:13px;opacity:0.7;margin-bottom:8px;";
1341if(steps[0].parentNode)steps[0].parentNode.insertBefore(prog,steps[0]);
1342}
1343var nav=document.createElement("div");
1344nav.setAttribute("data-htmlpub-ms-nav","");
1345nav.style.cssText="display:flex;gap:8px;margin-top:8px;";
1346// Reuse an authored Back if the form has one (only Next was missing) — don't inject a duplicate.
1347if(!back){back=document.createElement("button");back.type="button";back.setAttribute("data-htmlpub-ms-back","");back.textContent="Back";}
1348next=document.createElement("button");next.type="button";next.setAttribute("data-htmlpub-ms-next","");next.textContent="Next";
1349if(submit&&submit.className){next.className=submit.className;if(!back.className)back.className=submit.className;}
1350nav.appendChild(back);nav.appendChild(next);
1351var lastStep=steps[steps.length-1];
1352if(lastStep.parentNode)lastStep.parentNode.insertBefore(nav,lastStep.nextSibling);
1353}
1354var nextDisp=next?next.style.display:"";
1355var backDisp=back?back.style.display:"";
1356function render(){
1357for(var i=0;i<steps.length;i++)steps[i].style.display=(i===cur?"":"none");
1358if(prog)prog.textContent="Step "+(cur+1)+" of "+steps.length;
1359_hpMsProgress(f,cur,steps.length);
1360var last=cur===steps.length-1;
1361if(back)back.style.display=cur>0?backDisp:"none";
1362if(next)next.style.display=last?"none":nextDisp;
1363if(submit)submit.style.display=last?submitDisp:"none";
1364// Spam protection: inject the Turnstile widget the moment the visitor reaches the last step
1365// (fresh + visible so its token populates reliably — hiding it via display:none does not).
1366if(last&&!_hpIsOwner&&f.hasAttribute("data-persist-captcha"))_hpInjectCaptcha(f);
1367}
1368function _hpValidatable(c){
1369// Skip fields the visitor can't see/act on — a data-persist-hidden (display:none) or
1370// disabled required field can't show a validation bubble and would silently trap them.
1371if(c.disabled)return false;
1372if(c.closest&&c.closest("[data-persist-hidden]"))return false;
1373return typeof c.checkValidity==="function";
1374}
1375function validateStep(){
1376var ctrls=steps[cur].querySelectorAll("input,select,textarea");
1377for(var i=0;i<ctrls.length;i++){
1378if(_hpValidatable(ctrls[i])&&!ctrls[i].checkValidity()){
1379if(typeof ctrls[i].reportValidity==="function")ctrls[i].reportValidity();
1380return false;
1381}
1382}
1383return _hpGroupReqOK(steps[cur]);
1384}
1385function validateOutside(){
1386// Required controls authored OUTSIDE any step container are never in validateStep's scope;
1387// f.noValidate is on form-wide, so validate them on final submit or they'd submit empty.
1388var all=f.querySelectorAll("input,select,textarea");
1389for(var i=0;i<all.length;i++){
1390var c=all[i];
1391if(c.closest&&c.closest("[data-persist-step]"))continue;
1392if(_hpValidatable(c)&&!c.checkValidity()){
1393if(typeof c.reportValidity==="function")c.reportValidity();
1394return false;
1395}
1396}
1397return true;
1398}
1399function advance(){if(cur<steps.length-1&&validateStep()){cur++;render();}}
1400if(next)next.addEventListener("click",function(e){if(e&&e.preventDefault)e.preventDefault();advance();});
1401if(back)back.addEventListener("click",function(e){if(e&&e.preventDefault)e.preventDefault();if(cur>0){cur--;render();}});
1402// validate() re-checks the CURRENT step + any required controls outside all steps — the
1403// submit handler calls it on the last step before sending (native validation is off).
1404f.__hpMultiStep={isLast:function(){return cur>=steps.length-1;},advance:advance,validate:function(){return validateStep()&&validateOutside();}};
1405render();
1406});
1407}catch(e){}}
1408
1409// ── International phone ── input[type=tel][data-persist-phone] gets a country
1410// dial-code <select> injected before it + a hidden E.164 companion that carries the
1411// real submitted value (+<dial><national digits>) under the field's name. The visible
1412// tel input keeps the national number for display and is excluded from collect()
1413// (data-hp-phone-nat); the <select> is excluded too (data-hp-phone-cc). On every
1414// input / country change the E.164 is reassembled and validated — E.164 spec (7–15
1415// total digits) plus a GENEROUS per-country national-length range where the length is
1416// well known — via setCustomValidity, so native validation blocks a malformed submit
1417// on single-step forms and validateStep() enforces it on multi-step ones. Runs BEFORE
1418// the data-hp-original tagging pass so the hidden companion is stamped and collected.
1419// Runs in owner mode too so the editor preview shows the selector (the validity flag
1420// is harmless there — owner previews don't submit real leads). data-persist-phone /
1421// data-persist-phone-country are persistent (never stripped from the AI diff); the
1422// injected select + companion are data-hp-* (transient) and never saved.
1423try{
1424var _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]];
1425var _hpPhoneByIso={};for(var _hppi=0;_hppi<_HP_PHONE.length;_hppi++){_hpPhoneByIso[_HP_PHONE[_hppi][0]]=_HP_PHONE[_hppi];}
1426var _hpDigits=function(s){return (s||"").replace(/[^0-9]/g,"");};
1427var _hpFlag=function(iso){try{return String.fromCodePoint(0x1F1E6+(iso.charCo
1427deAt(0)-65))+String.fromCodePoint(0x1F1E6+(iso.charCodeAt(1)-65));}catch(e){return "";}};
1428// Reduce the typed digits to the country's national significant number (NSN): strip a
1429// user-typed country code (guarded by the expected NSN length so a valid number is never
1430// shortened) and a national trunk prefix "0" (guarded so it never drops below the country
1431// minimum; suppressed for keepLeadingZero countries like Italy). Lead-safe: each strip
1432// fires only when it yields a still-plausible NSN, so "07911 123456", "06 12 34 56 78" and
1433// "1 (415) 555-2671" all assemble to correct E.164 and pass validation.
1434var _hpNSN=function(nat,cur){
1435var dial=cur[2],min=cur[3],max=cur[4],keep0=cur[5];
1436if(dial&&nat.length>dial.length&&nat.substring(0,dial.length)===dial){
1437var rest=nat.substring(dial.length);
1438var restT=(!keep0&&rest.charAt(0)==="0")?rest.replace(/^0+/,""):rest;
1439if(min&&max){
1440// Known national length: keep whichever candidate fits the country's range.
1441if(restT.length>=min&&restT.length<=max)nat=restT;
1442else if(rest.length>=min&&rest.length<=max)nat=rest;
1443}
1444// Variable-length country (min/max unknown): do NOT strip the leading dial-code digits —
1445// for those countries a real national number can legitimately begin with its own dial
1446// code, and chopping it corrupts the number (F4). "+"-prefixed input is handled before
1447// _hpNSN, so this only affects the ambiguous no-"+" case; keep the digits as typed.
1448}
1449if(!keep0&&nat.charAt(0)==="0"){var s=nat.replace(/^0+/,"");if(!min||s.length>=min)nat=s;}
1450// Russia/Kazakhstan (dial 7) use "8" as the national trunk prefix, not "0". Strip a lone
1451// leading "8", guarded by the country minimum so a valid NSN is never shortened (F3).
1452if(dial==="7"&&nat.charAt(0)==="8"){var s8=nat.replace(/^8/,"");if(!min||s8.length>=min)nat=s8;}
1453return nat;
1454};
1455document.querySelectorAll('input[type="tel"][data-persist-phone]').forEach(function(inp){
1456if(inp.getAttribute("data-hp-phone-on")==="1")return;
1457inp.setAttribute("data-hp-phone-on","1");
1458// Key the E.164 companion the SAME way collect()/extract-form-fields key a plain tel
1459// input (name -> id -> data-field -> "phone"), computed BEFORE any DOM mutation so
1460// enabling international never changes which lead column the number lands in.
1461var nm=fieldKey(inp,0);
1462var iso=(inp.getAttribute("data-persist-phone-country")||"US").toUpperCase();
1463if(!_hpPhoneByIso[iso])iso="US";
1464// Custom country combobox. A native <select> can't show a compact "flag +dial" when
1465// CLOSED and full country names when OPEN: its closed text IS the selected option's
1466// text, and a same-value close fires no event to swap it back. So we build a small
1467// self-contained combobox — a styled trigger button showing "flag +dial" + a panel
1468// listing full country names. curIso tracks the selection for E.164 assembly. Lay it
1469// out side by side with the number in a flex row (standard international-phone layout);
1470// the trigger copies the number input's border/radius/padding/font/colors so it matches
1471// whatever the page's forms look like (align-items:stretch matches their heights).
1472var curIso=iso;
1473// Brand accent for the selected-row tint + checkmark, sampled from the form's primary
1474// (submit) button background so the dropdown matches the page. A light tint keeps the
1475// dark option text legible. Falls back to a neutral blue; transparent / near-white
1476// buttons are ignored so the accent stays visible.
1477var _hpAccent="#2563eb",_hpAccentTint="rgba(37,99,235,0.12)";
1478try{
1479var _hpBtnForm=inp.closest?inp.closest("form"):null;
1480var _hpBtn=_hpBtnForm?_hpFindSubmit(_hpBtnForm):null;
1481if(_hpBtn){
1482var _bg=window.getComputedStyle(_hpBtn).backgroundColor||"";
1483var _pp=_bg.replace(/[^0-9,.]/g,"").split(",");
1484var _rr=parseInt(_pp[0],10),_gg=parseInt(_pp[1],10),_bb=parseInt(_pp[2],10),_aa=_pp.length>3?parseFloat(_pp[3]):1;
1485if(!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)";}
1486}
1487}catch(e){}
1488// Use the AUTHORED wrapper (data-persist-phone-field) that the editor saved, so the
1489// published layout is identical to the edit canvas. Create one only as a fallback for
1490// legacy pages saved before the authored wrapper existed (served pages aren't re-saved,
1491// so a runtime-created wrapper is harmless there).
1492var wrap=inp.closest?inp.closest("[data-persist-phone-field]"):null;
1493if(!wrap){
1494wrap=document.createElement("span");
1495wrap.setAttribute("data-persist-phone-field","");
1496wrap.style.cssText="display:flex;align-items:stretch;gap:6px;width:100%;position:relative;";
1497if(inp.parentNode)inp.parentNode.insertBefore(wrap,inp);
1498wrap.appendChild(inp);
1499}
1500if(!wrap.style.position)wrap.style.position="relative";
1501// A <span role=button>, NOT a <button>: the widget sits inside the field's <label>, and
1502// a <label> forwards clicks to its first LABELABLE descendant. A <button> would become
1503// that control, so clicking a list item fires a synthetic click on the trigger and
1504// re-opens the panel. A <span> isn't labelable, so the label keeps targeting the number
1505// input; preventDefault on the trigger/list clicks stops any residual label forwarding.
1506var trig=document.createElement("span");
1507trig.setAttribute("role","button");
1508trig.setAttribute("tabindex","0");
1509trig.setAttribute("data-hp-phone-cc","");
1510trig.setAttribute("aria-haspopup","listbox");
1511trig.setAttribute("aria-expanded","false");
1512trig.setAttribute("aria-label","Country calling code");
1513trig.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;";
1514try{
1515var _cs=window.getComputedStyle(inp);
1516var _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"];
1517for(var _cpi=0;_cpi<_cp.length;_cpi++){try{if(_cs[_cp[_cpi]])trig.style[_cp[_cpi]]=_cs[_cp[_cpi]];}catch(e){}}
1518}catch(e){}
1519var trigLabel=document.createElement("span");
1520trig.appendChild(trigLabel);
1521var trigCaret=document.createElement("span");trigCaret.textContent="▾";trigCaret.style.cssText="opacity:0.55;font-size:0.8em;";
1522trig.appendChild(trigCaret);
1523var panel=document.createElement("ul");
1524panel.setAttribute("role","listbox");
1525panel.setAttribute("data-hp-phone-list","");
1526panel.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;";
1527// Hover + selected-item styling (one shared, id-guarded stylesheet — inline styles can't
1528// express :hover / ::after). Injected on the live page only; never touches the editor.
1529if(!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);}
1530panel.style.setProperty("--hp-accent",_hpAccent);
1531panel.style.setProperty("--hp-accent-tint",_hpAccentTint);
1532for(var pci=0;pci<_HP_PHONE.length;pci++){
1533var pc=_HP_PHONE[pci];
1534var li=document.createElement("li");
1535li.setAttribute("role","option");
1536li.setAttribute("data-iso",pc[0]);
1537li.setAttribute("data-dial",pc[2]);
1538li.setAttribute("aria-selected",pc[0]===iso?"true":"false");
1539li.textContent=_hpFlag(pc[0])+" "+pc[1]+" +"+pc[2];
1540li.style.cssText="padding:7px 14px;cursor:pointer;white-space:nowrap;";
1541panel.appendChild(li);
1542}
1543var renderTrig=function(){var c=_hpPhoneByIso[curIso];trigLabel.textContent=_hpFlag(curIso)+" +"+(c?c[2]:"");};
1544var closePanel=function(){panel.style.display="none";trig.setAttribute("aria-expanded","false");};
1545var openPanel=function(){panel.style.display="block";trig.setAttribute("aria-expanded","true");};
1546inp.style.flex="1";
1547inp.style.minWidth="0";
1548wrap.insertBefore(trig,inp); // trigger sits before the number input (input already in the wrap)
1549wrap.appendChild(panel);
1550var hid=document.createElement("input");
1551hid.type="hidden";
1552hid.name=nm;
1553hid.setAttribute("data-hp-phone-e164","");
1554wrap.appendChild(hid);
1555inp.setAttribute("data-hp-phone-nat","");
1556inp.removeAttribute("name");
1557var _hpSyncPhone=function(){
1558var raw=(inp.value||"").trim();
1559var cur=_hpPhoneByIso[curIso];
1560var dial=cur?cur[2]:"";
1561var e164="",nsnLen=0,isIntl=false;
1562if(raw.charAt(0)==="+"){e164="+"+_hpDigits(raw);isIntl=true;}
1563else{var nsn=_hpNSN(_hpDigits(raw),cur);nsnLen=nsn.length;e164=nsn?("+"+dial+nsn):"";}
1564hid.value=e164;
1565var msg="";
1566if(raw){
1567var total=_hpDigits(e164).length;
1568// Hard rules only: E.164 spec (7-15 total digits) + a per-country MINIMUM NSN length
1569// (catches a genuinely incomplete number, e.g. a US local number with no area code). No
1570// per-country maximum -> an over-long entry (usually the visitor including extra
1571// country/trunk digits) is accepted, never rejected. A "+"-prefixed entry skips the
1572// per-country floor entirely (the visitor owns their own format).
1573if(total<7||total>15)msg="Enter a valid phone number.";
1574else if(!isIntl&&nsnLen&&cur&&cur[3]&&nsnLen<cur[3])msg="Enter a valid "+cur[1]+" phone number.";
1575}
1576if(typeof inp.setCustomValidity==="function")inp.setCustomValidity(msg);
1577};
1578var 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();};
1579panel.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){}}});
1580trig.addEventListener("click",function(e){e.preventDefault();if(panel.style.display==="block")closePanel();else openPanel();});
1581trig.addEventListener("keydown",function(e){if(e.key==="Escape"){closePanel();}else if(e.key==="ArrowDown"||e.key==="Enter"||e.key===" "){e.preventDefault();openPanel();}});
1582document.addEventListener("click",function(e){if(!wrap.contains(e.target))closePanel();});
1583inp.addEventListener("input",_hpSyncPhone);
1584inp.addEventListener("blur",_hpSyncPhone);
1585renderTrig();
1586_hpSyncPhone();
1587});
1588}catch(e){}
1589
1590// Tag all hidden inputs present in the original HTML so we can distinguish
1591// them from ones injected later by third-party scripts (Customers.ai, etc.)
1592document.querySelectorAll('input[type="hidden"]').forEach(function(el){el.setAttribute("data-hp-original","1");});
1593
1594function showNotice(f,text,ok){
1595var m=document.createElement("div");
1596m.textContent=text;
1597m.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");
1598f.parentNode.insertBefore(m,f.nextSibling);
1599setTimeout(function(){m.remove()},5000);
1600}
1601// showMsg wraps showNotice; the optional 3rd arg lets callers override the default success/error
1602// copy (e.g. the Turnstile "still verifying" gate). Omit it for the standard success/error notices.
1603function 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);}
1604// reCAPTCHA v2 (HP-1475). The response token lives in a hidden
1605// <textarea name="g-recaptcha-response"> that grecaptcha creates inside the
1606// widget's container; scoping the lookup to the submitted form/container picks
1607// the right token when a page renders more than one widget.
1608function _hpCaptchaToken(f){
1609try{var ta=f.querySelector&&f.querySelector('textarea[name="g-recaptcha-response"]');return ta&&ta.value?ta.value:"";}catch(e){return "";}
1610}
1611
1612function formId(f){
1613if(f.tagName==="FORM")return f.id||f.getAttribute("name")||"form-"+([].indexOf.call(document.forms,f));
1614return f.id||"section-"+([].indexOf.call(document.querySelectorAll("[data-htmlpub-fake-form]"),f));
1615}
1616
1617// Listen for postMessage responses from parent shell
1618if(inSandbox){
1619window.addEventListener("message",function(e){
1620var d=e.data;
1621if(d&&d.type==="htmlpub-form-submit-result"&&d.correlationId){
1622var cb=pendingCallbacks[d.correlationId];
1623if(cb){
1624delete pendingCallbacks[d.correlationId];
1625cb(d.success,d.captchaRequired,d.error);
1626}
1627}
1628});
1629}
1630
1631function isSafeRedirectUrl(url){
1632if(!url)return false;
1633if(/^https?:\/\//i.test(url))return true;
1634if(url.charAt(0)==="/"&&url.charAt(1)!=="/")return true;
1635return false;
1636}
1637// 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).
1638function stripTrailingSlash(url){
1639try{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){}
1640return url;
1641}
1642function maybeRedirectUrl(url){
1643if(window.htmlpubEditorMode)return;
1644if(!isSafeRedirectUrl(url))return;
1645if(/\/api\/forms\/submit$/.test(url))return;
1646setTimeout(function(){window.location.href=stripTrailingSlash(url);},600);
1647}
1648function maybeRedirect(f){maybeRedirectUrl(f.getAttribute("action"));}
1649function reportFail(kind,status,fident){
1650try{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){}
1651}
1652function submitData(f,data){
1653var btn=_hpFindSubmit(f);
1654if(btn)btn.disabled=true;
1655
1656// Generate one eventID per submission, threaded through three places:
1657//   (a) the JSON payload to /api/forms/submit → integration queue
1658//   (b) the postMessage to the parent shell (which forwards to the same API)
1659//   (c) the browser pixel events fired on success below
1660// Both browser and server emit the SAME id to the same downstream provider
1661// so Meta / TikTok dedup them as a single conversion.
1662var eventId=_hpUuid();
1663var fident=formId(f);
1664// Per-form Turnstile token — delivered by the bridge iframe via postMessage (see the listener
1665// near _hpInjectCaptcha) and stashed on the form element.
1666var _hpCapTok=(f.__hpCaptchaToken||"");
1667// stable editor-stamped form id → native lead-magnet routing; "" when unbound
1668var fkey=f.getAttribute("data-persist-form-id")||"";
1669// Classic form uuid re-stamped on migrated forms → per-form scope for carried Classic Zaps
1670var fsrc=f.getAttribute("data-persist-form-source-id")||"";
1671
1672// reCAPTCHA gate (HP-1475): when the page enforces reCAPTCHA but the visitor
1673// hasn't solved the challenge, block the submit with an inline error instead
1674// of POSTing an unverifiable payload the server will reject anyway. Gated on
1675// the enforcement flag (not mere widget presence) so pages with a decorative
1676// widget are unaffected. The server remains the security boundary — it
1677// re-verifies the token against Google. A form that opts into per-form Turnstile
1678// (data-persist-captcha) is handled by the Turnstile gate below instead — skip it here so a
1679// Turnstile form on a reCAPTCHA-enforced page isn't blocked for lacking a reCAPTCHA token
1680// (mirrors the server, which skips its reCAPTCHA block for forms in captchaFormIds).
1681var captchaToken=_hpCaptchaToken(f);
1682if(_hpRecaptchaEnforced&&!captchaToken&&!(f.hasAttribute&&f.hasAttribute("data-persist-captcha"))){
1683if(btn)btn.disabled=false;
1684showNotice(f,"Please complete the reCAPTCHA challenge before submitting.",false);
1685reportFail("captcha_required",0,fident);
1686return;
1687}
1688
1689if(inSandbox){
1690var id="fs-"+(++corrId);
1691var timer=setTimeout(function(){
1692delete pendingCallbacks[id];
1693if(btn)btn.disabled=false;
1694reportFail("timeout",0,fident);showMsg(f,false);
1695},10000);
1696
1697pendingCallbacks[id]=function(success,captchaRequired,errMsg){
1698clearTimeout(timer);
1699if(btn)btn.disabled=false;
1700if(success){setEmailCookie(data);_hpFireLead(fident,data,eventId);showMsg(f,true);if(typeof f.reset==="function")f.reset();maybeRedirect(f);}
1701else if(captchaRequired){showMsg(f,false,"Spam protection is still verifying — please wait a moment and submit again.");}
1702else{reportFail("relay_error",0,fident);showMsg(f,false,errMsg||undefined);}
1703};
1704
1705var vid="";try{vid=localStorage.getItem("_hp_vid")||"";}catch(e){}
1706parent.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"))},"*");
1707}else{
1708// Send in top-level (direct fetch) mode. Wrapped in _hpProceed so it can be deferred
1709// until the Turnstile token has had a bounded chance to arrive (see _hpWaitForCaptcha).
1710function _hpProceed(){
1711var vid2="";try{vid2=localStorage.getItem("_hp_vid")||"";}catch(e){}
1712// The captcha token rides EVERY attempt (not just the first). It is single-use,
1713// but the server checks its verified-eventId cache before re-verifying, so a
1714// re-sent already-spent token short-circuits to pass, while a token that was never
1715// spent (attempt 0 failed at the transport / rate-limit layer before verify) still
1716// verifies on retry. Stripping it from retries silently dropped leads on captcha
1717// forms whenever attempt 0 hit a 429/5xx/network error. [HP-2467] Prefer the per-form
1718// Turnstile token; fall back to the reCAPTCHA token (a form uses one mechanism). Read
1719// the FRESHEST token here — it may have arrived via postMessage during the bounded wait,
1720// after _hpCapTok was snapshotted at the top of submitData.
1721var _capTok=(f.__hpCaptchaToken||"")||_hpCapTok||captchaToken;
1722var _hpBody=JSON.stringify({pageId:pageId,pageSlug:slug,formIdentifier:fident,formKey:fkey,classicFormUuid:fsrc,data:data,visitorId:vid2,eventId:eventId,captchaToken:_capTok});
1723// Retry transient failures so a network blip / 5xx / 429 doesn't silently lose
1724// a lead. Every attempt reuses the same eventId, so the server dedups to
1725// exactly one row + one set of side-effects. Backoff: immediate, 15s, 30s,
1726// 2min, then give up. Failed submits don't redirect, so the page stays open
1727// and the delayed retries can run. The submit button stays disabled across
1728// retries to prevent a manual re-submit (which would mint a new eventId). [HP-2467]
1729var _hpDelays=[0,15000,30000,120000];
1730function _hpAttempt(n){
1731fetch(api,{method:"POST",keepalive:true,headers:{"Content-Type":"application/json"},body:_hpBody}).then(function(r){
1732if(r.ok){if(btn)btn.disabled=false;setEmailCookie(data);_hpFireLead(fident,data,eventId);showMsg(f,true);if(typeof f.reset==="function")f.reset();
1733// The Turnstile token is single-use; after a successful submit clear it and reload the bridge
1734// iframe so a subsequent submit on the same (non-reset / non-redirected) form gets a FRESH token
1735// instead of re-sending the spent one and being rejected.
1736try{f.__hpCaptchaToken="";f.__hpCaptchaError=false;var _hpCf=f.querySelector("iframe[data-hp-turnstile]");if(_hpCf)_hpCf.src=_hpCf.src;}catch(e){}
1737maybeRedirect(f);return;}
1738if((r.status>=500||r.status===429)&&n<_hpDelays.length){setTimeout(function(){_hpAttempt(n+1);},_hpDelays[n]);return;}
1739if(btn)btn.disabled=false;reportFail("http_error",r.status,fident);
1740// A 403 on a captcha-protected form means the token was spent/invalid — refresh the widget
1741// so a retry gets a fresh one (otherwise "try again" resends the same rejected token).
1742try{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){}
1743// Surface the server's specific message (e.g. "CAPTCHA verification failed") instead of a
1744// generic one; fall back to the generic message when the body isn't JSON.
1745try{r.json().then(function(d){showMsg(f,false,(d&&d.error)||undefined);},function(){showMsg(f,false);});}catch(e){showMsg(f,false);}
1746}).catch(function(){
1747if(n<_hpDelays.length){setTimeout(function(){_hpAttempt(n+1);},_hpDelays[n]);return;}
1748if(btn)btn.disabled=false;reportFail("network",0,fident);showMsg(f,false);
1749});
1750}
1751_hpAttempt(0);
1752}
1753// Spam-protected form whose Turnstile widget exists but hasn't produced a token yet
1754// (fast submit, or the challenge is still loading). Per the decided fail policy, give the
1755// widget a bounded window plus ONE re-challenge, then submit anyway (the server accepts +
1756// flags unverified — never lose a lead) instead of the old "please submit again" dead end.
1757// Skip the wait when: owner preview (widget is owner-gated), a token is already in hand, the
1758// bridge reported a TERMINAL error (no token could ever come), or no widget is present (site
1759// key unconfigured → nothing to wait for, server fails open). The button stays disabled
1760// during the (sub-second-typical, 6s-worst-case) wait so the visitor can't double-submit.
1761if(_hpIsOwner||_hpCapTok!==""||f.__hpCaptchaError||!f.querySelector("iframe[data-hp-turnstile]")){
1762_hpProceed();
1763}else{
1764_hpWaitForCaptcha(f,_hpProceed);
1765}
1766}
1767}
1768
1769// A pasted third-party form the migration kept (data-persist-third-party-form: Kit, Mailchimp,
1770// PayPal, ...) submits to its own action, natively or through the vendor's script, so it is never
1771// cancelled. Its lead is also sent here, fire-and-forget: keepalive survives the navigation, and
1772// there is no UI, retry or redirect because the page is leaving and the vendor owns the outcome.
1773function _hpBackgroundCapture(f,data){
1774var eventId=_hpUuid();
1775var fident=formId(f);
1776var vid="";try{vid=localStorage.getItem("_hp_vid")||"";}catch(e){}
1777var 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)};
1778try{
1779if(inSandbox){msg.type="htmlpub-form-submit";msg.correlationId="fs-"+(++corrId);parent.postMessage(msg,"*");}
1780else{fetch(api,{method:"POST",keepalive:true,headers:{"Content-Type":"application/json"},body:JSON.stringify(msg)}).catch(function(){reportFail("network",0,fident);});}
1781setEmailCookie(data);_hpFireLead(fident,data,eventId);
1782}catch(e){}
1783}
1784
1785// Snapshot forms in the server-rendered HTML at init; third-party embeds (ConvertKit/ActiveCampaign) inject their <form> later, so they're excluded.
1786var _hpNativeForms=(function(){try{return new WeakSet([].slice.call(document.querySelectorAll("form")));}catch(e){return null;}})();
1787document.addEventListener("submit",function(e){
1788var f=e.target;
1789if(f.tagName!=="FORM")return;
1790if(_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]
1791if(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
1792var tpData=collect(f);if(Object.keys(tpData).length)_hpBackgroundCapture(f,tpData);return;}
1793e.preventDefault();
1794// Multi-step: if this form is split into steps and the visitor isn't on the last
1795// one yet, advance (validate current step + Next) instead of submitting. Captures
1796// Enter-in-field and any early submit so a partial lead is never sent. On the last
1797// step, validate it in JS first (the form is novalidate, so nothing else does). [P3]
1798if(f.__hpMultiStep){
1799if(!f.__hpMultiStep.isLast()){f.__hpMultiStep.advance();return;}
1800if(f.__hpMultiStep.validate&&!f.__hpMultiStep.validate())return;
1801}
1802// "Select at least one" enforcement for required checkbox groups (single-step forms have no
1803// __hpMultiStep validate pass, and it's a cheap final guard for multi-step too).
1804if(!_hpGroupReqOK(f))return;
1805var data=collect(f);
1806if(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]
1807submitData(f,data);
1808},true);
1809
1810// --- Fake form interception (inputs outside <form>) ---
1811var lastFakeSubmit=0;
1812// Walk up from the clicked element to find a "form-like" container:
1813//   - has 1-20 inputs/textareas/selects (excluding hidden/submit/button types)
1814//   - has ≤4 button-like descendants (more = page scaffolding, not a form)
1815//   - the clicked element sits AFTER the last input in document order
1816//     (real submits sit below fields; uses DOCUMENT_POSITION_PRECEDING = 2)
1817// Walk depth capped at 3 — further up is <section>, <main>, or <body>.
1818function findFormLikeContainer(t){
1819var cur=t.parentElement;
1820for(var depth=0;cur&&depth<3;depth++){
1821if(cur.tagName==="FORM")return null;// real form, skip
1822var inputs=cur.querySelectorAll("input:not([type=hidden]):not([type=submit]):not([type=button]),textarea,select");
1823if(inputs.length>=1&&inputs.length<=20){
1824var ctas=cur.querySelectorAll("button,a[href],[role=button],input[type=submit]");
1825if(ctas.length<=4){
1826return (t.compareDocumentPosition(inputs[inputs.length-1])&2)?cur:null;
1827}
1828}
1829cur=cur.parentElement;
1830}
1831return null;
1832}
1833// <a> with a real href is navigation, never a submit. Allow empty href / "#" /
1834// no href (button-styled anchors). <button type="button"> is explicitly non-submit.
1835function isPlausibleSubmit(t){
1836if(t.tagName==="A"){
1837var href=t.getAttribute("href");
1838if(href!==null){
1839href=(href+"").trim();
1840if(href!==""&&href!=="#")return false;
1841}
1842}
1843if(t.tagName==="BUTTON"&&t.getAttribute("type")==="button")return false;
1844return true;
1845}
1846document.addEventListener("click",function(e){
1847var t=e.target.closest?e.target.closest("button,a,[role=button],input[type=submit]"):null;
1848if(!t)return;
1849if(t.closest("form"))return;// inside a real form, native handler covers it
1850// Third-party consent-management (CMP) dialogs (Cookiebot/OneTrust/Osano/
1851// TrustArc/CookieYes) inject a body-level dialog whose "Accept" button + single
1852// pre-checked "necessary" checkbox otherwise look form-like. Cheap early-exit
1853// for known vendors; the lead-field guard below is the load-bearing check.
1854// Vendor tokens are boundary-anchored (CookieYes cky-* matched as whole class
1855// tokens; TrustArc truste-/truste_/trustarc) so they never collide with
1856// sticky-*/trusted* names. [HP-3292]
1857try{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){}
1858if(!isPlausibleSubmit(t))return;
1859var now=Date.now();
1860if(now-lastFakeSubmit<2000)return;
1861var container=findFormLikeContainer(t);
1862if(!container)return;
1863// A genuine lead always carries at least one typed (non checkbox/radio) field
1864// with a value. A form-like container whose only populated field is a
1865// checkbox/radio is a consent/cookie dialog, not a form — never fire a lead for
1866// it. Keys off DOM field TYPE, never payload values, so real leads whose data
1867// merely contains vendor-name substrings are unaffected. [HP-3292]
1868var _leadFields=container.querySelectorAll("input:not([type=hidden]):not([type=submit]):not([type=button]):not
1868([type=checkbox]):not([type=radio]),textarea,select");
1869var _hasLeadField=false;
1870for(var _lf=0;_lf<_leadFields.length;_lf++){if((_leadFields[_lf].value||"").trim()){_hasLeadField=true;break;}}
1871if(!_hasLeadField)return;
1872var data=collect(container);
1873if(Object.keys(data).length===0)return;
1874// Skip if all field values are empty (button click near empty form fields)
1875var hasAnyValue=Object.keys(data).some(function(k){return data[k]&&String(data[k]).trim();});
1876if(!hasAnyValue)return;
1877lastFakeSubmit=now;
1878container.setAttribute("data-htmlpub-fake-form","");
1879submitData(container,data);
1880// Notify analytics script about this fake form submission
1881try{document.dispatchEvent(new CustomEvent("htmlpub:form-submit",{detail:{formIdentifier:formId(container)}}));}catch(e){}
1882},true);
1883
1884// Always report form count to parent (even for non-owner views)
1885function reportFormCount(){
1886var forms=document.querySelectorAll("form");
1887var count=forms.length;
1888var labels=[];
1889for(var i=0;i<forms.length;i++){
1890var ff=forms[i];
1891var label=ff.getAttribute("aria-label")||ff.getAttribute("name")||ff.getAttribute("id")||"";
1892if(!label){
1893var heading=ff.querySelector("h1,h2,h3,h4,h5,h6,label,legend");
1894if(heading) label=heading.textContent.trim().substring(0,50);
1895}
1896labels.push(label||("Form "+(i+1)));
1897}
1898// needsCaptcha tells the parent shell (sandbox path) to render a Turnstile
1899// widget on its own origin: the sandboxed iframe has an opaque origin so it
1900// can't run Turnstile itself. Owner previews never require the token.
1901var _hpNeedsCaptcha=!_hpIsOwner&&!!document.querySelector("form[data-persist-captcha]");
1902if(inSandbox) parent.postMessage({type:"htmlpub-form-count",count:count,labels:labels,needsCaptcha:_hpNeedsCaptcha},"*");
1903}
1904function initFormCount(){
1905reportFormCount();
1906if(typeof MutationObserver!=="undefined"){
1907new MutationObserver(function(){reportFormCount();}).observe(document.body||document.documentElement,{childList:true,subtree:true});
1908}
1909}
1910if(document.readyState==="loading"){document.addEventListener("DOMContentLoaded",initFormCount);}
1911else{initFormCount();}
1912
1913// Leadbox/leadbar iframe form bridge. SYNC with worker-pages/src/scripts/forms.ts.
1914window.addEventListener("message",function(e){
1915if(!e||!e.data||e.data.type!=="htmlpub:iframe-form-submit")return;
1916var fid=String(e.data.formIdentifier||"iframe-form").slice(0,255);
1917var fsid=e.data.formSourceId?String(e.data.formSourceId).slice(0,255):"";
1918var raw=e.data.data;
1919if(!raw||typeof raw!=="object")return;
1920var clean={};
1921for(var k in raw){
1922if(!Object.prototype.hasOwnProperty.call(raw,k))continue;
1923if(looksLikeRawId(k))continue;
1924if(_blocklist.test(k))continue;
1925clean[k]=raw[k];
1926}
1927var redirect=typeof e.data.redirect==="string"?e.data.redirect:"";
1928var relayCaptcha=typeof e.data.captchaToken==="string"?e.data.captchaToken:undefined;
1929fetch(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);});
1930},false);
1931})();
1932</script>
1932<script data-htmlpub-popups>
1933(function(){
1934"use strict";
1935
1936var SCOPE = "pathname";
1937var PAGE_ID = "v8s3dfzxmk03xqctda3738k8";
1938
1939var DEFAULTS = {
1940  overlay:   { color: "#000000", opacity: 0.5,  clickToClose: true },
1941  close:     { enabled: true, position: "top-right", escapeToClose: true },
1942  animation: { open: "fade", close: "fade", durationMs: 250 },
1943  frequency: { visitsBefore: 0, daysCooldown: 0 }
1944};
1945
1946function parseJsonAttr(raw, fallback) {
1947  if (!raw) return fallback;
1948  try {
1949    var p = JSON.parse(raw);
1950    if (p && typeof p === "object") return p;
1951  } catch(e) {}
1952  return fallback;
1953}
1954
1955// Back-compat: pages published before the data-persist-popup* rename carry the
1956// legacy data-htmlpub-popup* names. Read canonical first, fall back to legacy.
1957// suffix is "" for the marker, "-name", "-overlay", "-target", etc.
1958function pAttr(el, suffix) {
1959  var v = el.getAttribute("data-persist-popup" + suffix);
1960  return v !== null ? v : el.getAttribute("data-htmlpub-popup" + suffix);
1961}
1962
1963function readConfig(el) {
1964  var overlay   = parseJsonAttr(pAttr(el, "-overlay"),   {});
1965  var closeCfg  = parseJsonAttr(pAttr(el, "-close"),     {});
1966  var anim      = parseJsonAttr(pAttr(el, "-animation"), {});
1967  var triggers  = parseJsonAttr(pAttr(el, "-triggers"),  {});
1968  var frequency = parseJsonAttr(pAttr(el, "-frequency"), {});
1969  return {
1970    overlay: {
1971      color:        typeof overlay.color === "string"   ? overlay.color   : DEFAULTS.overlay.color,
1972      opacity:      typeof overlay.opacity === "number" ? overlay.opacity : DEFAULTS.overlay.opacity,
1973      clickToClose: overlay.clickToClose !== false
1974    },
1975    close: {
1976      enabled:        closeCfg.enabled !== false,
1977      position:       closeCfg.position === "top-left" ? "top-left" : "top-right",
1978      escapeToClose:  closeCfg.escapeToClose !== false
1979    },
1980    animation: {
1981      open:        typeof anim.open === "string"  ? anim.open       : DEFAULTS.animation.open,
1982      close:       typeof anim.close === "string" ? anim.close      : DEFAULTS.animation.close,
1983      durationMs:  typeof anim.durationMs === "number" ? anim.durationMs : DEFAULTS.animation.durationMs
1984    },
1985    triggers: triggers,
1986    frequency: {
1987      visitsBefore: typeof frequency.visitsBefore === "number" ? frequency.visitsBefore : 0,
1988      daysCooldown: typeof frequency.daysCooldown === "number" ? frequency.daysCooldown : 0
1989    },
1990    // "none" = the marker's content owns its own chrome, so this runtime paints none of it.
1991    // Migrated Classic pop-ups: the marker holds an <iframe> whose document already draws the
1992    // mask, centring, card and close button. Nova-authored pop-ups never set it.
1993    chrome: pAttr(el, "-chrome") === "none" ? "none" : "default"
1994  };
1995}
1996
1997// ── LocalStorage helpers — scoped per pathname so two pages on the same
1998// origin track their popups independently. Wrapped in try/catch because
1999// Safari private mode + iframe sandboxing can throw on access.
2000var STORE_PREFIX = SCOPE === "popup-id" ? "_hpup:popup:" : ("_hpup:" + (location.pathname || "/") + ":");
2001function lsGet(key) {
2002  try { return localStorage.getItem(STORE_PREFIX + key); } catch(e) { return null; }
2003}
2004function lsSet(key, value) {
2005  try { localStorage.setItem(STORE_PREFIX + key, value); } catch(e) {}
2006}
2007
2008// Bump the visit counter once per pageload. Used by visitsBefore. In embed
2009// (popup-id) scope callers pass the popup id as a suffix so each standalone
2010// popup tracks its own visit count — a busy host page with two embedded popups
2011// must not let one popup's pageviews satisfy another's visitsBefore gate.
2012function recordVisit(suffix) {
2013  var key = "visits" + (suffix ? ":" + suffix : "");
2014  var prev = parseInt(lsGet(key) || "0", 10);
2015  if (!isFinite(prev) || prev < 0) prev = 0;
2016  lsSet(key, String(prev + 1));
2017  return prev + 1;
2018}
2019
2020function isEligible(cfg, popupId, currentVisit) {
2021  // visitsBefore: 0 = show on first visit. So eligible iff currentVisit > visitsBefore.
2022  if (cfg.frequency.visitsBefore > 0 && currentVisit <= cfg.frequency.visitsBefore) return false;
2023  // daysCooldown: don't reshow until N days have passed since last close.
2024  if (cfg.frequency.daysCooldown > 0) {
2025    var last = parseInt(lsGet("close:" + popupId) || "0", 10);
2026    if (last > 0) {
2027      var elapsedMs = Date.now() - last;
2028      if (elapsedMs < cfg.frequency.daysCooldown * 86400000) return false;
2029    }
2030  }
2031  return true;
2032}
2033
2034function composeRgba(color, opacity) {
2035  var hex = color.match(/^#([0-9a-f]{6})$/i);
2036  if (hex) {
2037    var n = parseInt(hex[1], 16);
2038    return "rgba(" + ((n >> 16) & 0xff) + "," + ((n >> 8) & 0xff) + "," + (n & 0xff) + "," + opacity + ")";
2039  }
2040  var rgb = color.match(/^rgb(?:a)?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
2041  if (rgb) return "rgba(" + rgb[1] + "," + rgb[2] + "," + rgb[3] + "," + opacity + ")";
2042  return "rgba(0,0,0," + opacity + ")";
2043}
2044
2045function animStart(kind) {
2046  if (kind === "slide-up")   return { opacity: 0, transform: "translate(-50%, -40%)" };
2047  if (kind === "slide-down") return { opacity: 0, transform: "translate(-50%, -60%)" };
2048  if (kind === "zoom")       return { opacity: 0, transform: "translate(-50%, -50%) scale(0.92)" };
2049  if (kind === "none")       return { opacity: 1, transform: "translate(-50%, -50%)" };
2050  return { opacity: 0, transform: "translate(-50%, -50%)" }; // fade
2051}
2052function animEnd() {
2053  return { opacity: 1, transform: "translate(-50%, -50%) scale(1)" };
2054}
2055
2056// Body scroll lock — refcounted because two popups can technically be in
2057// the open state at once (different triggers firing back-to-back). The
2058// first lock saves the body's prior overflow + padding-right and replaces
2059// them; subsequent locks just bump the count. We only restore once every
2060// popup that locked has closed, so a still-open popup keeps the page held.
2061// padding-right compensates for the scrollbar disappearing — without it,
2062// the dimmed page behind the overlay shifts right at the moment of open.
2063var _hpupLockCount = 0;
2064var _hpupSavedOverflow = "";
2065var _hpupSavedPaddingRight = "";
2066function lockBodyScroll() {
2067  if (_hpupLockCount === 0) {
2068    var scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
2069    _hpupSavedOverflow = document.body.style.overflow || "";
2070    _hpupSavedPaddingRight = document.body.style.paddingRight || "";
2071    if (scrollbarWidth > 0) document.body.style.paddingRight = scrollbarWidth + "px";
2072    document.body.style.overflow = "hidden";
2073  }
2074  _hpupLockCount++;
2075}
2076function unlockBodyScroll() {
2077  if (_hpupLockCount === 0) return;
2078  _hpupLockCount--;
2079  if (_hpupLockCount === 0) {
2080    if (_hpupSavedOverflow) document.body.style.overflow = _hpupSavedOverflow;
2081    else document.body.style.removeProperty("overflow");
2082    if (_hpupSavedPaddingRight) document.body.style.paddingRight = _hpupSavedPaddingRight;
2083    else document.body.style.removeProperty("padding-right");
2084  }
2085}
2086
2087// Build the overlay backdrop. Returns the element. Wired with the right
2088// pointer behaviour for the popup's clickToClose setting.
2089function buildOverlay(cfg, onOverlayClick) {
2090  var ov = document.createElement("div");
2091  ov.setAttribute("data-htmlpub-popup-backdrop", "");
2092  ov.style.cssText = "position:fixed;inset:0;background:" + composeRgba(cfg.overlay.color, cfg.overlay.opacity) +
2093    ";z-index:2147483646;opacity:0;transition:opacity " + cfg.animation.durationMs + "ms ease;";
2094  if (cfg.overlay.clickToClose) ov.addEventListener("click", onOverlayClick);
2095  return ov;
2096}
2097
2098// Inject a close ✕ button if the popup doesn't already have one. We mark our
2099// injected button so re-show calls don't duplicate it.
2100function ensureCloseButton(el, cfg, onClose) {
2101  if (el.querySelector("[data-htmlpub-popup-close-btn]")) return;
2102  if (!cfg.close.enabled) return;
2103  var btn = document.createElement("button");
2104  btn.setAttribute("type", "button");
2105  btn.setAttribute("aria-label", "Close pop-up");
2106  btn.setAttribute("data-htmlpub-popup-close-btn", "");
2107  var posStyle = cfg.close.position === "top-left" ? "left:12px;" : "right:12px;";
2108  btn.style.cssText = "position:absolute;top:12px;" + posStyle +
2109    "width:28px;height:28px;border-radius:9999px;border:0;background:rgba(0,0,0,0.45);color:#fff;" +
2110    "font:16px/1 system-ui,-apple-system,sans-serif;cursor:pointer;display:flex;align-items:center;" +
2111    "justify-content:center;z-index:1;padding:0;";
2112  btn.textContent = "×";
2113  btn.addEventListener("click", function(e){ e.preventDefault(); e.stopPropagation(); onClose(); });
2114  // Ensure the popup itself is a positioning context so absolute close button
2115  // anchors to it. Cheap: only sets if not already positioned.
2116  var pos = getComputedStyle(el).position;
2117  if (pos === "static" || !pos) el.style.position = "relative";
2118  el.appendChild(btn);
2119}
2120
2121// ── Per-popup wiring ──────────────────────────────────────────────────────
2122// Each popup gets its own controller closing over its element + config +
2123// trigger cleanup so we can tear down listeners on close.
2124
2125// HP-1557 — fire an analytics event for this popup. Hands off to the
2126// analytics.ts buffer via window.__hpupPushEvent (registered there). If
2127// analytics isn't loaded for some reason we silently no-op rather than
2128// breaking the popup runtime.
2129function emitPopupEvent(eventType, popup, extras) {
2130  if (typeof window.__hpupPushEvent !== "function") return;
2131  var payload = {
2132    popupId: popup.getAttribute("id") || "",
2133    popupName: (pAttr(popup, "-name") || "").replace(/^\s+|\s+$/g, "")
2134  };
2135  // Standalone-embed pop-ups carry their page id via data-hp-popup-cuid (set by
2136  // the embed bundle) so analytics can attribute to a specific pop-up; the DOM
2137  // id in popupId is a shared name-slug. In-page pop-ups have no cuid so this
2138  // stays absent.
2139  var _hpCuid = popup.getAttribute("data-hp-popup-cuid");
2140  if (_hpCuid) payload.popupCuid = _hpCuid;
2141  if (extras && typeof extras === "object") {
2142    for (var k in extras) {
2143      if (Object.prototype.hasOwnProperty.call(extras, k)) payload[k] = extras[k];
2144    }
2145  }
2146  window.__hpupPushEvent(eventType, payload);
2147}
2148
2149// Single-open invariant: only one popup may be visible at a time.
2150// show() force-closes whatever is up before painting itself.
2151var _hpupCurrentOpen = null;
2152
2153function wirePopup(el, currentVisit) {
2154  var cfg = readConfig(el);
2155  var popupId = el.getAttribute("id") || "";
2156  if (!popupId) return;
2157  // The editor stores popups hidden via inline display:none. We never strip
2158  // that — we toggle it at show/close time so a never-shown popup stays
2159  // invisible (matching its rendered state in the DOM).
2160  var originalDisplay = el.style.display || "";
2161  var isOpen = false;
2162  var overlay = null;
2163  var keyListener = null;
2164  var sizeObserver = null;
2165  var cleanups = []; // trigger listener removers; called on first show
2166
2167  function cleanupTriggers() {
2168    for (var i = 0; i < cleanups.length; i++) {
2169      try { cleanups[i](); } catch(e) {}
2170    }
2171    cleanups = [];
2172  }
2173
2174  // Why a popup was opened — informs the parent analytics event so the
2175  // dashboard can distinguish "fired by timer" vs. "explicit user click".
2176  // Updated by callers before show() and reset to null after emit.
2177  var lastShowReason = null;
2178
2179  // HP-4147 — the overflow choice in show() is a measurement, and measurements
2180  // go stale. Content can grow AFTER open: an inline checkout's Stripe Payment
2181  // Element progressively reveals card fields and then a "Save your info"
2182  // section as the buyer types. A popup that opened short (overflow:visible, so
2183  // its shadow paints) and then grows past maxHeight spills below the viewport
2184  // instead of scrolling, and because show() locked the body scroll there is
2185  // nothing left for a touch drag to move — the Pay button becomes unreachable
2186  // and closing + reopening is the only way out (reopening re-runs show(), which
2187  // re-measures against the now-tall content and lands on "auto"). So re-decide
2188  // on every resize — but only ever upgrade to "auto", never back to "visible".
2189  //
2190  // One-way on purpose. Reverting to "visible" when content shrinks back under
2191  // maxHeight sounds harmless (it hands HP-3771 its shadow back) but it
2192  // re-releases horizontal spill that "auto" had been containing, so a wide
2193  // child paints outside the popup over the page, and it discards scrollTop,
2194  // bouncing the buyer to the top of the form. Both were measured in Chromium
2195  // at a Pixel viewport. Staying in "auto" costs only the shadow, and only for
2196  // the rest of an open in which content already exceeded the viewport: "auto"
2197  // paints no scrollbar once content fits again, it merely clips. close()
2198  // strips every inline style, so the next open re-decides from scratch.
2199  function syncOverflow() {
2200    if (!isOpen) return;
2201    // Guarded on a real change. Toggling overflow lays out a scrollbar, which
2202    // resizes content, which re-enters this observer — an unguarded write there
2203    // is a feedback loop that never settles.
2204    if (el.scrollHeight > el.clientHeight + 1 && el.style.overflow !== "auto") {
2205      el.style.overflow = "auto";
2206    }
2207  }
2208
2209  function observeSize() {
2210    if (sizeObserver || typeof ResizeObserver === "undefined") return;
2211    sizeObserver = new ResizeObserver(syncOverflow);
2212    // Observing el catches the growth that crosses maxHeight, since its own box
2213    // tracks content right up to the clamp. Observing its element children
2214    // catches growth that continues past the clamp, where el's box is pinned and
2215    // reports no resize at all — content 1px under maxHeight that then expands
2216    // would otherwise never re-decide.
2217    sizeObserver.observe(el);
2218    for (var childIndex = 0; childIndex < el.children.length; childIndex++) {
2219      sizeObserver.observe(el.children[childIndex]);
2220    }
2221  }
2222
2223  function unobserveSize() {
2224    if (!sizeObserver) return;
2225    sizeObserver.disconnect();
2226    sizeObserver = null;
2227  }
2228
2229  function show(reason) {
2230    if (isOpen) return;
2231    if (_hpupCurrentOpen && _hpupCurrentOpen !== el && typeof _hpupCurrentOpen._hpupClose === "function") {
2232      _hpupCurrentOpen._hpupClose("replaced", true);
2233    }
2234    _hpupCurrentOpen = el;
2235    isOpen = true;
2236    lastShowReason = reason || "explicit";
2237    cleanupTriggers();
2238    lockBodyScroll();
2239    // chrome:none — the content paints its own ✕ (Classic's .js-close, inside the iframe).
2240    var bare = cfg.chrome === "none";
2241    if (!bare) ensureCloseButton(el, cfg, function(){ close("close_button"); });
2242
2243    var start = animStart(cfg.animation.open);
2244    var end   = animEnd();
2245
2246    // Centered fixed positioning with a high z-index so we float above any
2247    // page content + checkout / chat widgets.
2248    el.style.display      = "block";
2249    el.style.position     = "fixed";
2250    el.style.zIndex       = "2147483647";
2251
2252    if (bare) {
2253      // Full-viewport shell. An <iframe> has no intrinsic content height (~150px, and it never
2254      // reports its content size to the parent), so the shrink-wrapped-card model below cannot
2255      // measure it — centring, the maxHeight/maxWidth clamps and the overflow logic all assume a
2256      // box that sizes to its content. Reproduce CDA's model instead: the frame fills the
2257      // viewport and the Classic document inside owns mask, centring and card.
2258      //
2259      // No transform either: every animStart transform carries translate(-50%,-50%), which
2260      // centres a card and would shove a full-viewport shell off screen. Opacity alone.
2261      el.style.top        = "0";
2262      el.style.left       = "0";
2263      el.style.width      = "100%";
2264      el.style.height     = "100%";
2265      el.style.opacity    = String(start.opacity);
2266      el.style.transition = "opacity " + cfg.animation.durationMs + "ms ease";
2267      openClassicFrame(el);
2268    } else {
2269    el.style.top          = "50%";
2270    el.style.left         = "50%";
2271    el.style.transform    = start.transform;
2272    el.style.opacity      = String(start.opacity);
2273    el.style.maxHeight    = "calc(100vh - 32px)";
2274    el.style.maxWidth     = "calc(100vw - 32px)";
2275    // HP-3771 — "auto" clips any box-shadow set on the popup's own content:
2276    // box-shadow doesn't affect layout size, so this element shrink-wraps to its
2277    // content and its own overflow then clips the shadow it should cast.
2278    //
2279    // But "visible" unconditionally is not safe either. This element is
2280    // position:fixed and centred, so content taller than maxHeight does not
2281    // clip — it spills past the viewport with no scrollbar and nothing can
2282    // reach it. A lead-capture form taller than the viewport would lose its
2283    // submit button. So decide per popup, after layout: overflow stays "auto"
2284    // only when the content genuinely overflows (scrolling wins), and is
2285    // "visible" otherwise (shadow wins). Short popups are the overwhelmingly
2286    // common case, so the shadow renders for almost everyone.
2287    //
2288    // syncOverflow re-applies this same rule for the rest of the open (HP-4147).
2289    el.style.overflow     = "visible";
2290    syncOverflow();
2291    el.style.transition   = "opacity " + cfg.animation.durationMs + "ms ease, transform " + cfg.animation.durationMs + "ms ease";
2292    }
2293
2294    if (!el.parentNode) document.body.appendChild(el);
2295    if (!bare) {
2296      // Backdrop. Inserted before the popup so the popup naturally paints on top.
2297      // chrome:none skips it — the Classic document paints its own .js-leadbox-mask, and a second
2298      // backdrop would darken the page twice.
2299      overlay = buildOverlay(cfg, function(){ close("overlay_click"); });
2300      (el.parentNode || document.body).insertBefore(overlay, el);
2301
2302      // Started only once el is attached, so the observer's first delivery measures
2303      // real geometry rather than the 0x0 of a detached node.
2304      observeSize();
2305    }
2306
2307    // Force reflow so the next style change animates from start, not jumps.
2308    /* eslint-disable-next-line no-unused-expressions */ void el.offsetWidth;
2309
2310    el.style.opacity   = String(end.opacity);
2311    if (!bare) el.style.transform = end.transform;
2312    if (overlay) overlay.style.opacity = "1";
2313
2314    if (cfg.close.escapeToClose) {
2315      keyListener = function(e) { if (e.key === "Escape") close("escape"); };
2316      document.addEventListener("keydown", keyListener);
2317    }
2318
2319    emitPopupEvent("popup_view", el, { reason: lastShowReason });
2320    lastShowReason = null;
2321  }
2322
2323  function close(reason, hard) {
2324    if (!isOpen) return;
2325    isOpen = false;
2326    emitPopupEvent("popup_dismiss", el, { reason: reason || "unknown" });
2327    var start = animStart(cfg.animation.close);
2328    el.style.opacity   = String(start.opacity);
2329    // Same reason as show(): the close transform centres a card, and applying it to a
2330    // full-viewport shell would slide it off screen instead of fading it out.
2331    if (cfg.chrome !== "none") el.style.transform = start.transform;
2332    if (overlay) overlay.style.opacity = "0";
2333
2334    if (keyListener) {
2335      document.removeEventListener("keydown", keyListener);
2336      keyListener = null;
2337    }
2338
2339    var afterAnim = function() {
2340      if (_hpupCurrentOpen === el) _hpupCurrentOpen = null;
2341      unobserveSize();
2342      el.style.display = originalDisplay || "none";
2343      el.style.removeProperty("position");
2344      el.style.removeProperty("top");
2345      el.style.removeProperty("left");
2346      el.style.removeProperty("transform");
2347      el.style.removeProperty("opacity");
2348      el.style.removeProperty("z-index");
2349      el.style.removeProperty("max-height");
2350      el.style.removeProperty("max-width");
2351      // Only the chrome:none branch sets these, and teardown must remove exactly what this show
2352      // set — a marker can legitimately carry an author's own inline width/height, and stripping
2353      // it on close would resize the pop-up the second time it opens.
2354      if (cfg.chrome === "none") {
2355        el.style.removeProperty("width");
2356        el.style.removeProperty("height");
2357      }
2358      el.style.removeProperty("overflow");
2359      el.style.removeProperty("transition");
2360      if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay);
2361      overlay = null;
2362      // Tear down the close button — its click handler closes over THIS
2363      // show's overlay reference. Leaving it across opens means a second
2364      // show's X would call the previous close() with a stale overlay and
2365      // leave the current overlay floating.
2366      var existingBtn = el.querySelector("[data-htmlpub-popup-close-btn]");
2367      if (existingBtn && existingBtn.parentNode) existingBtn.parentNode.removeChild(existingBtn);
2368      // Release the scroll lock only after the popup is fully torn down so
2369      // the page doesn't pop back to scrollable while the close animation
2370      // is still painting.
2371      unlockBodyScroll();
2372      lsSet("close:" + popupId, String(Date.now()));
2373    };
2374    if (hard || cfg.animation.close === "none") afterAnim();
2375    else setTimeout(afterAnim, cfg.animation.durationMs);
2376  }
2377
2378  // ── Triggers ────────────────────────────────────────────────────────────
2379  // Only wire if the popup is eligible to show this pageload (frequency
2380  // rules). Click-to-open via [data-htmlpub-popup-target] always works,
2381  // regardless of eligibility — it's an explicit user action.
2382  var eligible = isEligible(cfg, popupId, currentVisit);
2383
2384  if (eligible) {
2385    var t = cfg.triggers || {};
2386
2387    if (t.timed && typeof t.timed.delaySec === "number") {
2388      var delayMs = Math.max(0, t.timed.delaySec) * 1000;
2389      var timer = setTimeout(function(){ show("timed"); }, delayMs);
2390      cleanups.push(function(){ clearTimeout(timer); });
2391    }
2392
2393    if (t.exitIntent === true) {
2394      // Mobile / touch devices don't have a meaningful "exit toward chrome"
2395      // gesture. Standard heuristic: skip if no mouse pointer is available.
2396      if (!matchMedia("(pointer: coarse)").matches) {
2397        var onMove = function(e) {
2398          if (e.clientY < 8) show("exit_intent");
2399        };
2400        document.addEventListener("mousemove", onMove);
2401        cleanups.push(function(){ document.removeEventListener("mousemove", onMove); });
2402      }
2403    }
2404
2405    if (typeof t.scrollPercent === "number" && t.scrollPercent > 0) {
2406      var threshold = Math.max(1, Math.min(100, t.scrollPercent));
2407      var onScroll = function() {
2408        var doc = document.documentElement;
2409        var scrolled = (window.scrollY || doc.scrollTop || 0);
2410        var max = (doc.scrollHeight - doc.clientHeight) || 1;
2411        var pct = (scrolled / max) * 100;
2412        if (pct >= threshold) show("scroll");
2413      };
2414      window.addEventListener("scroll", onScroll, { passive: true });
2415      cleanups.push(function(){ window.removeEventListener("scroll", onScroll); });
2416    }
2417  }
2418
2419  // Note: in-popup form submits and CTA clicks are NOT emitted as separate
2420  // popup_* events. They already fire as form_submit / element_click via
2421  // analytics.ts's global listeners, which now attach popupId + popupName
2422  // to the payload so customers can still see which pop-up the conversion
2423  // came from while continuing to track the standard goal types.
2424
2425  // Expose the show function on the element so [data-htmlpub-popup-target]
2426  // clicks (wired globally below) can find and trigger it without re-parsing
2427  // the config. _hpupShow / _hpupClose are deliberately namespaced + ugly to
2428  // avoid colliding with user code.
2429  el._hpupShow = show;
2430  el._hpupClose = close;
2431  // Returned so boot() can flush unfired trigger listeners on pagehide.
2432  return cleanupTriggers;
2433}
2434
2435// A served Classic leadbox renders INACTIVE: its controller only adds js-active to the dialog
2436// and mask when it receives an "open" message, so a frame nobody tells to open is a full-viewport
2437// sheet showing nothing.
2438//
2439// TWO WIRE FORMATS, because there are two leadbox builds in the corpus and they do NOT agree —
2440// measured across all 462 live clones on 2026-09-09, not inferred from one of them (D71):
2441//
2442//   · newer build (410 of 462): a handleMessage controller that accepts EITHER the bare string
2443//     "open" or {type:"open"}.
2444//   · OLDER build (52 of 462): a hand-rolled listener whose whole test is
2445//         window.addEventListener('message', function(e){ e && e.data && 'open' === e.data && j.open() })
2446//     read verbatim out of the served bundle. The test 'open' === e.data is a STRING identity check, so
2447//     an object payload fails it silently and that pop-up never opens.
2448//
2449// This function used to post ONLY {type:"open"}, and the comment here used to assert both builds
2450// took it. They do not: every old-build pop-up rendered a full-viewport dark sheet with an
2451// invisible modal — no error, no console message, and zero lead capture. So post BOTH forms. The
2452// duplicate is harmless in each direction: the old build ignores the object, and the newer one's
2453// open() just re-adds the class and refocuses.
2454//
2455// The string form is also what Classic's own embed.js sent, which is why the defect stayed hidden
2456// on the two pages that still carried embed.js — its copy opened correctly beside our blank one.
2457// (The close relay below had this right already: it reads Classic's bare-string "close leadbox"
2458// off the wire. Open is the direction that was guessed.)
2459//
2460// Posted on both paths because the ordering is not ours to control: if the frame is already
2461// loaded the immediate post lands, and if it is not, the load listener catches it. Opening twice
2462// is harmless — open() just re-adds the class and refocuses.
2463function openClassicFrame(el) {
2464  var frame = el.querySelector("iframe");
2465  if (!frame) return;
2466  var post = function() {
2467    // targetOrigin "*": the frame is served sandboxed, so its origin is opaque and cannot be
2468    // named. The payload is a bare open signal carrying nothing worth protecting.
2469    try {
2470      if (!frame.contentWindow) return;
2471      // String FIRST: it is the format the old build needs, and the one that was missing.
2472      frame.contentWindow.postMessage("open", "*");
2473      frame.contentWindow.postMessage({ type: "open" }, "*");
2474    } catch(e) {}
2475  };
2476  post();
2477  frame.addEventListener("load", post);
2478}
2479
2480// Global close relay. A chrome:none pop-up's ✕ lives INSIDE its iframe (Classic's .js-close), so
2481// the click is invisible to this document — the frame posts htmlpub:leadbox-close instead and we
2482// close the marker holding it. Without this the Classic card hides itself and the full-viewport
2483// iframe stays put, covering the page with an invisible sheet that swallows every click.
2484//
2485// Identify the marker by matching iframe.contentWindow against e.source, NOT by the message's
2486// uuid: a page can carry several migrated pop-ups, and a uuid can be placed more than once. This
2487// is the technique CDA's own injected handler used.
2488function handleCloseRelay(e) {
2489  if (!e || !e.data) return;
2490  // Classic's own leadbox controller posts the bare STRING "close leadbox" to window.parent when
2491  // it closes (300ms after the fade, from its close() method) — read out of the served bundle, not
2492  // guessed. It is already the wire format, so nothing needs injecting into the frame. The
2493  // namespaced form is accepted too, for anything we serve ourselves later.
2494  var type = typeof e.data === "string" ? e.data : e.data.type;
2495  if (type !== "close leadbox" && type !== "htmlpub:leadbox-close") return;
2496  var frames = document.querySelectorAll("[data-persist-popup] iframe, [data-htmlpub-popup] iframe");
2497  for (var i = 0; i < frames.length; i++) {
2498    if (frames[i].contentWindow !== e.source) continue;
2499    var marker = frames[i].closest("[data-persist-popup], [data-htmlpub-popup]");
2500    if (marker && typeof marker._hpupClose === "function") marker._hpupClose("close_relay");
2501    return;
2502  }
2503}
2504
2505// The migrated pop-up runs in a sandboxed cross-origin iframe and cannot read this page's anal
2505ytics
2506// session/visitor ids (opaque origin blocks its storage). Hand them over when the in-frame capture
2507// script asks — matching e.source to one of our own pop-up iframes so we only answer our own frames.
2508// The ids exist only once analytics consent has minted them, so with no consent nothing is sent and
2509// the ingest keeps the lead but skips the conversion event (there is no page_view session to join).
2510// The page id rides along either way, so the lead files under the page that was served, whatever
2511// URL the browser reports.
2512function handleSessionReq(e) {
2513  if (!e || !e.data || e.data.type !== "hp-popup-session-req" || !e.source) return;
2514  var frames = document.querySelectorAll("[data-persist-popup] iframe, [data-htmlpub-popup] iframe");
2515  var mine = false;
2516  for (var i = 0; i < frames.length; i++) { if (frames[i].contentWindow === e.source) { mine = true; break; } }
2517  if (!mine) return;
2518  var sid = null, vid = null;
2519  try { sid = sessionStorage.getItem("_hp_sid"); } catch(err) {}
2520  try { vid = localStorage.getItem("_hp_vid"); } catch(err) {}
2521  try { e.source.postMessage({ type: "hp-popup-session", sid: sid, vid: vid, pid: PAGE_ID }, "*"); } catch(err) {}
2522}
2523
2524// A migrated pop-up's thank-you redirect. Its frame is sandboxed with no top-navigation grant, so
2525// after capturing the lead its script asks this page to navigate. Only the clone frame the page
2526// rewrite emits may ask (an /api/serve-classic-popup/ src, absolute when the WordPress plugin
2527// re-serves the page) — not any iframe a pop-up holds, or a third-party embed could send the visitor
2528// anywhere. The src is the page's own markup, which the framed party cannot change. Only an absolute
2529// http(s) URL is followed. The
2530// ack tells the frame not to fall back to opening the page inside itself. In-page only: the embed
2531// bundle hosts native pop-ups on third-party sites, never a migrated clone.
2532function handleRedirectRelay(e) {
2533  if (!e || !e.data || e.data.type !== "hp-popup-redirect" || !e.source) return;
2534  var url = typeof e.data.url === "string" ? e.data.url : "";
2535  try { var proto = new URL(url).protocol; if (proto !== "http:" && proto !== "https:") return; } catch(err) { return; }
2536  var frames = document.querySelectorAll("[data-persist-popup] iframe, [data-htmlpub-popup] iframe");
2537  var mine = false;
2538  for (var i = 0; i < frames.length; i++) {
2539    if (frames[i].contentWindow !== e.source) continue;
2540    try { mine = new URL(frames[i].getAttribute("src") || "", location.href).pathname.indexOf("/api/serve-classic-popup/") === 0; } catch(err) {}
2541    break;
2542  }
2543  if (!mine) return;
2544  try { e.source.postMessage({ type: "hp-popup-redirect-ack" }, "*"); } catch(err) {}
2545  window.location.href = url;
2546}
2547
2548// Global click-to-open dispatcher. Targets carry [data-persist-popup-target]
2549// (or legacy [data-htmlpub-popup-target]) or href="#popup-…" (for plain anchors
2550// that the link picker auto-generated).
2551function handleTargetClick(e) {
2552  var t = e.target;
2553  if (!t || !t.closest) return;
2554  var hit = t.closest("[data-persist-popup-target], [data-htmlpub-popup-target], a[href^='#popup-'], a[href^='#modal-'], a[href^='#dialog-']");
2555  if (!hit) return;
2556  var id = pAttr(hit, "-target");
2557  if (!id) {
2558    var href = hit.getAttribute("href") || "";
2559    if (href.charAt(0) === "#") id = href.slice(1);
2560  }
2561  if (!id) return;
2562  var popup = document.getElementById(id);
2563  if (!popup || pAttr(popup, "") === null) return;
2564  e.preventDefault();
2565  if (typeof popup._hpupShow === "function") popup._hpupShow("click_target");
2566}
2567
2568// ── Boot ──────────────────────────────────────────────────────────────────
2569
2570function flushTeardowns(list) {
2571  for (var j = 0; j < list.length; j++) {
2572    try { list[j](); } catch(e) {}
2573  }
2574}
2575
2576function boot() {
2577  if (SCOPE === "popup-id") {
2578    // Embed mode: each standalone popup ships its OWN copy of this runtime, so
2579    // a single global boot guard would make a second embedded popup bail. Wire
2580    // each popup once via a per-element marker, count visits per popup id, and
2581    // attach the global click / pagehide listeners only once across copies.
2582    var list = document.querySelectorAll("[data-persist-popup],[data-htmlpub-popup]");
2583    if (!list.length) return;
2584    if (!window.__hpupTeardowns) window.__hpupTeardowns = [];
2585    for (var n = 0; n < list.length; n++) {
2586      var pel = list[n];
2587      if (pel.__hpupWired) continue;
2588      pel.__hpupWired = true;
2589      var pt = wirePopup(pel, recordVisit(pel.getAttribute("id") || ""));
2590      if (typeof pt === "function") window.__hpupTeardowns.push(pt);
2591    }
2592    if (!window.__hpupClickWired) {
2593      window.__hpupClickWired = true;
2594      document.addEventListener("click", handleTargetClick, false);
2595      window.addEventListener("message", handleCloseRelay, false);
2596      window.addEventListener("message", handleSessionReq, false);
2597      window.addEventListener("pagehide", function(){ flushTeardowns(window.__hpupTeardowns || []); });
2598    }
2599    return;
2600  }
2601
2602  // In-page mode: one runtime per page wires every popup. Guard against
2603  // double-injection (HMR, accidental double-include).
2604  if (window.__hpupBooted) return;
2605  window.__hpupBooted = true;
2606  var popups = document.querySelectorAll("[data-persist-popup],[data-htmlpub-popup]");
2607  if (!popups.length) return;
2608  var visit = recordVisit();
2609  var teardowns = [];
2610  for (var i = 0; i < popups.length; i++) {
2611    var t = wirePopup(popups[i], visit);
2612    if (typeof t === "function") teardowns.push(t);
2613  }
2614  document.addEventListener("click", handleTargetClick, false);
2615  window.addEventListener("message", handleCloseRelay, false);
2616  window.addEventListener("message", handleSessionReq, false);
2617  window.addEventListener("message", handleRedirectRelay, false);
2618  // Flush unfired trigger listeners on pagehide.
2619  window.addEventListener("pagehide", function(){ flushTeardowns(teardowns); });
2620}
2621
2622if (document.readyState === "loading") {
2623  document.addEventListener("DOMContentLoaded", boot);
2624} else {
2625  boot();
2626}
2627
2628})();
2629</script>
2629<script data-htmlpub-countdown-runtime>
2630(function(){
2631var nodes=document.querySelectorAll('[data-htmlpub-countdown]');
2632if(!nodes.length)return;
2633function pad(n){n=String(n);return n.length<2?'0'+n:n;}
2634function setDigit(root,sel,val){var el=root.querySelector(sel);if(el)el.textContent=pad(val);}
2635function tick(node,endMs){
2636var diff=Math.max(0,endMs-Date.now());
2637var s=Math.floor(diff/1000);
2638var d=Math.floor(s/86400);s-=d*86400;
2639var h=Math.floor(s/3600);s-=h*3600;
2640var m=Math.floor(s/60);s-=m*60;
2641setDigit(node,'.countdown-days',d);
2642setDigit(node,'.countdown-hours',h);
2643setDigit(node,'.countdown-minutes',m);
2644setDigit(node,'.countdown-seconds',s);
2645if(diff===0){
2646var redirect=node.getAttribute('data-redirect');
2647var inFrame=false;try{inFrame=window!==window.top;}catch(e){inFrame=true;}
2648if(redirect&&!inFrame){try{window.location.href=redirect;}catch(e){}}
2649return true;
2650}
2651return false;
2652}
2653function endMsFor(node){
2654var mode=node.getAttribute('data-htmlpub-countdown');
2655if(mode==='fixed'){
2656var target=node.getAttribute('data-target');
2657var t=target?Date.parse(target):NaN;
2658return isNaN(t)?0:t;
2659}
2660if(mode==='evergreen'){
2661var dur=parseInt(node.getAttribute('data-duration')||'0',10);
2662if(!dur)return 0;
2663var idx=Array.prototype.indexOf.call(nodes,node);
2664var base='htmlpub_cd_'+(node.getAttribute('data-persist-countdown-id')||node.id||(location.pathname+'|'+dur+'|'+idx));
2665var ver=parseInt(node.getAttribute('data-persist-countdown-reset')||'0',10)||0;
2666var key=ver>0?base+'|v'+ver:base;
2667if(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){}}
2668var first=null;
2669try{first=localStorage.getItem(key);}catch(e){}
2670if(!first){first=String(Date.now());try{localStorage.setItem(key,first);}catch(e){}}
2671return parseInt(first,10)+dur*1000;
2672}
2673return 0;
2674}
2675for(var i=0;i<nodes.length;i++){(function(node){
2676var endMs=endMsFor(node);
2677if(!endMs)return;
2678if(tick(node,endMs))return;
2679var iv=setInterval(function(){if(tick(node,endMs))clearInterval(iv);},1000);
2680})(nodes[i]);}
2681})();
2682</script>
2682<script data-text-anim-runtime>
2683(function(){
2684var nodes=document.querySelectorAll('[data-persist-text-anim]');
2685if(!nodes.length)return;
2686try{if(window.matchMedia('(prefers-reduced-motion: reduce)').matches)return;}catch(e){}
2687var KEYFRAMES={
2688fadeIn:[{opacity:0},{opacity:1}],
2689slideUp:[{opacity:0,transform:'translateY(32px)'},{opacity:1,transform:'translateY(0)'}],
2690slideDown:[{opacity:0,transform:'translateY(-32px)'},{opacity:1,transform:'translateY(0)'}],
2691slideLeft:[{opacity:0,transform:'translateX(32px)'},{opacity:1,transform:'translateX(0)'}],
2692slideRight:[{opacity:0,transform:'translateX(-32px)'},{opacity:1,transform:'translateX(0)'}],
2693zoomIn:[{opacity:0,transform:'scale(0.85)'},{opacity:1,transform:'scale(1)'}],
2694blurIn:[{opacity:0,filter:'blur(8px)'},{opacity:1,filter:'blur(0)'}],
2695bounceIn:[{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)'}]
2696};
2697var DEFAULTS={duration:600,delay:0,easing:'ease-out'};
2698function readInt(el,attr,fallback,min){
2699var n=parseInt(el.getAttribute(attr)||'',10);
2700if(!isFinite(n)||n<min)return fallback;
2701return n;
2702}
2703function arm(el){
2704var type=el.getAttribute('data-persist-text-anim')||'fadeIn';
2705var frames=KEYFRAMES[type];
2706if(!frames)return null;
2707var duration=readInt(el,'data-persist-text-anim-duration',DEFAULTS.duration,1);
2708var delay=readInt(el,'data-persist-text-anim-delay',DEFAULTS.delay,0);
2709var easing=el.getAttribute('data-persist-text-anim-easing')||DEFAULTS.easing;
2710try{
2711var anim=el.animate(frames,{duration:duration,delay:delay,easing:easing,fill:'both'});
2712anim.pause();
2713anim.currentTime=0;
2714return anim;
2715}catch(e){return null;}
2716}
2717if('IntersectionObserver' in window){
2718var io=new IntersectionObserver(function(entries){
2719for(var i=0;i<entries.length;i++){
2720var en=entries[i];
2721if(!en.isIntersecting)continue;
2722io.unobserve(en.target);
2723var armed=en.target._hpTextAnim;
2724if(!armed)continue;
2725en.target._hpTextAnim=null;
2726try{armed.play();}catch(e){}
2727}
2728},{threshold:0.15});
2729for(var j=0;j<nodes.length;j++){
2730var pending=arm(nodes[j]);
2731if(!pending)continue;
2732nodes[j]._hpTextAnim=pending;
2733io.observe(nodes[j]);
2734}
2735}else{
2736for(var k=0;k<nodes.length;k++){
2737var immediate=arm(nodes[k]);
2738if(immediate)try{immediate.play();}catch(e){}
2739}
2740}
2741})();
2742</script>
2742<script data-htmlpub-nav>
2743(function(){
2744"use strict";
2745
2746function closeAllSubmenus(except) {
2747  var panels = document.querySelectorAll('[data-persist-nav-submenu-panel]');
2748  for (var i = 0; i < panels.length; i++) {
2749    var panel = panels[i];
2750    if (panel === except) continue;
2751    panel.classList.remove('is-open');
2752    var trigger = panel.__hpNavTrigger;
2753    if (trigger) trigger.setAttribute('aria-expanded', 'false');
2754  }
2755}
2756
2757function wireSubmenus() {
2758  var triggers = document.querySelectorAll('[data-persist-nav-submenu]');
2759  if (!triggers.length) return;
2760  for (var i = 0; i < triggers.length; i++) {
2761    (function(trigger) {
2762      if (trigger.__hpNavWired) return;
2763      trigger.__hpNavWired = true;
2764      var panelId = trigger.getAttribute('data-persist-nav-submenu');
2765      var panel = panelId ? document.getElementById(panelId) : (trigger.parentElement ? trigger.parentElement.querySelector('[data-persist-nav-submenu-panel]') : null);
2766      if (!panel) return;
2767      panel.__hpNavTrigger = trigger;
2768      trigger.setAttribute('aria-haspopup', 'true');
2769      trigger.setAttribute('aria-expanded', 'false');
2770      trigger.addEventListener('click', function(e) {
2771        var isOpen = panel.classList.contains('is-open');
2772        e.preventDefault();
2773        e.stopPropagation();
2774        closeAllSubmenus(isOpen ? null : panel);
2775        panel.classList.toggle('is-open', !isOpen);
2776        trigger.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
2777      });
2778    })(triggers[i]);
2779  }
2780  document.addEventListener('click', function(e) {
2781    var t = e.target;
2782    if (t && t.closest && t.closest('[data-persist-nav-submenu-panel], [data-persist-nav-submenu]')) return;
2783    closeAllSubmenus(null);
2784  });
2785}
2786
2787function wireMobileMenu() {
2788  var toggles = document.querySelectorAll('[data-persist-nav-mobile-toggle], [data-persist-nav-tablet-toggle]');
2789  if (!toggles.length) return;
2790  for (var i = 0; i < toggles.length; i++) {
2791    (function(toggle) {
2792      if (toggle.__hpNavWired) return;
2793      toggle.__hpNavWired = true;
2794      var isTablet = toggle.hasAttribute('data-persist-nav-tablet-toggle');
2795      var targetId = isTablet ? toggle.getAttribute('data-persist-nav-tablet-toggle') : toggle.getAttribute('data-persist-nav-mobile-toggle');
2796      var panelSelector = isTablet ? '[data-persist-nav-tablet-panel]' : '[data-persist-nav-mobile-panel]';
2797      var panel = targetId ? document.getElementById(targetId) : (toggle.parentElement ? toggle.parentElement.querySelector(panelSelector) : null);
2798      if (!panel) return;
2799      toggle.setAttribute('aria-expanded', 'false');
2800      toggle.addEventListener('click', function(e) {
2801        var isOpen = panel.classList.contains('is-open');
2802        e.preventDefault();
2803        e.stopPropagation();
2804        if (isOpen) panel.classList.remove('is-open'); else panel.classList.add('is-open');
2805        toggle.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
2806      });
2807    })(toggles[i]);
2808  }
2809}
2810
2811function wireScrollBackground() {
2812  // Every nav this system manages, not just ones with scroll-bg/blur
2813  // currently set — a page published while an OLDER build still applied
2814  // blur directly to nav carries that inline backdrop-filter baked into
2815  // its served HTML even after blur is later turned off, so the self-heal
2816  // below has to run regardless of the CURRENT attribute state.
2817  var navs = document.querySelectorAll('[data-persist-nav-id]');
2818  if (!navs.length) return;
2819  for (var i = 0; i < navs.length; i++) {
2820    (function(nav) {
2821      // Self-heal: strip a stale inline backdrop-filter baked into a page
2822      // saved before this fix (HP-3558 round 15 follow-up) — see the
2823      // dedicated blur-layer comment below for why it must never live on
2824      // nav itself.
2825      if (nav.style.getPropertyValue('backdrop-filter') || nav.style.getPropertyValue('-webkit-backdrop-filter')) {
2826        nav.style.removeProperty('backdrop-filter');
2827        nav.style.removeProperty('-webkit-backdrop-filter');
2828      }
2829      // "Transparent" (data-persist-nav-always-transparent, HP-3558 round
2830      // 16) — the non-sticky counterpart of scroll-bg above. "Useful over a
2831      // hero image" only holds if nav visually overlaps whatever follows
2832      // it, so lift it out of flow the same way Sticky's own
2833      // position:fixed/sticky already does, rather than leaving it
2834      // position:static (which just reveals blank page background above
2835      // the hero, not the hero itself — see iframe-script/utils.ts's
2836      // ensureNavScrollBackground for the matching editor-side comment).
2837      var alwaysTransparent = nav.hasAttribute('data-persist-nav-always-transparent');
2838      if (alwaysTransparent) {
2839        var navCsForOverlay = window.getComputedStyle(nav);
2840        if (!navCsForOverlay || (navCsForOverlay.position !== 'fixed' && navCsForOverlay.position !== 'sticky')) {
2841          nav.style.position = 'absolute';
2842          nav.style.top = '0px';
2843          nav.style.left = '0px';
2844          nav.style.right = '0px';
2845          if (!nav.style.zIndex) nav.style.zIndex = '1000';
2846        }
2847      }
2848      // Blur needs no scroll-reactivity — a blurred OPAQUE background
2849      // looks identical to an unblurred one, so it's simplest and correct
2850      // to apply it once, unconditionally, regardless of scroll state.
2851      // Applied to a dedicated decorative layer BEHIND nav's real
2852      // content, never to nav itself — backdrop-filter promotes whatever
2853      // element carries it into a new containing block for any
2854      // position:fixed descendant, which silently shrank the full-screen/
2855      // bottom-drawer hamburger panel (both position:fixed) down to nav's
2856      // own small box instead of the viewport when it lived on nav
2857      // directly (HP-3558 round 15 follow-up: "the full-screen hamburger
2858      // menu... ended up being covered by the next section").
2859      var blur = nav.getAttribute('data-persist-nav-scroll-blur');
2860      if (blur) {
2861        var blurLayer = nav.querySelector('[data-persist-nav-scroll-blur-layer]');
2862        if (!blurLayer) {
2863          blurLayer = document.createElement('div');
2864          blurLayer.setAttribute('data-persist-nav-scroll-blur-layer', '');
2865          // z-index:-1 required — without it, backdrop-filter's own
2866          // compositing samples nav's LATER siblings (logo/links/CTA) too,
2867          // visibly blurring them despite painting after this layer in
2868          // plain DOM order. Safe from escaping behind nav's own box into
2869          // the page below because Sticky header always sets an explicit,
2870          // non-auto nav.style.zIndex — position:fixed/sticky + a non-auto
2871          // z-index already makes nav its own stacking context.
2872          blurLayer.style.cssText = 'position:absolute;inset:0;pointer-events:none;z-index:-1;';
2873          nav.insertBefore(blurLayer, nav.firstChild);
2874          var navCs = window.getComputedStyle(nav);
2875          if (!navCs || navCs.position === 'static' || !navCs.position) nav.style.position = 'relative';
2876        }
2877        // Self-heal a layer saved before the z-index fix landed (see
2878        // iframe-script/utils.ts's ensureNavScrollBackground for the
2879        // matching editor-side comment).
2880        if (blurLayer.style.zIndex !== '-1') blurLayer.style.zIndex = '-1';
2881        blurLayer.style.setProperty('backdrop-filter', 'blur(' + blur + ')', 'important');
2882        blurLayer.style.setProperty('-webkit-backdrop-filter', 'blur(' + blur + ')', 'important');
2883      }
2884      if (alwaysTransparent) {
2885        nav.style.setProperty('background-color', 'transparent', 'important');
2886        return;
2887      }
2888      var color = nav.getAttribute('data-persist-nav-scroll-bg');
2889      if (!color || nav.__hpNavScrollWired) return;
2890      nav.__hpNavScrollWired = true;
2891      function sync() {
2892        nav.style.setProperty('background-color', window.scrollY > 24 ? color : 'transparent', 'important');
2893      }
2894      window.addEventListener('scroll', sync, { passive: true });
2895      sync();
2896    })(navs[i]);
2897  }
2898}
2899
2900function boot() {
2901  wireSubmenus();
2902  wireMobileMenu();
2903  wireScrollBackground();
2904}
2905
2906if (document.readyState === "loading") {
2907  document.addEventListener("DOMContentLoaded", boot);
2908} else {
2909  boot();
2910}
2911
2912})();
2913</script>
2913<script data-htmlpub-dtr>
2914(function(){
2915if(false)return;
2916var params=new URLSearchParams(location.search);
2917if(!params.toString())return;
2918function run(){
2919var MAX=500;
2920var replacements=[];
2921function strip(s){return s.replace(/<[^>]*>/g,"");}
2922function clean(s){try{s=decodeURIComponent(s);}catch(e){}s=strip(s);if(s.length>MAX)s=s.slice(0,MAX);return s;}
2923function replace(el,val,param){
2924if(!el||!val)return;
2925var orig=el.textContent||"";
2926var cleaned=clean(val);
2927if(cleaned===orig)return;
2928el.setAttribute("data-dtr-original",orig);
2929el.textContent=cleaned;
2930replacements.push({param:param,target:el.tagName.toLowerCase()+(el.id?"#"+el.id:""),original:orig,replacement:cleaned});
2931}
2932// 1. Explicit data-dtr attributes
2933var dtrMap={headline:"utm_headline",subheadline:"utm_subheadline",cta:"utm_cta"};
2934var explicitFound={};
2935for(var key in dtrMap){
2936var pv=params.get(dtrMap[key]);
2937if(pv){
2938var els=document.querySelectorAll('[data-dtr="'+key+'"]');
2939if(els.length>0){
2940explicitFound[key]=true;
2941for(var i=0;i<els.length;i++)replace(els[i],pv,dtrMap[key]);
2942}
2943}
2944}
2945// 2. Custom data-dtr-param attributes
2946var customEls=document.querySelectorAll("[data-dtr-param]");
2947for(var j=0;j<customEls.length;j++){
2948var pn=customEls[j].getAttribute("data-dtr-param");
2949if(pn){var cv=params.get(pn);if(cv)replace(customEls[j],cv,pn);}
2950}
2951// 3. Auto-detect fallback for standard params (only if no explicit match)
2952if(!explicitFound.headline){
2953var hv=params.get("utm_headline");
2954if(hv){var h1=document.querySelector("h1");if(h1)replace(h1,hv,"utm_headline");}
2955}
2956if(!explicitFound.subheadline){
2957var sv=params.get("utm_subheadline");
2958if(sv){var sub=document.querySelector("h2")||document.querySelector(".subtitle")||document.querySelector(".subheadline");if(sub)replace(sub,sv,"utm_subheadline");}
2959}
2960if(!explicitFound.cta){
2961var cv2=params.get("utm_cta");
2962if(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");}
2963}
2964// 4. Generic dtr_* prefix params
2965params.forEach(function(val,key){
2966if(key.indexOf("dtr_")===0){
2967var slug=key.slice(4);
2968var targets=document.querySelectorAll('[data-dtr-param="'+key+'"]');
2969if(targets.length>0){for(var k=0;k<targets.length;k++)replace(targets[k],val,key);}
2970}
2971});
2972// Fire analytics event
2973if(replacements.length===0)return;
2974var PID="v8s3dfzxmk03xqctda3738k8";
2975var API="https://htmlpub.com"+"/api/analytics/collect";
2976function vid(){try{return localStorage.getItem("_hp_vid")||""}catch(e){return""}}
2977function sid(){try{return sessionStorage.getItem("_hp_sid")||""}catch(e){return""}}
2978var ev={pageId:PID,eventType:"dtr_applied",visitorId:vid(),sessionId:sid(),payload:{replacements:replacements},timestamp:Date.now()};
2979var body=JSON.stringify({events:[ev]});
2980try{if(navigator.sendBeacon){navigator.sendBeacon(API,new Blob([body],{type:"text/plain"}));}
2981else{fetch(API,{method:"POST",headers:{"Content-Type":"application/json"},body:body,keepalive:true}).catch(function(){});}}catch(e){}
2982}
2983if(document.readyState==="loading"){document.addEventListener("DOMContentLoaded",run);}else{run();}
2984})();
2985</script>
2985<script async="async" src="https://b.visitor-speed.com/js/45e130c5bbaa2da9ed3d.js"></script>
2985<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:'a46c29894eff6197',t:'MTc5MTM2ODMxMQ=='};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>
2985</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.