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&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&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. </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.