<!DOCTYPE html><html lang="en" style=""><head><meta name="description" content="View this page published on www.sukiebaxter.com">
<meta property="og:title" content="Sukie Baxter — Writer &amp; Somatic Educator">
<meta property="og:description" content="View this page published on www.sukiebaxter.com">
<meta property="og:image" content="https://htmlpub.com/api/og/screenshot?id=hkqp68gnhsow4z4gvc31azx8&amp;v=1788153408403">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:url" content="https://sukiebaxter.com/">
<meta name="twitter:url" content="https://sukiebaxter.com/">
<meta property="og:type" content="website">
<meta property="og:site_name" content="www.sukiebaxter.com">
<meta property="og:locale" content="en_US">
<meta property="fb:app_id" content="4198265810445529">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Sukie Baxter — Writer &amp; Somatic Educator">
<meta name="twitter:description" content="View this page published on www.sukiebaxter.com">
<meta name="twitter:image" content="https://htmlpub.com/api/og/screenshot?id=hkqp68gnhsow4z4gvc31azx8&amp;v=1788153408403">
<meta name="twitter:image:width" content="1200">
<meta name="twitter:image:height" content="630">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://sukiebaxter.com/">
<link rel="icon" href="https://htmlpub.com/api/library/eiys7naifhs6ejeqkox495rz">
<title>Sukie Baxter — Writer &amp; Somatic Educator</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
<link href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,500;1,400;1,500&amp;family=Source+Sans+3:wght@300;400;500&amp;display=swap" rel="stylesheet">

<style id="htmlpub-breakpoint-visibility">@media (max-width: 767px) { .hp-hide-mobile { display: none !important; } }
@media (min-width: 768px) and (max-width: 1199px) { .hp-hide-tablet { display: none !important; } }
@media (min-width: 1200px) { .hp-hide-desktop { display: none !important; } }</style><style>
@import url('https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,500;0,600;1,400;1,500&family=Fira+Sans:wght@300;400;500&display=swap');
:root {
  --cream: #eae7df;
  --cream-dark: #d8d4ca;
  --teal: #1f2e38;
  --teal-light: #2d4254;
  --red: #7b2d26;
  --red-light: #9a3a32;
  --ink: #1c1c1c;
  --ink-soft: #3a3530;
  --muted: #7a736b;
  --rule: #d8d2c8;
  --heading: 'Lora', Georgia, serif;
  --body: 'Fira Sans', sans-serif;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { font-size: 16px; scroll-behavior: smooth; }
body { background: var(--cream); color: var(--ink-soft); font-family: var(--body); font-weight: 400; line-height: 1.7; -webkit-font-smoothing: antialiased; }

nav { position: fixed; top: 0; left: 0; right: 0; z-index: 100; background: rgba(247,243,236,0.92); backdrop-filter: blur(12px); border-bottom: 1px solid var(--rule); padding: 0 clamp(1.5rem,5vw,4rem); }
.nav-inner { max-width: 1100px; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; height: 64px; }
.nav-logo { font-family: var(--heading); font-size: 1rem; font-weight: 500; color: var(--ink); letter-spacing: .01em; text-decoration: none; }
.nav-links { display: flex; gap: 2.5rem; list-style: none; }
.nav-links a { font-family: var(--body); font-size: .875rem; font-weight: 500; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); text-decoration: none; transition: color .2s; }
.nav-links a:hover { color: var(--teal); }
.nav-cta { font-family: var(--body); font-size: .875rem; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--cream); background: var(--teal); padding: .6rem 1.4rem; border-radius: 2px; text-decoration: none; transition: background .2s; }
.nav-cta:hover { background: var(--teal-light); }
.hamburger { display: none; cursor: pointer; border: none; background: none; padding: .5rem; flex-direction: column; gap: 5px; }
.hamburger span { display: block; width: 24px; height: 2px; background: var(--ink); border-radius: 2px; transition: all .3s; }
.mobile-nav { display: none; position: fixed; top: 64px; left: 0; right: 0; background: var(--cream); border-bottom: 1px solid var(--rule); padding: 1.5rem clamp(1.5rem,5vw,4rem); z-index: 99; }
.mobile-nav.open { display: block; }
.mobile-nav ul { list-style: none; display: flex; flex-direction: column; gap: 1.25rem; }
.mobile-nav a { font-family: var(--body); font-size: 1rem; font-weight: 500; color: var(--ink-soft); text-decoration: none; }
.mobile-nav .mnav-cta { display: inline-block; margin-top: .5rem; font-family: var(--body); font-size: .875rem; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--cream); background: var(--teal); padding: .6rem 1.4rem; border-radius: 2px; text-decoration: none; }

.hero { min-height: 100svh; display: flex; align-items: center; padding: clamp(5rem,8vw,8rem) clamp(1.5rem,5vw,4rem) clamp(4rem,6vw,6rem); position: relative; overflow: hidden; background: var(--cream); }
.hero::before { content: ''; position: absolute; inset: 0; z-index: 0; background: radial-gradient(ellipse 60% 50% at 80% 30%, rgba(29,74,71,.06) 0%, transparent 70%), radial-gradient(ellipse 40% 60% at 15% 70%, rgba(184,76,58,.04) 0%, transparent 60%); }
.hero-grain { position: absolute; inset: 0; z-index: 0; opacity: .3; background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.4'/%3E%3C/svg%3E"); background-size: 180px 180px; }
.hero-inner { position: relative; z-index: 1; max-width: 1100px; margin: 0 auto; width: 100%; }
.hero-eyebrow { font-family: var(--body); font-size: .75rem; font-weight: 600; letter-spacing: .12em; text-transform: uppercase; color: var(--red); margin-bottom: 2rem; opacity: 0; animation: fadeUp .8s cubic-bezier(.16,1,.3,1) .1s forwards; }
.hero h1 { font-family: var(--heading); font-size: clamp(3rem,7vw,6.5rem); font-weight: 400; line-height: 1.06; letter-spacing: -.025em; color: var(--ink); margin-bottom: 2rem; opacity: 0; animation: fadeUp .9s cubic-bezier(.16,1,.3,1) .25s forwards; width: max-content; max-width: 100%; }
.hero h1 em { font-style: italic; color: var(--teal); }
.hero-sub { font-family: var(--body); font-size: clamp(1rem,1.5vw,1.2rem); font-weight: 400; line-height: 1.6; color: var(--ink-soft); max-width: 52ch; margin-bottom: 3rem; opacity: 0; animation: fadeUp .9s cubic-bezier(.16,1,.3,1) .4s forwards; }
.hero-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 2rem; opacity: 0; animation: fadeUp .9s cubic-bezier(.16,1,.3,1) .55s forwards; }
.btn-primary { display: inline-block; font-family: var(--body); font-size: .9rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; text-decoration: none; color: var(--cream); background: var(--teal); padding: 1rem 2.2rem; border-radius: 2px; transition: background .25s, transform .2s; box-shadow: 0 2px 12px rgba(29,74,71,.18); }
.btn-primary:hover { background: var(--teal-light); transform: translateY(-1px); }
.btn-secondary { font-family: var(--body); font-size: .9rem; font-weight: 500; color: var(--teal); text-decoration: none; border-bottom: 1px solid var(--teal); padding-bottom: 2px; transition: color .2s, border-color .2s; }
.btn-secondary:hover { color: var(--red); border-color: var(--red); }
.hero-rule { position: absolute; bottom: 0; left: clamp(1.5rem,5vw,4rem); right: clamp(1.5rem,5vw,4rem); height: 1px; background: var(--rule); }

.sec { padding: clamp(5rem,10vw,9rem) clamp(1.5rem,5vw,4rem); }
.sec-inner { max-width: 1100px; margin: 0 auto; }
.sec-label { font-family: var(--body); font-size: .7rem; font-weight: 600; letter-spacing: .14em; text-transform: uppercase; color: var(--muted); margin-bottom: 3rem; }

.opening-bg { background: var(--cream); }
.opening-grid { display: grid; grid-template-columns: 1fr 2fr; gap: clamp(3rem,6vw,7rem); align-items: start; }
.opening-aside { padding-top: .4rem; }
.opening-rule { width: 40px; height: 3px; background: var(--red); margin-bottom: 1.5rem; }
.opening-aside p { font-family: var(--body); font-size: .85rem; font-weight: 400; line-height: 1.65; color: var(--muted); max-width: 22ch; }
.essay-text p { font-family: var(--body); font-size: clamp(1.05rem,1.4vw,1.2rem); font-weight: 400; line-height: 1.8; color: var(--ink-soft); max-width: 62ch; margin-bottom: 1.5rem; }
.essay-text p:last-child { margin-bottom: 0; }
.essay-text strong { font-weight: 500; color: var(--ink); }

.work-bg { background: var(--teal); }
.work-bg .sec-label { color: rgba(247,243,236,.5); }
.work-grid { display: grid; grid-template-columns: 2fr 1fr; gap: clamp(3rem,6vw,7rem); align-items: start; }
.work-text h2 { font-family: var(--heading); font-size: clamp(2rem,4vw,3.5rem); font-weight: 400; line-height: 1.12; letter-spacing: -.02em; color: var(--cream); margin-bottom: 2.5rem; max-width: 20ch; }
.work-text h2 em { font-style: italic; color: rgba(247,243,236,.65); }
.work-text p { font-family: var(--body); font-size: clamp(1rem,1.3vw,1.1rem); font-weight: 400; line-height: 1.8; color: rgba(247,243,236,.82); max-width: 58ch; margin-bottom: 1.5rem; }
.work-text p:last-child { margin-bottom: 0; }
.work-aside { padding-top: 5rem; }
.work-pullquote { border-left: 3px solid var(--red); padding-left: 1.5rem; }
.work-pullquote p { font-family: var(--heading); font-size: clamp(1.15rem,1.8vw,1.4rem); font-style: italic; font-weight: 400; line-height: 1.5; color: rgba(247,243,236,.7); }

.find-bg { background: var(--cream-dark); }
.find-grid { display: grid; grid-template-columns: repeat(3,1fr); gap: clamp(2rem,4vw,5rem); margin-top: 1rem; }
.find-item { padding: 2.5rem 0 0; border-top: 2px solid var(--teal); transition: border-color .2s; }
.find-item:hover { border-top-color: var(--red); }
.find-item-tag { font-family: var(--body); font-size: .7rem; font-weight: 600; letter-spacing: .14em; text-transform: uppercase; color: var(--muted); margin-bottom: .75rem; }
.find-item h3 { font-family: var(--heading); font-size: clamp(1.4rem,2.2vw,2rem); font-weight: 500; line-height: 1.2; letter-spacing: -.01em; color: var(--ink); margin-bottom: 1rem; }
.find-item p { font-family: var(--body); font-size: .95rem; font-weight: 400; line-height: 1.7; color: var(--ink-soft); margin-bottom: 1.5rem; }
.find-link { font-family: var(--body); font-size: .82rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--teal); text-decoration: none; display: inline-flex; align-items: center; gap: .5rem; transition: color .2s; }
.find-link:hover { color: var(--red); }
.find-link svg { transition: transform .2s; }
.find-link:hover svg { transform: translateX(3px); }

.about-bg { background: var(--cream); }
.about-grid { display: grid; grid-template-columns: 1fr 2fr; gap: clamp(3rem,6vw,7rem); align-items: start; }
.about-monogram { width: 72px; height: 72px; background: var(--teal); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin-bottom: 2rem; }
.about-monogram span { font-family: var(--heading); font-size: 1.6rem; font-weight: 500; color: var(--cream); letter-spacing: -.02em; }
.about-left h2 { font-family: var(--heading); font-size: clamp(1.8rem,3vw,2.6rem); font-weight: 400; line-height: 1.15; letter-spacing: -.02em; color: var(--ink); }
.about-left h2 em { font-style: italic; color: var(--teal); }
.about-text p { font-family: var(--body); font-size: clamp(1rem,1.3vw,1.1rem); font-weight: 400; line-height: 1.8; color: var(--ink-soft); max-width: 60ch; margin-bottom: 1.4rem; }
.about-text p:last-child { margin-bottom: 0; }
.about-creds { margin-top: 2.5rem; padding-top: 2rem; border-top: 1px solid var(--rule); display: flex; gap: 2.5rem; flex-wrap: wrap; }
.cred-num { font-family: var(--heading); font-size: 2rem; font-weight: 400; color: var(--teal); line-height: 1; display: block; }
.cred-label { font-family: var(--body); font-size: .8rem; font-weight: 500; letter-spacing: .05em; color: var(--muted); text-transform: uppercase; display: block; }

.newsletter-bg { background: var(--teal); padding: clamp(5rem,10vw,9rem) clamp(1.5rem,5vw,4rem); }
.newsletter-inner { max-width: 1100px; margin: 0 auto; display: grid; grid-template-columns: 1fr 1fr; gap: clamp(3rem,6vw,6rem); align-items: center; }
.newsletter-copy h2 { font-family: var(--heading); font-size: clamp(1.8rem,3.5vw,3rem); font-weight: 400; line-height: 1.15; letter-spacing: -.02em; color: var(--cream); margin-bottom: 1.25rem; }
.newsletter-copy h2 em { font-style: italic; color: rgba(247,243,236,.6); }
.newsletter-copy p { font-family: var(--body); font-size: clamp(.95rem,1.2vw,1.05rem); line-height: 1.75; color: rgba(247,243,236,.75); max-width: 44ch; }
.nl-label { font-family: var(--body); font-size: .7rem; font-weight: 600; letter-spacing: .14em; text-transform: uppercase; color: rgba(247,243,236,.5); margin-bottom: .75rem; display: block; }
.nl-row { display: flex; }
.nl-input { flex: 1; font-family: var(--body); font-size: .95rem; color: var(--ink); background: var(--cream); border: none; padding: .9rem 1.2rem; border-radius: 2px 0 0 2px; outline: none; transition: box-shadow .2s; box-sizing: border-box; }
.nl-input::placeholder { color: var(--muted); }
.nl-input:focus { box-shadow: inset 0 0 0 2px var(--red); }
.nl-btn { font-family: var(--body); font-size: .82rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--cream); background: var(--red); border: 1.5px solid var(--red); border-left: none; padding: .9rem 1.5rem; border-radius: 0 2px 2px 0; cursor: pointer; transition: background .2s; white-space: nowrap; box-sizing: border-box; }
.nl-btn:hover { background: var(--red-light); }
.nl-note { font-family: var(--body); font-size: .8rem; color: rgba(247,243,236,.4); line-height: 1.5; margin-top: .75rem; }

footer { background: var(--teal); color: rgba(247,243,236,.5); padding: 3rem clamp(1.5rem,5vw,4rem); }
.footer-inner { max-width: 1100px; margin: 0 auto; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 2rem; }
.footer-logo { font-family: var(--heading); font-size: .95rem; font-weight: 500; color: rgba(247,243,236,.7); text-decoration: none; }
.footer-links { display: flex; gap: 2rem; list-style: none; }
.footer-links a { font-family: var(--body); font-size: .8rem; color: rgba(247,243,236,.4); text-decoration: none; transition: color .2s; }
.footer-links a:hover { color: rgba(247,243,236,.8); }
.footer-social { display: flex; gap: 1.25rem; align-items: center; }
.footer-social a { color: rgba(247,243,236,.35); display: flex; align-items: center; transition: color .2s; }
.footer-social a:hover { color: rgba(247,243,236,.8); }

.reveal { opacity: 0; transform: translateY(24px); transition: opacity .8s cubic-bezier(.16,1,.3,1), transform .8s cubic-bezier(.16,1,.3,1); }
.reveal.visible { opacity: 1; transform: translateY(0); }
.reveal-d1 { transition-delay: .1s; }
.reveal-d2 { transition-delay: .2s; }
.reveal-d3 { transition-delay: .3s; }

@keyframes fadeUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }

@media (max-width: 900px) {
  .opening-grid { grid-template-columns: 1fr; }
  .opening-aside { display: none; }
  .work-grid { grid-template-columns: 1fr; }
  .work-aside { display: none; }
  .find-grid { grid-template-columns: 1fr; }
  .about-grid { grid-template-columns: 1fr; }
  .newsletter-inner { grid-template-columns: 1fr; }
}
@media (max-width: 767px) {
  .nav-links, .nav-cta { display: none; }
  .hamburger { display: flex; }
  .footer-inner { flex-direction: column; align-items: flex-start; gap: 1.5rem; }
}
@media (prefers-reduced-motion: reduce) {
  .hero h1, .hero-eyebrow, .hero-sub, .hero-actions { animation: none; opacity: 1; }
  .reveal { opacity: 1; transform: none; transition: none; }
}

.hero-desc { max-width: 480px; margin-bottom: 0; padding-top: 0.25rem; }
.hero-desc-rule { width: 36px; height: 2px; background: var(--red); margin-bottom: 1.25rem; }
.hero-desc-text { font-family: var(--body); font-size: clamp(1rem, 1.4vw, 1.1rem); font-weight: 400; line-height: 1.75; color: var(--ink-soft); }

.hero-inner { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(3rem,6vw,7rem); align-items: center; width: 100%; max-width: 1100px; }
.hero-photo-col { position: relative; }
.hero-photo { width: 100%; height: clamp(480px,70vh,760px); object-fit: cover; object-position: center top; display: block; }
.hero-text-col { display: flex; flex-direction: column; gap: 0; }
.hero-text-col h1 { font-family: var(--heading); font-size: clamp(2rem,3.5vw,3.2rem); font-weight: 400; line-height: 1.1; letter-spacing: -.02em; color: var(--teal); margin-bottom: 1.5rem; }
.hero-text-col h1 em { font-style: italic; color: var(--ink-soft); }
.hero-form-label { font-family: var(--body); font-size: .7rem; font-weight: 600; letter-spacing: .14em; text-transform: uppercase; color: var(--muted); margin-bottom: 1rem; display: block; }
.hero-nl-input { border: 1.5px solid var(--red); border-right: none; background: var(--cream); }
.hero-nl-input:focus { border-color: var(--red); outline: none; box-shadow: none; }
@media (max-width: 900px) {
  .hero-inner { grid-template-columns: 1fr; }
  .hero-photo { height: 360px; object-position: center 20%; }
  .hero-photo-col { order: 2; }
  .hero-text-col { order: 1; }
}
</style></head>
<body style="" class="">
<nav style="">
  <div class="nav-inner" style="">
    <a href="/" class="nav-logo" style=""><b style=""><span style="font-weight: normal; color: rgb(31, 46, 56);">SUKIE BAXTER</span></b></a>
    <ul class="nav-links" style="">
      <li style="" class=""><a href="https://sukie-baxter.lpcontent.net/8jb8FCqXzT?v=1779318768914" style="" rel="noopener noreferrer">ABOUT</a></li>
      <li style=""><a href="https://sukiebaxter.substack.com/" style="" rel="noopener noreferrer">Essays</a></li>
      <li style=""><a href="https://sukie-baxter.lpcontent.net/OGG_1bw8PA" style="" rel="noopener noreferrer">TONIC</a></li>
    </ul>
    
    <button class="hamburger" id="nav-hamburger" aria-label="Open menu">
      <span></span><span></span><span></span>
    </button>
  </div>
</nav>

<div class="mobile-nav" id="mobile-nav">
  <ul>
    <li><a href="#the-work">The Work</a></li>
    <li><a href="#essays">Essays</a></li>
    <li><a href="#about">About</a></li>
    <li><a href="#tonic" class="mnav-cta">Learn about Tonic</a></li>
  </ul>
</div>

<section class="hero">
  <div class="hero-grain" style=""></div>
  <div class="hero-inner" style="">
  <div class="hero-photo-col" style="min-height: 500px; overflow: hidden;">
    <img src="/api/pages/6lZ-H2eoaq/assets/et7j9y10bz5t2n60etsv8wt5?w=1200&fit=scale-down&format=auto&quality=82" alt="Sukie Baxter" class="hero-photo" style="width: 100%; height: 100%; min-height: 500px; object-fit: cover; object-position: center 15%; display: block;" fetchpriority="high">
  </div>
  <div class="hero-text-col" style="">
    
    <h1 style="width: auto; max-width: 100%; color: rgb(31, 46, 56); font-size: 62px;">You can't <i style="">research</i> your way out of anxiety.<br></h1>
    <p class="hero-desc-text" style="">You've spent years and real money collecting tools that work for a week and then don't. Your problem isn't a lack of information. You could probably teach a class on this by now.<br></p><label class="nl-label hero-form-label" for="nl-email-hero" style="color: var(--red); margin-bottom: 1rem; margin-top: 1.5rem;">Subscribe to The Reboot</label>
    <div class="hero-desc" style="">
      <div class="hero-desc-rule"></div>
      <p class="hero-desc-text" style="font-size: 16px; color: rgb(31, 46, 56);" data-htmlpub-settled="1"><b>Join 16,794 humans reading&nbsp;</b><b style="">weekly-ish essays on the structured, body-first way out of anxiety. <i style="">No more research required.</i></b></p>
    </div>
    <form name="newsletter-signup" data-success-message="You're in. The next essay lands in your inbox soon." style="margin-top: 1.5rem;">
      <div class="nl-row" style="align-items: stretch;">
        <input class="nl-input hero-nl-input" type="email" id="nl-email-hero" name="email" placeholder="your@email.com" required="" style="">
        <button class="nl-btn" type="submit" style="">Subscribe</button>
      </div>
      <p class="nl-note" style="color: rgb(122, 115, 107); font-weight: 300;">Zero spam. Unsubscribe at any time. I won't take it personally.</p>
    </form>
  </div>
</div>
  <div class="hero-rule" style=""></div>
</section>











<footer style="">
  <div class="footer-inner" style="">
    <a href="/" class="footer-logo" style="">SUKIE BAXTER</a>
    <ul class="footer-links" style="">
      <li><a href="#" style="">Privacy</a></li>
      <li><a href="#" style="">Terms</a></li>
      <li><a href="#">Disclaimer</a></li>
    </ul>
    <div class="footer-social">
      <a href="https://www.youtube.com/sukiebaxter" aria-label="YouTube"><svg width="20" height="20" viewBox="0 0 24 24" fill="none"><path d="M22.54 6.42a2.78 2.78 0 0 0-1.95-1.96C18.88 4 12 4 12 4s-6.88 0-8.59.46a2.78 2.78 0 0 0-1.95 1.96A29 29 0 0 0 1 12a29 29 0 0 0 .46 5.58A2.78 2.78 0 0 0 3.41 19.54C5.12 20 12 20 12 20s6.88 0 8.59-.46a2.78 2.78 0 0 0 1.95-1.96A29 29 0 0 0 23 12a29 29 0 0 0-.46-5.58z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><polygon points="9.75 15.02 15.5 12 9.75 8.98 9.75 15.02" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></polygon></svg></a>
      <a href="https://sukiebaxter.substack.com/" aria-label="Substack"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" style=""><path d="M22 5H2v3.5h20V5zM2 11.5V22l10-5.5 10 5.5V11.5H2z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style=""></path></svg></a>
      
    </div>
  </div>
</footer>

<script>
(function(){
  var btn = document.getElementById('nav-hamburger');
  var menu = document.getElementById('mobile-nav');
  if(btn && menu){
    btn.addEventListener('click', function(){ menu.classList.toggle('open'); });
  }
  var obs = new IntersectionObserver(function(entries){
    entries.forEach(function(e){
      if(e.isIntersecting){ e.target.classList.add('visible'); obs.unobserve(e.target); }
    });
  }, { threshold: 0.1 });
  document.querySelectorAll('.reveal').forEach(function(el){ obs.observe(el); });
})();
</script>
<script data-htmlpub-analytics>
(function(){
var PID="hkqp68gnhsow4z4gvc31azx8";
var EXP_ID="";
var EXP_VARIANT="";
var API="/collect";
var APP_ORIGIN="https://htmlpub.com";
var FLUSH_INTERVAL=5000;
var FLUSH_MAX=20;

function 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)});}
function 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();}}

var visitorId=getOrSet(localStorage,"_hp_vid");
var sessionId=getOrSet(sessionStorage,"_hp_sid");

var params=new URLSearchParams(location.search);
var utmSource=params.get("utm_source")||undefined;
var utmMedium=params.get("utm_medium")||undefined;
var utmCampaign=params.get("utm_campaign")||undefined;
// Ad-platform click IDs (conversion feedback loop) — captured like the UTMs.
// SYNC with canonical at app/src/lib/analytics-tracking-script.ts.
var gclid=params.get("gclid")||undefined;
var fbclid=params.get("fbclid")||undefined;

var vw=window.innerWidth||0;
var vh=window.innerHeight||0;
var ua=navigator.userAgent||"";
var isMobileUA=/Android|iPhone|iPod|iPad|webOS|BlackBerry|IEMobile|Opera Mini/i.test(ua);
var maxScreenDim=Math.max(screen.width||0,screen.height||0);
var deviceType=isMobileUA?(maxScreenDim<=812||vw<768?"mobile":"tablet"):"desktop";

function detectBrowser(){
if(ua.indexOf("Edg/")>-1)return"Edge";
if(ua.indexOf("Chrome/")>-1)return"Chrome";
if(ua.indexOf("Safari/")>-1&&ua.indexOf("Chrome/")===-1)return"Safari";
if(ua.indexOf("Firefox/")>-1)return"Firefox";
return"Other";
}
function detectOS(){
if(ua.indexOf("iPhone")>-1||ua.indexOf("iPad")>-1)return"iOS";
if(ua.indexOf("Android")>-1)return"Android";
if(ua.indexOf("Mac OS")>-1)return"macOS";
if(ua.indexOf("Windows")>-1)return"Windows";
if(ua.indexOf("Linux")>-1)return"Linux";
return"Other";
}
var browserFamily=detectBrowser();
var osFamily=detectOS();

var buffer=[];
function makeEvent(type,payload){
return{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,deviceType:deviceType,browserFamily:browserFamily,osFamily:osFamily,viewportWidth:vw,viewportHeight:vh,experimentId:EXP_ID||undefined,experimentVariant:EXP_VARIANT||undefined,timestamp:Date.now()};
}
function pushEvent(type,payload){buffer.push(makeEvent(type,payload));if(buffer.length>=FLUSH_MAX)flush();}

// HP-1557 — expose a single-arg helper so the popups runtime (separate IIFE,
// no shared closure with analytics) can feed events into the same buffer.
// Defensive: only set if not already defined so a future second analytics
// injection doesn't clobber an in-flight handler.
if(!window.__hpupPushEvent){window.__hpupPushEvent=function(t,p){try{pushEvent(t,p);}catch(e){}};}
// NOTE (HP-3461): this copy deliberately does NOT register window.__hpTrack, even
// though the app copy registers both names. checkout-script.ts guards on
// __hpTrack, and inject.ts appends that script to every edge-served page, so
// adding the name here would start writing checkout_started for the entire edge
// fleet (pubhtml.com + all custom domains) where that beacon has always been a
// no-op — beginning an ecommerce event series mid-history, and moving the derived
// checkout_abandoned with it, from a change about page engagement. It is a real
// gap and wants its own change with a backfill story.
// (No backticks anywhere in this file: it is one big template literal, so a
// backtick in a comment terminates the string. This exact comment did it once.)

function readMmuid(){try{var v=localStorage.getItem("mmuid");return v&&v.length<=128?v:undefined;}catch(e){return undefined;}}

function flush(){
if(buffer.length===0)return;
var batch=buffer.splice(0,FLUSH_MAX);
var payload={events:batch};
var mmuid=readMmuid();
if(mmuid)payload.mmuid=mmuid;
try{navigator.sendBeacon(API,JSON.stringify(payload));}catch(e){}
}

// Engaged time-on-page (HP-3592) — count a second only while the tab is
// visible AND the visitor was active in the last 30s; emit the accrued DELTA
// every 30 engaged seconds and on hide/pagehide. Registered BEFORE the flush
// handlers below so the final delta lands in the buffer before the last
// beacon fires (listener registration order = firing order).
// SYNC with canonical at app/src/lib/analytics-tracking-script.ts.
var dwellSec=0,dwellSent=0,dwellLastActive=Date.now();
var DWELL_IDLE_MS=30000,DWELL_EMIT_SEC=30,DWELL_CAP_SEC=1800;
function dwellActive(){dwellLastActive=Date.now();}
document.addEventListener("scroll",dwellActive,{passive:true});
document.addEventListener("mousemove",dwellActive,{passive:true});
document.addEventListener("keydown",dwellActive,true);
document.addEventListener("touchstart",dwellActive,{passive:true});
document.addEventListener("click",dwellActive,true);
function dwellEmit(){var d=dwellSec-dwellSent;if(d>0){dwellSent=dwellSec;pushEvent("dwell",{seconds:d});}}
var dwellTimer=setInterval(function(){
if(document.visibilityState!=="visible")return;
if(Date.now()-dwellLastActive>DWELL_IDLE_MS)return;
dwellSec++;
if(dwellSec-dwellSent>=DWELL_EMIT_SEC)dwellEmit();
if(dwellSec>=DWELL_CAP_SEC){dwellEmit();clearInterval(dwellTimer);}
},1000);
document.addEventListener("visibilitychange",function(){if(document.visibilityState==="hidden")dwellEmit();});
window.addEventListener("pagehide",dwellEmit);

setInterval(flush,FLUSH_INTERVAL);
document.addEventListener("visibilitychange",function(){if(document.visibilityState==="hidden")flush();});
window.addEventListener("pagehide",flush);

pushEvent("page_view");

// On-platform conversion goal ("a thank-you page I own"): report the current
// pageId plus the visitor's experiment assignments (_hp_exp_<id> cookies) so the
// server can count this page-view as a conversion. Fires once per session per
// page. HP-2925 Layer 1: must fire on EVERY page, not just experiment
// participants — a standalone "Offsite event" goal has no experiment cookie, so
// gating on cookies means the server never gets to attribute it. The server
// no-ops cheaply when the page is neither an experiment nor a goal target.
// SYNC with canonical at app/src/lib/analytics-tracking-script.ts.
(function(){
try{
if(!APP_ORIGIN)return;
var guard="_hp_goal:"+PID;
if(sessionStorage.getItem(guard))return;
sessionStorage.setItem(guard,"1");
var exps=[];var ck=document.cookie.split(";");
for(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())});}}
var body=JSON.stringify({pageId:PID,visitorId:visitorId,sessionId:sessionId,experiments:exps});
var url=APP_ORIGIN+"/api/experiments/onsite-conversion";
if(navigator.sendBeacon){navigator.sendBeacon(url,new Blob([body],{type:"text/plain"}));}
else{fetch(url,{method:"POST",headers:{"Content-Type":"application/json"},body:body,keepalive:true}).catch(function(){});}
}catch(e){}
})();

// HP-3461 — milestones reached WITHOUT the visitor scrolling carry {auto:1}.
// checkScroll runs on a 1s timer regardless of input and at scrollY=0 pct is
// just winH/docH, so any page under ~4 screens tall banked scroll_depth:25 from
// every visitor who did nothing (a page fitting the viewport banked 100). Those
// rows mark the session engaged in the rollup, which pushed the bounce rate of
// every short page toward zero and inflated avgScrollDepth the same way. The
// events still fire (that depth WAS visible, and the scroll funnel wants it);
// the stamp lets the bounce query demand a real interaction.
// SYNC with app/src/lib/analytics-tracking-script.ts.
var scrollMilestones={25:false,50:false,75:false,100:false};
var scrollUserMoved=false;
function checkScroll(){
var docH=Math.max(document.body.scrollHeight,document.documentElement.scrollHeight);
var scrollY=window.scrollY||window.pageYOffset;
var winH=window.innerHeight;
if(docH<=winH){if(!scrollMilestones[100]){scrollMilestones[100]=true;pushEvent("scroll_depth",{depth:100,auto:1});}return;}
var pct=Math.round(((scrollY+winH)/docH)*100);
[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});}});
}
window.addEventListener("scroll",function(){scrollUserMoved=true;checkScroll();},{passive:true});
setTimeout(checkScroll,1000);

// HP-1557 — when a click/submit happens inside a pop-up, add the popup's id +
// name to the payload. Customers track conversions on the standard form_submit /
// element_click types; this is just attribution metadata so the analytics surface
// can say "this conversion came from the Welcome offer pop-up". Matches the
// canonical data-persist-popup marker and the legacy data-htmlpub-popup one.
function popupAttribution(node){
var p=node&&node.closest&&node.closest("[data-persist-popup],[data-htmlpub-popup]");
if(!p)return null;
var nm=p.getAttribute("data-persist-popup-name");if(nm===null)nm=p.getAttribute("data-htmlpub-popup-name");
return{popupId:p.getAttribute("id")||"",popupName:(nm||"").replace(/^\s+|\s+$/g,"")};
}

document.addEventListener("click",function(e){
var el=e.target;
if(!el||!el.tagName)return;
var tag=el.tagName.toLowerCase();
if(!(tag==="a"||tag==="button"||tag==="input"||tag==="select"||tag==="textarea"||el.getAttribute("role")==="button"||el.closest("a,button,[role=button]")))return;
var docH=Math.max(document.body.scrollHeight,document.documentElement.scrollHeight,1);
var rect=el.getBoundingClientRect();
var 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};
// HP-3227 — toggling "count clicks as conversions" on a button stamps
// data-lp-conversion="<configId>" on that element, and both the rollup worker
// and the dashboard read match a click goal by that id. This tracker never
// reported it, so a goal whose stored selector is the placeholder
// a[data-lp-conversion] had nothing to match on and counted zero conversions
// forever, no matter how many buttons were toggled on. Read from the closest
// stamped ancestor, since the click target is often an inner <span> of the CTA.
// SYNC with app/src/lib/analytics-tracking-script.ts.
var lpConvEl=el.closest?el.closest("[data-lp-conversion]"):null;
var lpConvId=lpConvEl?lpConvEl.getAttribute("data-lp-conversion"):null;
if(lpConvId)payload.lpConversion=lpConvId;
// HP-1557 — only attribute element_click to a pop-up when the actual
// click target is a button-shaped CTA. A click on the email input
// inside a lead-capture popup would otherwise inflate the popup's
// "Button clicks" count on the dashboard.
var isCtaClick=(tag==="a"||tag==="button"||el.getAttribute("role")==="button"||(el.closest&&el.closest("a,button,[role=button]")));
if(isCtaClick){var pa=popupAttribution(el);if(pa){payload.popupId=pa.popupId;payload.popupName=pa.popupName;}}
// HP-3461 — two NON-IDENTIFYING facts about the clicked anchor: the host it leaves
// for (external only) and the file extension. The selector above records only "#id"
// or "tag.class", so a PDF link and any other button were indistinguishable and no
// past click can be reclassified. The full href is NOT stored: pathnames carry
// customer identity and this is the highest-volume table (see absUrl).
// SYNC with app/src/lib/analytics-tracking-script.ts.
var hrefEl=tag==="a"?el:(el.closest&&el.closest("a[href]"));
var clickUrl=hrefEl?absUrl(hrefEl):null;
var clickFacts=clickUrl?linkFacts(clickUrl):null;
if(clickFacts&&clickFacts.external)payload.linkHost=clickFacts.host;
if(clickFacts&&clickFacts.ext)payload.linkExt=clickFacts.ext;
pushEvent("element_click",payload);
// Classify into a micro-conversion. Emitted ALONGSIDE the element_click, never
// instead of it, so a classification we later decide was wrong can be re-derived.
if(hrefEl)classifyLinkClick(hrefEl,clickUrl);
// HP-2811 — same-tab anchor navigation races the page unload before the 5s
// flush timer; visibilitychange/pagehide flushes are unreliable (esp. iOS
// Safari, same-tab nav), so element_click goals for outbound CTAs (e.g. a
// HubSpot Meetings link) see 0 conversions despite real clicks. Mirror the
// form_submit flush pattern for anchors that will navigate this tab.
var navAnchor=tag==="a"?el:(el.closest&&el.closest("a[href]"));
if(navAnchor&&navAnchor.getAttribute("target")!=="_blank"&&!e.ctrlKey&&!e.metaKey&&!e.shiftKey){
var navHref=(navAnchor.getAttribute("href")||"").replace(/^\s+/,"");
if(navHref&&navHref.charAt(0)!=="#"&&navHref.slice(0,11).toLowerCase()!=="javascript:")flush();
}
},true);

document.addEventListener("submit",function(e){
var f=e.target;
if(!f||f.tagName!=="FORM")return;
var payload={formIdentifier:f.id||f.getAttribute("name")||"form-"+([].indexOf.call(document.forms,f))};
var pa=popupAttribution(f);
if(pa){payload.popupId=pa.popupId;payload.popupName=pa.popupName;}
pushEvent("form_submit",payload);
flush();// send now — a thank-you-page redirect may navigate away before the 5s timer, dropping the experiment conversion
},true);

// ─── HP-3461 on-page micro-conversions ──────────────────────────────────────
// Kinds + classification rules mirror app/src/lib/analytics/page-engagement-kinds.ts
// (the server-side reference). Literal here because this compiles to an inline
// string and cannot import; page-engagement-kinds.test.ts asserts both script
// copies only ever emit registered kinds.
// SYNC with app/src/lib/analytics-tracking-script.ts.

// Resolve an anchor for RULE MATCHING only — the url itself is never sent. Only
// host + extension are reported (linkFacts): customer delivery links routinely put
// identity in the PATH (/thank-you/jane-doe, /download/lead/8842/Invoice-Jane-Doe.pdf),
// and AnalyticsEvent is the highest-volume table with no redaction pass and no
// consent gate, so a stored pathname is PII a DSAR erase cannot reach.
function absUrl(a){
try{
var raw=(a.getAttribute("href")||"").replace(/^\s+/,"");
if(!raw||raw.charAt(0)==="#"||raw.slice(0,11).toLowerCase()==="javascript:")return null;
var u=new URL(raw,location.href);
if(u.protocol!=="http:"&&u.protocol!=="https:")return null;
return u;
}catch(e){return null;}
}
// Host + extension, derived from the WHOLE url (never a truncated copy) so a long
// signed URL cannot lose its extension to a length cap and read as plain navigation.
function linkFacts(u){
var p=u.pathname.toLowerCase();
var dot=p.lastIndexOf(".");
var ext=dot<0?"":p.slice(dot+1);
if(ext.length>8)ext="";
return{host:u.hostname.toLowerCase(),ext:ext,external:u.hostname!==location.hostname};
}

var DL_EXT=["pdf","epub","zip","csv","xlsx","xls","docx","doc","pptx","ppt","mp3","wav"];
// calendar.google.com is deliberately ABSENT: the standard add-to-calendar button
// is not a booking, and host-only matching cannot tell them apart, so including it
// reported bookings on pages where nobody booked. Host entries only — this copy
// matches on hostname, so a host/path entry could never match here.
var BOOK_HOSTS=["calendly.com","cal.com","acuityscheduling.com"];

// Every kind counts at most once PER PAGE LOAD: three clicks on the same PDF is one
// download, and a scrubbed video cannot re-bank a quartile.
//
// IN-MEMORY, and it must stay that way. This used to consult sessionStorage and
// return true from its catch block, so wherever storage throws (sandboxed iframe,
// "block all cookies", in-app webviews, full quota) every timeupdate re-emitted
// three quartiles — 12+/sec, which trips the collect route's 30-req/min-per-IP
// limit and then 429s that visitor's REAL page_view / form_submit / conversion
// beacons. A session-scoped key also outlived the page load, so a batch lost to a
// 429 or a closed tab could never be re-reported. The server counts DISTINCT
// SESSIONS per kind, so duplicates across page views are deduped there anyway.
var microFired={};
function latch(kind,id){
var k=kind+":"+(id||"");
if(microFired[k])return false;
microFired[k]=true;
return true;
}
function microEvent(kind,id){
if(!latch(kind,id))return;
pushEvent("page_engagement",id?{kind:kind,id:id}:{kind:kind});
}

function classifyLinkClick(a,u){
try{
if(!u)return;
var f=linkFacts(u);
if((a.hasAttribute&&a.hasAttribute("download"))||(f.ext&&DL_EXT.indexOf(f.ext)>-1)){
// id is the EXTENSION, not the file name: "Invoice-Jane-Doe.pdf" is as
// identifying as the path we refuse to store, and phase 1 renders only an
// aggregate count.
microEvent("file_download",f.ext||undefined);
return;
}
if(!f.external)return;
for(var i=0;i<BOOK_HOSTS.length;i++){
var b=BOOK_HOSTS[i];
if(f.host===b||f.host.slice(-(b.length+1))==="."+b){microEvent("booking_open",b);return;}
}
}catch(e){}
}

// <video> only. YouTube/Vimeo/Loom/Wistia are cross-origin iframes whose
// playback is invisible without the provider's JS API plus an embed-URL rewrite,
// both deferred out of phase 1 — a page whose only video is an embed reports NO
// video kinds, which the read side must treat as "not measurable", not as 0%.
function videoId(v){
try{
if(v.id)return v.id.slice(0,128);
var src=v.currentSrc||v.getAttribute("src")||"";
if(!src)return undefined;
var seg=src.split("?")[0].split("/");
return (seg[seg.length-1]||"").slice(0,128)||undefined;
}catch(e){return undefined;}
}
// Autoplay policy only lets a video start by itself while MUTED, so "muted and the
// visitor has never touched the page" is the signature of decorative playback.
// A <video autoplay muted loop playsinline> hero fires 'play' on every load, so
// counting it would report "100% played the video" on exactly the pages most likely
// to have a hero video. Unmuted playback always counts: it needs a gesture.
// mousedown as well as pointerdown: a path delivering only mouse events would
// otherwise never set this, silently disabling the feature with nothing in the logs.
var userGestured=false;
function markGesture(){userGestured=true;}
document.addEventListener("pointerdown",markGesture,true);
document.addEventListener("mousedown",markGesture,true);
document.addEventListener("keydown",markGesture,true);
document.addEventListener("touchstart",markGesture,{passive:true,capture:true});
// Decide ONCE at 'play' and LATCH on the element. Re-checking the live flag on every
// timeupdate was a real bug found in Chrome: a decorative muted autoplay correctly
// suppressed video_start, then one unrelated trusted keystroke flipped userGestured
// and every later timeupdate passed — banking p25/p50/p75 for a video nobody chose
// to watch, with NO video_start, leaving the read side dividing by a zero
// denominator. Unmuted playback always counts: it cannot start without a gesture.
function decidePlay(v){
var intent=(!v.muted)||userGestured;
try{v.__hpIntent=intent?1:0;}catch(e){}
return intent;
}
// Undefined = never saw this element's 'play' (script injected mid-playback): fail
// closed, since crediting is the direction that corrupts the headline percentage.
function countedPlay(v){return v.__hpIntent===1;}
// Unmuting a decorative video IS intent, and no 'play' fires for it, so re-decide.
// Emitting video_start keeps the stream coherent: a start precedes its quartiles.
function onVideoVolume(e){
var v=e.target;
if(!v||v.tagName!=="VIDEO")return;
if(v.muted||v.paused||v.__hpIntent===1)return;
v.__hpIntent=1;
microEvent("video_start",videoId(v));
}
function onVideoTime(e){
var v=e.target;
if(!v||v.tagName!=="VIDEO")return;
// Consult the LATCHED decision, never the live flag — see decidePlay.
if(!countedPlay(v))return;
var d=v.duration;
// Live streams / metadata not yet loaded report 0, NaN or Infinity — a quartile
// of an unknown duration is meaningless, so report the play and nothing more.
if(!isFinite(d)||d<=0)return;
var pct=(v.currentTime/d)*100;
var id=videoId(v);
if(pct>=25)microEvent("video_p25",id);
if(pct>=50)microEvent("video_p50",id);
if(pct>=75)microEvent("video_p75",id);
// p100 comes from 'ended', not a threshold: the last timeupdate can land a
// fraction short of duration, so a full watch would never bank it.
}
document.addEventListener("play",function(e){var v=e.target;if(v&&v.tagName==="VIDEO"&&decidePlay(v))microEvent("video_start",videoId(v));},true);
document.addEventListener("timeupdate",onVideoTime,true);
document.addEventListener("volumechange",onVideoVolume,true);
// SYNC-START embed-video-listeners (app/edge twins pinned by page-engagement-kinds.test.ts)
// ─── Embedded video: Vimeo (HP-3461) ────────────────────────────────────────
// <video> is handled above by reading the element directly. A Vimeo embed is a
// CROSS-ORIGIN iframe, so its state is unreadable — but the player reports its own
// progress over postMessage, and needs NO change to the embed URL. A plainly pasted
// Vimeo embed becomes measurable as-is. (YouTube is different: it is silent unless
// enablejsapi=1 is on the URL. Handled separately.)
//
// Verified against live players, and two details cost an attempt each:
//   1. SUBSCRIBE ON THE PLAYER'S ready MESSAGE, not the iframe's load event.
//      Subscribing on load races the player's own init: the addEventListener calls
//      are silently dropped, no ACK comes back, and no timeupdate ever arrives — it
//      looks exactly like "Vimeo does not work".
//   2. ATTRIBUTE BY e.source, not player_id. Vimeo only echoes a player_id if the
//      embed URL carries one, and a customer's pasted embed has none.
//
// Vimeo sends OBJECTS (YouTube sends JSON strings), and timeupdate carries
// data.percent as a 0..1 fraction, so quartiles need no arithmetic of our own.
function vimeoHostOk(origin){
try{
var h=new URL(origin).hostname.toLowerCase();
return h==="player.vimeo.com"||h==="vimeo.com"||h.slice(-10)===".vimeo.com";
}catch(e){return false;}
}
// Low-cardinality, non-identifying label: the numeric video id off the embed URL,
// prefixed so it cannot collide with a <video> filename in the tally.
function vimeoLabel(f){
try{
var m=(f.getAttribute("src")||"").match(/vimeo\.com\/video\/(\d+)/);
return m?"vimeo:"+m[1]:"vimeo";
}catch(e){return "vimeo";}
}
function vimeoFrameFor(src){
try{
var fr=document.querySelectorAll("iframe");
for(var i=0;i<fr.length;i++){if(fr[i].contentWindow===src)return fr[i];}
}catch(e){}
return null;
}
function vimeoSubscribe(f){
try{
var w=f.contentWindow;
var evs=["play","timeupdate","ended"];
for(var i=0;i<evs.length;i++)w.postMessage({method:"addEventListener",value:evs[i]},"*");
}catch(e){}
}
window.addEventListener("message",function(e){
try{
if(!vimeoHostOk(e.origin))return;
var d=e.data;
if(!d||typeof d!=="object")return;
var f=vimeoFrameFor(e.source);
if(!f)return;
if(d.event==="ready"){vimeoSubscribe(f);return;}
var id=vimeoLabel(f);
if(d.event==="play"){
// Same intent rule as <video>, minus the muted check we cannot read through an
// iframe: browser policy only permits unprompted autoplay while MUTED, so
// "playback began before the visitor touched anything" is the decorative case.
if(!userGestured)return;
f.__hpEmbedCounted=1;
microEvent("video_start",id);
return;
}
if(f.__hpEmbedCounted!==1)return;
if(d.event==="timeupdate"){
var pct=d.data&&typeof d.data.percent==="number"?d.data.percent*100:-1;
if(pct<0)return;
if(pct>=25)microEvent("video_p25",id);
if(pct>=50)microEvent("video_p50",id);
if(pct>=75)microEvent("video_p75",id);
// p100 comes from 'ended', as with <video>: the last timeupdate lands short.
}else if(d.event==="ended"){
microEvent("video_p25",id);microEvent("video_p50",id);microEvent("video_p75",id);microEvent("video_p100",id);
}
}catch(err){}
});

// ─── Embedded video: YouTube (HP-3461) ──────────────────────────────────────
// YouTube is SILENT unless the embed URL carries enablejsapi=1 — verified live: a
// plain embed answers nothing, even if we send it the handshake. So this listener
// only hears players whose URL has that flag; adding it to customer HTML at serve
// time is a separate commit, deliberately, because that mutates what visitors load.
//
// With the flag, the protocol is: post a listening message, then the player streams
// infoDelivery with playerState, currentTime and duration.
//
// The handshake repeats on a timer, and the honest reason is that it catches frames
// that appear after load (a tab, an accordion, a script-mounted embed). Whether a
// player also STOPS streaming when we stop asking was not established: repeats are
// answered "alreadyInitialized", which suggests the first one is what arms it. The
// polling stays because the version with polling is the one verified to work end to
// end, and trading that for an unverified optimisation would risk silently losing
// every embed metric. The steady-state cost is one postMessage per api-enabled
// frame every 2s.
//
// Unlike Vimeo, YouTube sends JSON STRINGS, so the payload needs parsing before it
// looks like anything.
//
// playerState is YouTube's enum: -1 unstarted, 0 ended, 1 playing, 2 paused,
// 3 buffering, 5 cued.
function ytHostOk(origin){
try{
var h=new URL(origin).hostname.toLowerCase();
return h==="www.youtube.com"||h==="youtube.com"||h==="www.youtube-nocookie.com"||h==="youtube-nocookie.com";
}catch(e){return false;}
}
function ytLabel(f){
try{
var m=(f.getAttribute("src")||"").match(/\/embed\/([A-Za-z0-9_-]{11})/);
return m?"youtube:"+m[1]:"youtube";
}catch(e){return "youtube";}
}
function ytFrameFor(src){
try{
var fr=document.querySelectorAll("iframe");
for(var i=0;i<fr.length;i++){if(fr[i].contentWindow===src)return fr[i];}
}catch(e){}
return null;
}
// Ask every api-enabled YouTube frame to start reporting, and keep asking. Cheap: a
// postMessage to a frame that is not a YouTube player is simply ignored.
function ytListen(){
try{
var fr=document.querySelectorAll('iframe[src*="enablejsapi=1"]');
for(var i=0;i<fr.length;i++){
try{fr[i].contentWindow.postMessage('{"event":"listening","id":"hp","channel":"widget"}',"*");}catch(e){}
}
}catch(e){}
}
setTimeout(ytListen,1200);
setInterval(ytListen,2000);
window.addEventListener("message",function(e){
try{
if(!ytHostOk(e.origin))return;
var d=e.data;
// YouTube sends strings; anything else is not its API talking.
if(typeof d!=="string")return;
try{d=JSON.parse(d);}catch(x){return;}
if(!d||typeof d!=="object"||d.event!=="infoDelivery")return;
var info=d.info;
if(!info||typeof info!=="object")return;
var f=ytFrameFor(e.source);
if(!f)return;
var id=ytLabel(f);
var state=info.playerState;
// Same intent rule as Vimeo and <video>, but YouTube hands us no discrete 'play'
// event: infoDelivery repeats playerState 1 for the WHOLE of playback. So the
// decision is made on the TRANSITION into playing and latched there, never
// re-read. Re-reading it is precisely the bug decidePlay documents, and it is
// reachable here in a way it is not for <video>: a muted autoplay is correctly
// suppressed, then one unrelated trusted click (the form, alt-tab, a screenshot)
// flips userGestured and the next state-1 update promotes a video nobody chose
// into a counted play, quartiles and all. Latching on the transition also keeps
// the <video> semantics that a chosen REPLAY after a decorative autoplay counts,
// because that arrives as 2 then 1.
var prev=f.__hpYtPrev;
f.__hpYtPrev=state;
if(state===1&&prev!==1){
f.__hpEmbedCounted=userGestured?1:0;
if(f.__hpEmbedCounted===1)microEvent("video_start",id);
}
if(f.__hpEmbedCounted!==1)return;
if(state===0){
// Ended implies every quartile, even if a throttled tab skipped an update.
microEvent("video_p25",id);microEvent("video_p50",id);microEvent("video_p75",id);microEvent("video_p100",id);
return;
}
var dur=info.duration,cur=info.currentTime;
// A live stream reports duration 0, and a quartile of an unknown length is
// meaningless — report the play and nothing more.
if(typeof dur!=="number"||typeof cur!=="number"||!isFinite(dur)||dur<=0)return;
var pct=(cur/dur)*100;
if(pct>=25)microEvent("video_p25",id);
if(pct>=50)microEvent("video_p50",id);
if(pct>=75)microEvent("video_p75",id);
// p100 comes from playerState 0, not a threshold: the final update lands short.
}catch(err){}
});
// SYNC-END embed-video-listeners
document.addEventListener("ended",function(e){
var v=e.target;
if(!v||v.tagName!=="VIDEO")return;
if(!countedPlay(v))return;
var id=videoId(v);
// Watched to the end implies every quartile, even if a throttled background tab
// skipped a timeupdate.
microEvent("video_p25",id);microEvent("video_p50",id);microEvent("video_p75",id);microEvent("video_p100",id);
},true);

// ─── Scheduler widgets (HP-3461 phase 3) ────────────────────────────────────
// An inline Calendly/Cal.com embed is a CROSS-ORIGIN iframe, so we cannot see a
// click inside it — which is why booking_open (a click on an outbound scheduling
// link) is absent on exactly the pages most likely to take bookings. The widget
// broadcasts its own progress to the parent window, so we get told instead of
// guessing. No SDK needed; Calendly posts these regardless.
//
// SECURITY: any frame can post a message claiming to be a booking, and this ships
// on every published page, so the sender's ORIGIN must be a recognised scheduling
// host AND the message must match the provider's shape. Everything else is ignored.
// PRIVACY: only WHICH STEP is read. event.data.payload (invitee details on
// event_scheduled) is never touched.
// SYNC with app/src/lib/analytics-tracking-script.ts.
function schedulerOrigin(origin){
try{
var h=new URL(origin).hostname.toLowerCase();
for(var i=0;i<BOOK_HOSTS.length;i++){
var b=BOOK_HOSTS[i];
if(h===b||h.slice(-(b.length+1))==="."+b)return b;
}
}catch(e){}
return null;
}
// calendly.profile_page_viewed is deliberately ignored: it fires for a profile
// listing rather than a bookable calendar, so it would inflate the funnel's top.
window.addEventListener("message",function(e){
try{
var host=schedulerOrigin(e.origin);
if(!host)return;
var d=e.data;
if(!d||typeof d!=="object")return;
// Calendly's vocabulary reads off d.event and is prefixed; Cal.com's reads off
// d.type and is not. Verified against live widgets on both, plus Cal.com's own
// EventDataMap in packages/embeds/embed-core/src/sdk-action-manager.ts.
// calendly.profile_page_viewed is ignored: it fires for a profile listing rather
// than a bookable calendar, so it would inflate the top of the funnel.
//
// Cal.com HAS NO time-selection event. Its full enum (27 keys) contains nothing for
// picking a date or slot -- availabilityLoaded and slotsFetched are the widget
// LOADING options, not a visitor choosing one. So Cal.com reports 2 of the 3 steps,
// and booking_time_selected is left ABSENT for it rather than synthesised from the
// booking. Fabricating it would make every Cal.com page show a flat 100% from
// "selected" to "completed" -- a number that looks measured and is not. Absent
// already means "not measurable here" everywhere else in this feature (see the
// video quartiles), and the read side honours that.
//
// Emissions are LITERALS, one branch per step, so the sync seam in
// page-engagement-kinds.test.ts can see them -- it caught booking_completed going
// unverifiable when this was table-driven.
var name=typeof d.event==="string"?d.event:(typeof d.type==="string"?d.type:null);
if(!name)return;
if(host==="calendly.com"){
if(name==="calendly.event_scheduled"){
microEvent("booking_viewed",host);
microEvent("booking_time_selected",host);
microEvent("booking_completed",host);
// The booking is the outcome this exists to catch and the visitor usually leaves
// for the confirmation, so flush rather than wait out the 5s timer.
flush();
}else if(name==="calendly.date_and_time_selected"){
microEvent("booking_viewed",host);
microEvent("booking_time_selected",host);
}else if(name==="calendly.event_type_viewed"){
microEvent("booking_viewed",host);
}
}else if(host==="cal.com"){
// bookingSuccessfulV2 deprecates bookingSuccessful and both are still in the enum,
// so accept either -- customers sit on different embed versions.
if(name==="bookingSuccessfulV2"||name==="bookingSuccessful"){
// NOTE: no booking_time_selected here. Cal.com cannot report it.
microEvent("booking_viewed",host);
microEvent("booking_completed",host);
flush();
}else if(name==="bookerViewed"){
microEvent("booking_viewed",host);
}
}
// No acuityscheduling.com branch, and there never can be one. Its embed script
// exchanges only sizing/load/scrollTo/scrollBy/gaMeasurementId with the parent and
// takes a Google Analytics client id INWARD — there is no booking message to hear.
// Acuity reports outward only through its webhooks (HP-4041's inbound conversion
// webhook is the route). A branch here would be dead code. See lib/scheduling-hosts.
// Its origin stays accepted by the gate above, which costs nothing: an origin that
// never speaks simply never matches.
}catch(err){}
});

window.addEventListener("load",function(){
setTimeout(function(){
try{
var perf=performance.getEntriesByType("navigation")[0];
if(perf){
var ev=makeEvent("rum_timing",{loadTimeMs:Math.round(perf.loadEventEnd-perf.startTime),ttfbMs:Math.round(perf.responseStart-perf.startTime),fcpMs:undefined});
var paintEntries=performance.getEntriesByType("paint");
for(var i=0;i<paintEntries.length;i++){if(paintEntries[i].name==="first-contentful-paint"){ev.payload.fcpMs=Math.round(paintEntries[i].startTime);break;}}
buffer.push(ev);
}
}catch(e){}
},2000);
});
})();
</script><script data-page-id="hkqp68gnhsow4z4gvc31azx8" data-page-slug="6lZ-H2eoaq" data-api-url="https://htmlpub.com" data-turnstile-key="0x4AAAAAAD7Sp6X7R_qRx-Cz" data-htmlpub-forms>
(function(){
var s=document.querySelector("script[data-htmlpub-forms]");
var pageId=s.getAttribute("data-page-id");
var slug=s.getAttribute("data-page-slug");
var api=s.getAttribute("data-api-url")+"/api/forms/submit";
// Owner-mode: editor previews and the in-app shell load this script with
// data-owner="1". We still listen for submits (the editor surfaces success
// state to the user via the toolbar), but we MUST NOT fire conversion
// pixels — that would pollute the customer's Meta / TikTok pixel data
// with editor test traffic indistinguishable from real visitors.
var _hpIsOwner=s.getAttribute("data-owner")==="1";
// Preview-mode: the editor's Preview loads owner-gated (draft access + no pixel
// pollution) but must show the true visitor experience. _hpIsPreview lets visitor-UX
// blocks (multi-step paging) run even though _hpIsOwner is set; pixel/submission gates
// stay owner-only.
var _hpIsPreview=s.getAttribute("data-preview")==="1";
var _hpAutomaticConversionEventsEnabled=s.getAttribute("data-automatic-conversion-events")!=="0";
var _hpTurnstileKey=s.getAttribute("data-turnstile-key")||"";
// data-persist-hidden: fields hidden via the inspector — hidden from visitors on
// the live page but still submitted (attribution/UTM). The edit canvas (owner, NOT
// preview) skips the rule so the inspector keeps them visible with a "Hidden" chip;
// Preview applies it so it matches the published page.
if(!_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){}}
// Multiple-choice radio/checkbox width normalization is NOT injected at runtime.
// The editor bakes an inline width:auto onto each option input (idempotently), so the
// fix persists in the saved HTML and follows the normal draft -> publish flow instead
// of overriding every served form's checkboxes/radios on every page load.
// A field hidden from visitors (data-persist-hidden -> display:none) that ALSO carries native
// required cannot be focused, so on a single-step form native constraint validation would
// silently abort submit on it (the multi-step path already skips hidden fields via
// _hpValidatable). Drop the required attribute from hidden controls on the served page so
// submission is never blocked by a field the visitor can't see; the field still submits its value.
if(!_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){}}
// The form's real submit button — never one of the authored multi-step nav buttons
// (data-persist-ms-back/next), which may be type-less if hand-authored.
function _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])");}
// A checkbox group marked required-as-a-group (data-persist-group-required) is satisfied when
// AT LEAST ONE member is checked — native required on each box would wrongly demand ALL.
// Returns true if every such group within the given scope is satisfied; otherwise reports validity on
// the first empty group (clearing on the next change) and returns false.
function _hpGroupReqOK(scope){
var boxes=scope.querySelectorAll('input[type="checkbox"][data-persist-group-required]');
var groups={};
for(var i=0;i<boxes.length;i++){var n=boxes[i].name||("__hp"+i);if(!groups[n])groups[n]=[];groups[n].push(boxes[i]);}
for(var nm in groups){if(!Object.prototype.hasOwnProperty.call(groups,nm))continue;
var g=groups[nm],any=false,first=null;
for(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;}
if(first&&!any){try{first.setCustomValidity("Please select at least one option.");if(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;}}
return true;
}
// Pick a Turnstile theme (light/dark) from the first opaque background behind the form, so the
// widget matches the PAGE rather than defaulting to the visitor's system theme ("auto"). Parses
// the rgb() string without a regex escape (template-literal safe). Falls back to "auto".
function _hpPickTheme(el){
try{
var n=el;
while(n&&n.nodeType===1){
var bg=window.getComputedStyle(n).backgroundColor||"";
if(bg.indexOf("rgb")===0){
var p=bg.replace(/[^0-9.,]/g,"").split(",");
var a=p.length>3?parseFloat(p[3]):1;
if(a>0.1){var lum=(0.299*+p[0]+0.587*+p[1]+0.114*+p[2])/255;return lum>0.5?"light":"dark";}
}
n=n.parentElement;
}
}catch(e){}
return "auto";
}
// Inject the Turnstile widget on a spam-protected form via the platform-origin bridge iframe.
// The widget CANNOT render inline on a customer's custom domain: Turnstile binds a widget to a
// hostname allowlist (Enterprise caps at 200, no wildcards), so customer domains can't be listed.
// Instead we embed /turnstile-bridge served from data-api-url (a platform origin that IS on the
// allowlist); it renders the widget there and postMessages the token back (see the listener below).
// Placed ABOVE the submit's outermost row so on a multi-step footer nav it sits on its own line.
// Starts 0px (interaction-only is invisible); the bridge posts a resize only if a challenge appears.
// Idempotent; no-op without the feature flag or inside a sandboxed iframe (the /p shell renders its
// own widget on the app origin).
function _hpInjectCaptcha(f){
if(!_hpTurnstileKey||inSandbox||!f||f.querySelector("iframe[data-hp-turnstile]"))return;
var base=(s.getAttribute("data-api-url")||"");
if(base&&base.charAt(base.length-1)==="/")base=base.slice(0,-1);
if(!base)return;
var fr=document.createElement("iframe");
fr.setAttribute("data-hp-turnstile","");
fr.setAttribute("title","Spam protection");
fr.setAttribute("scrolling","no");
fr.style.cssText="width:100%;max-width:320px;height:0;border:0;display:block;overflow:hidden;";
fr.src=base+"/turnstile-bridge?theme="+encodeURIComponent(_hpPickTheme(f));
var sb=_hpFindSubmit(f);
if(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);
}
// Receive the Turnstile token from the platform-origin bridge iframe. Origin-checked against
// data-api-url. Stores the token on the owning form for the submit path; resizes the iframe when
// Managed mode shows an actual challenge (invisible/auto-pass stays 0px). One listener serves
// every captcha form on the page.
(function(){
var _hpBO="";try{_hpBO=new URL(s.getAttribute("data-api-url")||"",location.href).origin;}catch(e){}
window.addEventListener("message",function(e){
if(!_hpBO||e.origin!==_hpBO)return;
var d=e.data;if(!d||d.source!=="hp-turnstile")return;
var frs=document.querySelectorAll("iframe[data-hp-turnstile]"),host=null;
for(var i=0;i<frs.length;i++){if(frs[i].contentWindow===e.source){host=frs[i];break;}}
if(!host)return;
var form=host.closest?host.closest("form"):null;
if(d.type==="token"){if(form){form.__hpCaptchaToken=(d.token||"");form.__hpCaptchaError=false;}}
else if(d.type==="expired"){if(form)form.__hpCaptchaToken="";}
// A terminal bridge error (misconfigured/unreachable widget, keys mismatch) means no token can
// ever be produced; flag it so the submit gate fails OPEN rather than trapping the visitor forever
// (the server still fails open for the same unconfigured case). Distinct from expired (retryable).
else if(d.type==="error"){if(form){form.__hpCaptchaToken="";form.__hpCaptchaError=true;}}
else 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");}
});
})();
// Bounded wait for a Turnstile token, with a single re-challenge (decided fail policy #1/#2).
// The bridge delivers the token via postMessage, which can land slightly AFTER the visitor
// clicks submit (invisible challenge still resolving). Give it a bounded window; if none
// arrives, force ONE fresh challenge (reload the bridge iframe) and wait a final window; if
// STILL none, proceed anyway — the server accepts the lead and flags it unverified rather
// than dropping it. done() runs exactly once: as soon as a token appears, the bridge reports
// a terminal error, or the budget is spent.
function _hpWaitForCaptcha(f,done){
var WAIT=3000,STEP=100,called=false;
function finish(){if(called)return;called=true;done();}
function poll(deadline,onExpire){
if((f.__hpCaptchaToken||"")!==""||f.__hpCaptchaError){finish();return;}
if(Date.now()>=deadline){onExpire();return;}
setTimeout(function(){poll(deadline,onExpire);},STEP);
}
poll(Date.now()+WAIT,function(){
if((f.__hpCaptchaToken||"")!==""||f.__hpCaptchaError){finish();return;}
try{var fr=f.querySelector("iframe[data-hp-turnstile]");if(fr)fr.src=fr.src;}catch(e){}
poll(Date.now()+WAIT,finish);
});
}
// reCAPTCHA enforcement (HP-1475): only pages whose owner opted in emit this
// flag. It gates the client-side submit block below so pages that merely carry
// a decorative widget (the pre-HP-1475 status quo) keep working unchanged.
var _hpRecaptchaEnforced=s.getAttribute("data-recaptcha-enforced")==="1";
var corrId=0;
var pendingCallbacks={};

// Detect if we're in a sandboxed iframe
var inSandbox=false;
try{inSandbox=window!==window.parent;}catch(e){inSandbox=true;}

function cleanKey(s){return s.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_|_$/g,"").substring(0,30);}
// SYNC: mirror worker-pages/src/scripts/forms.ts and app/src/lib/extract-form-fields.ts
function 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);}
function fieldKey(el,idx){
if(el.name&&!looksLikeRawId(el.name))return el.name;
if(el.id&&!looksLikeRawId(el.id))return el.id;
var df=el.getAttribute("data-field")||el.getAttribute("data-name");
if(df&&!looksLikeRawId(df))return df;
// Type-based semantic hint. <input type="email"> without an explicit name
// should still be identified as "email" so the lead-capture extractor can
// find the address. Same for tel/url. Critical when the page builder
// (HTMLPub editor / Sacca's offers.leadpages.com pages) emits inputs
// without name attributes — without this, the email field gets keyed off
// its placeholder ("alex_at_youragency_com") and no lead is extracted.
if(el.type==="email")return "email";
if(el.type==="tel")return "phone";
if(el.type==="url")return "url";
// Sibling label pattern: <label>Foo</label><input ...> inside a wrapper.
// Common in modern form layouts. The existing closest(label) check below
// only catches WRAPPING labels, not sibling ones.
var prev=el.previousElementSibling;
if(prev&&prev.tagName==="LABEL"&&prev.textContent){var pk=cleanKey(prev.textContent);if(pk)return pk;}
if(el.placeholder){var ck=cleanKey(el.placeholder);if(ck)return ck;}
var al=el.getAttribute("aria-label");
if(al){var ck2=cleanKey(al);if(ck2)return ck2;}
if(el.id){var lbl=document.querySelector('label[for="'+el.id+'"]');if(lbl&&lbl.textContent){var ck3=cleanKey(lbl.textContent);if(ck3)return ck3;}}
var par=el.closest?el.closest("label"):null;
if(par&&par.textContent){var ck4=cleanKey(par.textContent);if(ck4)return ck4;}
return (el.type||"field")+"_"+idx;
}
// Known third-party tracking field patterns to ignore
var _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;
function _isInjected(el){
// Skip hidden inputs not present in the original page HTML.
// Third-party scripts (Customers.ai, HubSpot, etc.) inject hidden fields
// at runtime. We detect these by checking if they were present at DOMContentLoaded.
if(el.type==="hidden"&&!el.hasAttribute("data-hp-original"))return true;
return false;
}
function collect(f){
var d={};
var els=f.elements||f.querySelectorAll("input,textarea,select");
var used={};
var hasVisibleValue=false;
for(var i=0;i<els.length;i++){
var el=els[i];
if(el.type==="file"||el.type==="submit"||el.type==="button")continue;
if((el.type==="checkbox"||el.type==="radio")&&!el.checked)continue;
if(_isInjected(el))continue;
// International-phone controls: the visible national input (data-hp-phone-nat) and
// the injected country <select> (data-hp-phone-cc) are NOT collected — the hidden
// E.164 companion carries the value under the field's name. The national input still
// counts toward hasVisibleValue so a phone-only form isn't treated as empty.
if(el.hasAttribute&&el.hasAttribute("data-hp-phone-nat")){if(el.value&&el.value.trim())hasVisibleValue=true;continue;}
if(el.hasAttribute&&el.hasAttribute("data-hp-phone-cc"))continue;
// Empty URL-param inputs are dropped from the payload so direct-traffic
// submissions from a form with data-persist-capture-utms (or hand-authored
// data-persist-url-param inputs) don't ship 7 blank attribution columns
// into every submission, integration webhook, CSV export, and CAPI event.
// Author-provided value="direct" (or any non-empty default) still ships.
if(el.type==="hidden"&&!(el.value||"").trim()&&el.hasAttribute("data-persist-url-param"))continue;
var key=fieldKey(el,i);
if(_blocklist.test(key))continue;
if(used[key]){key=key+"_"+i;}
used[key]=true;
d[key]=el.value;
if(el.type!=="hidden"&&el.value&&el.value.trim())hasVisibleValue=true;
}
// Require at least one visible field with a value — pure hidden-field submissions are noise
if(!hasVisibleValue)return {};
return d;
}

// Generate a UUID v4 (browser-side). Used as the eventID that ties the
// fbq/ttq browser event to the matching CAPI event the server sends via
// the integration queue → provider workflow, so pixel platforms can dedup.
function _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);});}

// Fire the canonical "Lead" event on every installed pixel platform. Only
// platforms that booted (window.fbq, window.ttq, window.gtag, window.dataLayer)
// receive a call — others no-op cleanly. The eventID is threaded into the
// provider-specific dedup parameter (Meta: eventID, TikTok: event_id).
function _hpFireLead(formIdent,data,eventId){
  if(_hpIsOwner||!_hpAutomaticConversionEventsEnabled)return;
  var em=null,ph=null,fn=null,ln=null;
  for(var k in data){
    if(!Object.prototype.hasOwnProperty.call(data,k))continue;
    var v=(data[k]||"").toString().trim();
    if(!v)continue;
    var kl=k.toLowerCase();
    if(!em&&/email/.test(kl)&&/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v))em=v;
    else if(!ph&&/phone|tel|mobile/.test(kl))ph=v;
    else if(!fn&&/first.?name|fname/.test(kl))fn=v;
    else if(!ln&&/last.?name|lname/.test(kl))ln=v;
  }
  var custom={};if(em)custom.em=em;if(ph)custom.ph=ph;if(fn)custom.fn=fn;if(ln)custom.ln=ln;
  try{if(window.fbq)window.fbq("track","Lead",custom,{eventID:eventId});}catch(e){}
  try{if(window.ttq&&window.ttq.track)window.ttq.track("SubmitForm",custom,{event_id:eventId});}catch(e){}
  try{if(window.gtag)window.gtag("event","generate_lead",{event_id:eventId,lp_form_id:formIdent});}catch(e){}
  try{if(window.dataLayer&&window.dataLayer.push)window.dataLayer.push({event:"lp_form_submit",lp_event_id:eventId,lp_form_id:formIdent});}catch(e){}
}

// First-interaction hook: when the visitor focuses any form field for the
// first time on this page, fire the platform's "started a form" event.
// Once per form per page (debounced via _hpFiredInitFocus).
var _hpFiredInitFocus={};
function _hpFireInitiateCheckout(formIdent){
  if(_hpIsOwner||!_hpAutomaticConversionEventsEnabled)return;
  if(_hpFiredInitFocus[formIdent])return;
  _hpFiredInitFocus[formIdent]=true;
  var eid=_hpUuid();
  try{if(window.fbq)window.fbq("track","InitiateCheckout",{},{eventID:eid});}catch(e){}
  try{if(window.ttq&&window.ttq.track)window.ttq.track("InitiateCheckout",{},{event_id:eid});}catch(e){}
  try{if(window.gtag)window.gtag("event","form_start",{event_id:eid,lp_form_id:formIdent});}catch(e){}
  try{if(window.dataLayer&&window.dataLayer.push)window.dataLayer.push({event:"lp_form_start",lp_event_id:eid,lp_form_id:formIdent});}catch(e){}
}
// HP-3461 — the same first-interaction moment, reported to OUR analytics. It has
// been detected here all along and told to Facebook, TikTok, Google and GTM
// (above) while our own funnel never heard about it, so "169 started the form, 17
// finished" was unanswerable from first-party data.
//
// Deliberately NOT inside _hpFireInitiateCheckout: that is gated on
// _hpAutomaticConversionEventsEnabled, the customer's AD-PIXEL preference, and
// switching off automatic ad conversion events must not blind our own analytics.
//
// Requires a real visitor gesture. A focusin alone is not evidence of intent:
// <input autofocus>, a popup that focuses its email field on open, and browser
// autofill all fire it with nobody touching the keyboard, which would report a
// form_start for ~every session and make the abandonment number this feature
// exists to answer ("169 started, 17 finished") read as a 100% start rate on a
// page nobody typed into. _hpFireInitiateCheckout above has always had this bug;
// it only ever fed ad pixels, so nobody noticed.
//
// Owner previews self-gate: analyticsTrackingScript returns on data-owner="1"
// BEFORE registering window.__hpupPushEvent, so this finds no bridge and no-ops.
// (The edge serves only public traffic, so the same code is correct on both
// hosts. Known gap, pre-existing and not widened by form_start: the Nova page
// switcher omits ?token=, so isOwnerForAnalytics misses it and page_view /
// scroll_depth / element_click already report from that path.)
// SYNC with worker-pages/src/scripts/forms.ts — these files must stay byte-identical.
var _hpFormStartGesture=false;
function _hpMarkFormGesture(){_hpFormStartGesture=true;}
document.addEventListener("pointerdown",_hpMarkFormGesture,true);
document.addEventListener("keydown",_hpMarkFormGesture,true);
document.addEventListener("touchstart",_hpMarkFormGesture,{passive:true,capture:true});
// Object.create(null), not {}: the key is formId(), i.e. the author-controlled
// id/name attribute. With a bare object a form named "constructor" or "toString"
// reads as already-fired on its very first focus and never reports at all, and
// "__proto__" cannot even be latched — the page would show submissions with no
// starts whatsoever.
var _hpFiredFormStart=Object.create(null);
function _hpReportFormStart(formIdent){
  if(!_hpFormStartGesture)return;
  var k=String(formIdent||"");
  if(_hpFiredFormStart[k])return;
  _hpFiredFormStart[k]=true;
  try{if(window.__hpupPushEvent)window.__hpupPushEvent("page_engagement",{kind:"form_start",id:k.slice(0,128)});}catch(e){}
}
document.addEventListener("focusin",function(e){
  var el=e.target;
  if(!el||(el.tagName!=="INPUT"&&el.tagName!=="TEXTAREA"&&el.tagName!=="SELECT"))return;
  if(el.type==="hidden"||el.type==="submit"||el.type==="button")return;
  var f=el.closest&&(el.closest("form")||el.closest("[data-htmlpub-fake-form]"));
  if(!f)return;
  _hpReportFormStart(formId(f));
  _hpFireInitiateCheckout(formId(f));
},true);

// Set _hp_email cookie when we find an email in form data (for CRM personalization)
function setEmailCookie(data){
var email=null;
for(var k in data){
if(!data.hasOwnProperty(k))continue;
var v=(data[k]||"").toString();
if(/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim())){email=v.trim();break;}
}
if(email){
try{var enc=btoa(email.toLowerCase().trim());document.cookie="_hp_email="+encodeURIComponent(enc)+";path=/;max-age=31536000;SameSite=Lax;Secure";}catch(e){}
}
}

// Read _hpid from URL and set cookie for future visits
(function(){
var p=new URLSearchParams(window.location.search);
var hpid=p.get("_hpid");
if(hpid){
try{var decoded=atob(hpid);if(decoded.indexOf("@")!==-1){document.cookie="_hp_email="+encodeURIComponent(hpid)+";path=/;max-age=31536000;SameSite=Lax;Secure";}}catch(e){}
}
})();

// URL-parameter auto-populate. Any input/textarea/select with
// data-persist-url-param="<name>" gets its value set from
// URLSearchParams(location.search).get(<name>) on script boot. Enables UTM /
// gclid / fbclid / any-custom-param capture into form submissions without a
// custom head script. An author (or the AI builder) drops
//   <input type="hidden" name="utm_source" data-persist-url-param="utm_source">
// and the runtime fills it in from ?utm_source=... on page load. Runs
// BEFORE the data-hp-original tagging pass below so the populated hidden
// inputs get stamped and pass _isInjected. utm_* names are not on
// _blocklist. Missing params leave any pre-existing value intact so authors
// can specify a fallback via a plain value="" on the input. Selector is
// scoped to form fields so a stray attribute on a non-form element is a
// no-op rather than a silent property write. Repeated params
// (?utm_source=a&utm_source=b) resolve to the first value ("a") per the
// URLSearchParams.get() spec.
try{
var _hpUrlParams=new URLSearchParams(window.location.search);
document.querySelectorAll("input[data-persist-url-param],textarea[data-persist-url-param],select[data-persist-url-param]").forEach(function(el){
var pname=el.getAttribute("data-persist-url-param");
if(!pname)return;
var pv=_hpUrlParams.get(pname);
if(pv!==null&&pv!=="")el.value=pv;
});
}catch(e){}

// Standard-UTM auto-capture. A form with data-persist-capture-utms gets the
// seven standard attribution params (utm_source, utm_medium, utm_campaign,
// utm_term, utm_content, gclid, fbclid) auto-injected as hidden inputs +
// populated from URLSearchParams if the URL carries them. Editor UI toggles
// the attribute; runtime does the injection. Idempotent by name, so a form
// with a pre-existing named hidden input (author-written or AI-authored)
// keeps its own definition and is only value-populated by the URL-param
// pass above. Runs BEFORE the data-hp-original tagging pass below so the
// injected inputs get stamped and pass _isInjected on submission.
try{
var _HP_STD_UTMS=["utm_source","utm_medium","utm_campaign","utm_term","utm_content","gclid","fbclid"];
document.querySelectorAll("form[data-persist-capture-utms]").forEach(function(f){
_HP_STD_UTMS.forEach(function(n){
if(f.querySelector('input[name="'+n+'"]'))return;
var inp=document.createElement("input");
inp.type="hidden";
inp.name=n;
inp.setAttribute("data-persist-url-param",n);
var v=_hpUrlParams.get(n);
if(v!==null&&v!=="")inp.value=v;
f.appendChild(inp);
});
});
}catch(e){}

// Cloudflare Turnstile CAPTCHA. A form with data-persist-captcha gets the bridge iframe
// injected before its submit control when the feature flag is set. TOP-LEVEL path only — in
// the sandboxed /p/ iframe the parent shell renders the widget (opaque origin can't run
// Turnstile). Owner/editor previews skip it. The bridge posts the token back (listener near
// _hpInjectCaptcha), stashed on the form and read by submitData as the top-level captchaToken.
if(!_hpIsOwner&&!inSandbox){try{
document.querySelectorAll("form[data-persist-captcha]").forEach(function(f){
// Multi-step forms defer injection to the LAST step (see the multi-step render), so the
// widget renders fresh + visible next to the submit and never appears on an earlier step.
if(f.hasAttribute("data-persist-multistep"))return;
_hpInjectCaptcha(f);
});
}catch(e){}}

// ── Multi-step forms ── a form[data-persist-multistep] shows one [data-persist-step]
// container at a time, and validates the current step's required fields before advancing.
// Navigation uses the AUTHORED, page-styled buttons the editor creates —
// [data-persist-ms-next] (advance) + [data-persist-ms-back] (retreat) — with the real submit
// shown only on the last step. Legacy / hand-authored forms with no such buttons get a
// transient injected Back/Next + progress line (data-htmlpub-*, never saved) so they still
// work. The controller is stashed on the form as __hpMultiStep so the submit handler advances
// instead of submitting until the last step. The edit canvas (owner, NOT preview) skips this
// so every field stays visible for editing; Preview (data-preview="1") runs it.
if(!_hpIsOwner||_hpIsPreview){try{
document.querySelectorAll("form[data-persist-multistep]").forEach(function(f){
var steps=[].slice.call(f.querySelectorAll("[data-persist-step]"));
if(steps.length<2){
// A single-step form has no navigation — hide any authored Back/Next so they don't render
// as dead buttons; the submit stays as the only action.
var _hpDead=f.querySelectorAll("[data-persist-ms-back],[data-persist-ms-next]");
for(var _hpd=0;_hpd<_hpDead.length;_hpd++)_hpDead[_hpd].style.display="none";
// A degenerate multi-step form (0 or 1 real steps) behaves like a plain form, but the boot-time
// captcha pass skipped it (it carries data-persist-multistep) and the last-step inject below
// never runs. Inject the widget here — otherwise the server enforces a token this page never
// produces and every submission is rejected (100% lead loss), while the editor still shows the
// placeholder. Mirrors the non-multistep boot path (owner/preview skip via !_hpIsOwner).
if(!_hpIsOwner&&f.hasAttribute("data-persist-captcha"))_hpInjectCaptcha(f);
return;
}
steps.sort(function(a,b){return (parseInt(a.getAttribute("data-persist-step"),10)||0)-(parseInt(b.getAttribute("data-persist-step"),10)||0);});
// Suppress native whole-form constraint validation: a required field in a
// not-yet-reached (display:none) step would otherwise abort submission and block
// Enter-to-advance. We validate each step in JS instead (validateStep on every hop
// + the last step on final submit), so required fields are still enforced.
f.noValidate=true;
// Exclude the authored nav buttons so a type-less [data-persist-ms-*] button is never picked as the submit.
var submit=_hpFindSubmit(f);
var submitDisp=submit?submit.style.display:"";
var cur=0;
// Prefer the authored nav buttons; fall back to injecting them for legacy forms.
var next=f.querySelector("[data-persist-ms-next]");
var back=f.querySelector("[data-persist-ms-back]");
var prog=null;
if(!next){
prog=document.createElement("div");
prog.setAttribute("data-htmlpub-ms-progress","");
prog.style.cssText="font-family:system-ui,sans-serif;font-size:13px;opacity:0.7;margin-bottom:8px;";
if(steps[0].parentNode)steps[0].parentNode.insertBefore(prog,steps[0]);
var nav=document.createElement("div");
nav.setAttribute("data-htmlpub-ms-nav","");
nav.style.cssText="display:flex;gap:8px;margin-top:8px;";
// Reuse an authored Back if the form has one (only Next was missing) — don't inject a duplicate.
if(!back){back=document.createElement("button");back.type="button";back.setAttribute("data-htmlpub-ms-back","");back.textContent="Back";}
next=document.createElement("button");next.type="button";next.setAttribute("data-htmlpub-ms-next","");next.textContent="Next";
if(submit&&submit.className){next.className=submit.className;if(!back.className)back.className=submit.className;}
nav.appendChild(back);nav.appendChild(next);
var lastStep=steps[steps.length-1];
if(lastStep.parentNode)lastStep.parentNode.insertBefore(nav,lastStep.nextSibling);
}
var nextDisp=next?next.style.display:"";
var backDisp=back?back.style.display:"";
function render(){
for(var i=0;i<steps.length;i++)steps[i].style.display=(i===cur?"":"none");
if(prog)prog.textContent="Step "+(cur+1)+" of "+steps.length;
var last=cur===steps.length-1;
if(back)back.style.display=cur>0?backDisp:"none";
if(next)next.style.display=last?"none":nextDisp;
if(submit)submit.style.display=last?submitDisp:"none";
// Spam protection: inject the Turnstile widget the moment the visitor reaches the last step
// (fresh + visible so its token populates reliably — hiding it via display:none does not).
if(last&&!_hpIsOwner&&f.hasAttribute("data-persist-captcha"))_hpInjectCaptcha(f);
}
function _hpValidatable(c){
// Skip fields the visitor can't see/act on — a data-persist-hidden (display:none) or
// disabled required field can't show a validation bubble and would silently trap them.
if(c.disabled)return false;
if(c.closest&&c.closest("[data-persist-hidden]"))return false;
return typeof c.checkValidity==="function";
}
function validateStep(){
var ctrls=steps[cur].querySelectorAll("input,select,textarea");
for(var i=0;i<ctrls.length;i++){
if(_hpValidatable(ctrls[i])&&!ctrls[i].checkValidity()){
if(typeof ctrls[i].reportValidity==="function")ctrls[i].reportValidity();
return false;
}
}
return _hpGroupReqOK(steps[cur]);
}
function validateOutside(){
// Required controls authored OUTSIDE any step container are never in validateStep's scope;
// f.noValidate is on form-wide, so validate them on final submit or they'd submit empty.
var all=f.querySelectorAll("input,select,textarea");
for(var i=0;i<all.length;i++){
var c=all[i];
if(c.closest&&c.closest("[data-persist-step]"))continue;
if(_hpValidatable(c)&&!c.checkValidity()){
if(typeof c.reportValidity==="function")c.reportValidity();
return false;
}
}
return true;
}
function advance(){if(cur<steps.length-1&&validateStep()){cur++;render();}}
if(next)next.addEventListener("click",function(e){if(e&&e.preventDefault)e.preventDefault();advance();});
if(back)back.addEventListener("click",function(e){if(e&&e.preventDefault)e.preventDefault();if(cur>0){cur--;render();}});
// validate() re-checks the CURRENT step + any required controls outside all steps — the
// submit handler calls it on the last step before sending (native validation is off).
f.__hpMultiStep={isLast:function(){return cur>=steps.length-1;},advance:advance,validate:function(){return validateStep()&&validateOutside();}};
render();
});
}catch(e){}}

// ── International phone ── input[type=tel][data-persist-phone] gets a country
// dial-code <select> injected before it + a hidden E.164 companion that carries the
// real submitted value (+<dial><national digits>) under the field's name. The visible
// tel input keeps the national number for display and is excluded from collect()
// (data-hp-phone-nat); the <select> is excluded too (data-hp-phone-cc). On every
// input / country change the E.164 is reassembled and validated — E.164 spec (7–15
// total digits) plus a GENEROUS per-country national-length range where the length is
// well known — via setCustomValidity, so native validation blocks a malformed submit
// on single-step forms and validateStep() enforces it on multi-step ones. Runs BEFORE
// the data-hp-original tagging pass so the hidden companion is stamped and collected.
// Runs in owner mode too so the editor preview shows the selector (the validity flag
// is harmless there — owner previews don't submit real leads). data-persist-phone /
// data-persist-phone-country are persistent (never stripped from the AI diff); the
// injected select + companion are data-hp-* (transient) and never saved.
try{
var _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]];
var _hpPhoneByIso={};for(var _hppi=0;_hppi<_HP_PHONE.length;_hppi++){_hpPhoneByIso[_HP_PHONE[_hppi][0]]=_HP_PHONE[_hppi];}
var _hpDigits=function(s){return (s||"").replace(/[^0-9]/g,"");};
var _hpFlag=function(iso){try{return String.fromCodePoint(0x1F1E6+(iso.charCodeAt(0)-65))+String.fromCodePoint(0x1F1E6+(iso.charCodeAt(1)-65));}catch(e){return "";}};
// Reduce the typed digits to the country's national significant number (NSN): strip a
// user-typed country code (guarded by the expected NSN length so a valid number is never
// shortened) and a national trunk prefix "0" (guarded so it never drops below the country
// minimum; suppressed for keepLeadingZero countries like Italy). Lead-safe: each strip
// fires only when it yields a still-plausible NSN, so "07911 123456", "06 12 34 56 78" and
// "1 (415) 555-2671" all assemble to correct E.164 and pass validation.
var _hpNSN=function(nat,cur){
var dial=cur[2],min=cur[3],max=cur[4],keep0=cur[5];
if(dial&&nat.length>dial.length&&nat.substring(0,dial.length)===dial){
var rest=nat.substring(dial.length);
var restT=(!keep0&&rest.charAt(0)==="0")?rest.replace(/^0+/,""):rest;
if(min&&max){
// Known national length: keep whichever candidate fits the country's range.
if(restT.length>=min&&restT.length<=max)nat=restT;
else if(rest.length>=min&&rest.length<=max)nat=rest;
}
// Variable-length country (min/max unknown): do NOT strip the leading dial-code digits —
// for those countries a real national number can legitimately begin with its own dial
// code, and chopping it corrupts the number (F4). "+"-prefixed input is handled before
// _hpNSN, so this only affects the ambiguous no-"+" case; keep the digits as typed.
}
if(!keep0&&nat.charAt(0)==="0"){var s=nat.replace(/^0+/,"");if(!min||s.length>=min)nat=s;}
// Russia/Kazakhstan (dial 7) use "8" as the national trunk prefix, not "0". Strip a lone
// leading "8", guarded by the country minimum so a valid NSN is never shortened (F3).
if(dial==="7"&&nat.charAt(0)==="8"){var s8=nat.replace(/^8/,"");if(!min||s8.length>=min)nat=s8;}
return nat;
};
document.querySelectorAll('input[type="tel"][data-persist-phone]').forEach(function(inp){
if(inp.getAttribute("data-hp-phone-on")==="1")return;
inp.setAttribute("data-hp-phone-on","1");
// Key the E.164 companion the SAME way collect()/extract-form-fields key a plain tel
// input (name -> id -> data-field -> "phone"), computed BEFORE any DOM mutation so
// enabling international never changes which lead column the number lands in.
var nm=fieldKey(inp,0);
var iso=(inp.getAttribute("data-persist-phone-country")||"US").toUpperCase();
if(!_hpPhoneByIso[iso])iso="US";
// Custom country combobox. A native <select> can't show a compact "flag +dial" when
// CLOSED and full country names when OPEN: its closed text IS the selected option's
// text, and a same-value close fires no event to swap it back. So we build a small
// self-contained combobox — a styled trigger button showing "flag +dial" + a panel
// listing full country names. curIso tracks the selection for E.164 assembly. Lay it
// out side by side with the number in a flex row (standard international-phone layout);
// the trigger copies the number input's border/radius/padding/font/colors so it matches
// whatever the page's forms look like (align-items:stretch matches their heights).
var curIso=iso;
// Brand accent for the selected-row tint + checkmark, sampled from the form's primary
// (submit) button background so the dropdown matches the page. A light tint keeps the
// dark option text legible. Falls back to a neutral blue; transparent / near-white
// buttons are ignored so the accent stays visible.
var _hpAccent="#2563eb",_hpAccentTint="rgba(37,99,235,0.12)";
try{
var _hpBtnForm=inp.closest?inp.closest("form"):null;
var _hpBtn=_hpBtnForm?_hpFindSubmit(_hpBtnForm):null;
if(_hpBtn){
var _bg=window.getComputedStyle(_hpBtn).backgroundColor||"";
var _pp=_bg.replace(/[^0-9,.]/g,"").split(",");
var _rr=parseInt(_pp[0],10),_gg=parseInt(_pp[1],10),_bb=parseInt(_pp[2],10),_aa=_pp.length>3?parseFloat(_pp[3]):1;
if(!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)";}
}
}catch(e){}
// Use the AUTHORED wrapper (data-persist-phone-field) that the editor saved, so the
// published layout is identical to the edit canvas. Create one only as a fallback for
// legacy pages saved before the authored wrapper existed (served pages aren't re-saved,
// so a runtime-created wrapper is harmless there).
var wrap=inp.closest?inp.closest("[data-persist-phone-field]"):null;
if(!wrap){
wrap=document.createElement("span");
wrap.setAttribute("data-persist-phone-field","");
wrap.style.cssText="display:flex;align-items:stretch;gap:6px;width:100%;position:relative;";
if(inp.parentNode)inp.parentNode.insertBefore(wrap,inp);
wrap.appendChild(inp);
}
if(!wrap.style.position)wrap.style.position="relative";
// A <span role=button>, NOT a <button>: the widget sits inside the field's <label>, and
// a <label> forwards clicks to its first LABELABLE descendant. A <button> would become
// that control, so clicking a list item fires a synthetic click on the trigger and
// re-opens the panel. A <span> isn't labelable, so the label keeps targeting the number
// input; preventDefault on the trigger/list clicks stops any residual label forwarding.
var trig=document.createElement("span");
trig.setAttribute("role","button");
trig.setAttribute("tabindex","0");
trig.setAttribute("data-hp-phone-cc","");
trig.setAttribute("aria-haspopup","listbox");
trig.setAttribute("aria-expanded","false");
trig.setAttribute("aria-label","Country calling code");
trig.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;";
try{
var _cs=window.getComputedStyle(inp);
var _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"];
for(var _cpi=0;_cpi<_cp.length;_cpi++){try{if(_cs[_cp[_cpi]])trig.style[_cp[_cpi]]=_cs[_cp[_cpi]];}catch(e){}}
}catch(e){}
var trigLabel=document.createElement("span");
trig.appendChild(trigLabel);
var trigCaret=document.createElement("span");trigCaret.textContent="▾";trigCaret.style.cssText="opacity:0.55;font-size:0.8em;";
trig.appendChild(trigCaret);
var panel=document.createElement("ul");
panel.setAttribute("role","listbox");
panel.setAttribute("data-hp-phone-list","");
panel.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;";
// Hover + selected-item styling (one shared, id-guarded stylesheet — inline styles can't
// express :hover / ::after). Injected on the live page only; never touches the editor.
if(!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);}
panel.style.setProperty("--hp-accent",_hpAccent);
panel.style.setProperty("--hp-accent-tint",_hpAccentTint);
for(var pci=0;pci<_HP_PHONE.length;pci++){
var pc=_HP_PHONE[pci];
var li=document.createElement("li");
li.setAttribute("role","option");
li.setAttribute("data-iso",pc[0]);
li.setAttribute("data-dial",pc[2]);
li.setAttribute("aria-selected",pc[0]===iso?"true":"false");
li.textContent=_hpFlag(pc[0])+" "+pc[1]+" +"+pc[2];
li.style.cssText="padding:7px 14px;cursor:pointer;white-space:nowrap;";
panel.appendChild(li);
}
var renderTrig=function(){var c=_hpPhoneByIso[curIso];trigLabel.textContent=_hpFlag(curIso)+" +"+(c?c[2]:"");};
var closePanel=function(){panel.style.display="none";trig.setAttribute("aria-expanded","false");};
var openPanel=function(){panel.style.display="block";trig.setAttribute("aria-expanded","true");};
inp.style.flex="1";
inp.style.minWidth="0";
wrap.insertBefore(trig,inp); // trigger sits before the number input (input already in the wrap)
wrap.appendChild(panel);
var hid=document.createElement("input");
hid.type="hidden";
hid.name=nm;
hid.setAttribute("data-hp-phone-e164","");
wrap.appendChild(hid);
inp.setAttribute("data-hp-phone-nat","");
inp.removeAttribute("name");
var _hpSyncPhone=function(){
var raw=(inp.value||"").trim();
var cur=_hpPhoneByIso[curIso];
var dial=cur?cur[2]:"";
var e164="",nsnLen=0,isIntl=false;
if(raw.charAt(0)==="+"){e164="+"+_hpDigits(raw);isIntl=true;}
else{var nsn=_hpNSN(_hpDigits(raw),cur);nsnLen=nsn.length;e164=nsn?("+"+dial+nsn):"";}
hid.value=e164;
var msg="";
if(raw){
var total=_hpDigits(e164).length;
// Hard rules only: E.164 spec (7-15 total digits) + a per-country MINIMUM NSN length
// (catches a genuinely incomplete number, e.g. a US local number with no area code). No
// per-country maximum -> an over-long entry (usually the visitor including extra
// country/trunk digits) is accepted, never rejected. A "+"-prefixed entry skips the
// per-country floor entirely (the visitor owns their own format).
if(total<7||total>15)msg="Enter a valid phone number.";
else if(!isIntl&&nsnLen&&cur&&cur[3]&&nsnLen<cur[3])msg="Enter a valid "+cur[1]+" phone number.";
}
if(typeof inp.setCustomValidity==="function")inp.setCustomValidity(msg);
};
var 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();};
panel.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){}}});
trig.addEventListener("click",function(e){e.preventDefault();if(panel.style.display==="block")closePanel();else openPanel();});
trig.addEventListener("keydown",function(e){if(e.key==="Escape"){closePanel();}else if(e.key==="ArrowDown"||e.key==="Enter"||e.key===" "){e.preventDefault();openPanel();}});
document.addEventListener("click",function(e){if(!wrap.contains(e.target))closePanel();});
inp.addEventListener("input",_hpSyncPhone);
inp.addEventListener("blur",_hpSyncPhone);
renderTrig();
_hpSyncPhone();
});
}catch(e){}

// Tag all hidden inputs present in the original HTML so we can distinguish
// them from ones injected later by third-party scripts (Customers.ai, etc.)
document.querySelectorAll('input[type="hidden"]').forEach(function(el){el.setAttribute("data-hp-original","1");});

function showNotice(f,text,ok){
var m=document.createElement("div");
m.textContent=text;
m.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");
f.parentNode.insertBefore(m,f.nextSibling);
setTimeout(function(){m.remove()},5000);
}
// showMsg wraps showNotice; the optional 3rd arg lets callers override the default success/error
// copy (e.g. the Turnstile "still verifying" gate). Omit it for the standard success/error notices.
function 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);}
// reCAPTCHA v2 (HP-1475). The response token lives in a hidden
// <textarea name="g-recaptcha-response"> that grecaptcha creates inside the
// widget's container; scoping the lookup to the submitted form/container picks
// the right token when a page renders more than one widget.
function _hpCaptchaToken(f){
try{var ta=f.querySelector&&f.querySelector('textarea[name="g-recaptcha-response"]');return ta&&ta.value?ta.value:"";}catch(e){return "";}
}

function formId(f){
if(f.tagName==="FORM")return f.id||f.getAttribute("name")||"form-"+([].indexOf.call(document.forms,f));
return f.id||"section-"+([].indexOf.call(document.querySelectorAll("[data-htmlpub-fake-form]"),f));
}

// Listen for postMessage responses from parent shell
if(inSandbox){
window.addEventListener("message",function(e){
var d=e.data;
if(d&&d.type==="htmlpub-form-submit-result"&&d.correlationId){
var cb=pendingCallbacks[d.correlationId];
if(cb){
delete pendingCallbacks[d.correlationId];
cb(d.success,d.captchaRequired,d.error);
}
}
});
}

function isSafeRedirectUrl(url){
if(!url)return false;
if(/^https?:\/\//i.test(url))return true;
if(url.charAt(0)==="/"&&url.charAt(1)!=="/")return true;
return false;
}
// 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).
function stripTrailingSlash(url){
try{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){}
return url;
}
function maybeRedirectUrl(url){
if(window.htmlpubEditorMode)return;
if(!isSafeRedirectUrl(url))return;
if(/\/api\/forms\/submit$/.test(url))return;
setTimeout(function(){window.location.href=stripTrailingSlash(url);},600);
}
function maybeRedirect(f){maybeRedirectUrl(f.getAttribute("action"));}
function reportFail(kind,status,fident){
try{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){}
}
function submitData(f,data){
var btn=_hpFindSubmit(f);
if(btn)btn.disabled=true;

// Generate one eventID per submission, threaded through three places:
//   (a) the JSON payload to /api/forms/submit → integration queue
//   (b) the postMessage to the parent shell (which forwards to the same API)
//   (c) the browser pixel events fired on success below
// Both browser and server emit the SAME id to the same downstream provider
// so Meta / TikTok dedup them as a single conversion.
var eventId=_hpUuid();
var fident=formId(f);
// Per-form Turnstile token — delivered by the bridge iframe via postMessage (see the listener
// near _hpInjectCaptcha) and stashed on the form element.
var _hpCapTok=(f.__hpCaptchaToken||"");
// stable editor-stamped form id → native lead-magnet routing; "" when unbound
var fkey=f.getAttribute("data-persist-form-id")||"";

// reCAPTCHA gate (HP-1475): when the page enforces reCAPTCHA but the visitor
// hasn't solved the challenge, block the submit with an inline error instead
// of POSTing an unverifiable payload the server will reject anyway. Gated on
// the enforcement flag (not mere widget presence) so pages with a decorative
// widget are unaffected. The server remains the security boundary — it
// re-verifies the token against Google. A form that opts into per-form Turnstile
// (data-persist-captcha) is handled by the Turnstile gate below instead — skip it here so a
// Turnstile form on a reCAPTCHA-enforced page isn't blocked for lacking a reCAPTCHA token
// (mirrors the server, which skips its reCAPTCHA block for forms in captchaFormIds).
var captchaToken=_hpCaptchaToken(f);
if(_hpRecaptchaEnforced&&!captchaToken&&!(f.hasAttribute&&f.hasAttribute("data-persist-captcha"))){
if(btn)btn.disabled=false;
showNotice(f,"Please complete the reCAPTCHA challenge before submitting.",false);
reportFail("captcha_required",0,fident);
return;
}

if(inSandbox){
var id="fs-"+(++corrId);
var timer=setTimeout(function(){
delete pendingCallbacks[id];
if(btn)btn.disabled=false;
reportFail("timeout",0,fident);showMsg(f,false);
},10000);

pendingCallbacks[id]=function(success,captchaRequired,errMsg){
clearTimeout(timer);
if(btn)btn.disabled=false;
if(success){setEmailCookie(data);_hpFireLead(fident,data,eventId);showMsg(f,true);if(typeof f.reset==="function")f.reset();maybeRedirect(f);}
else if(captchaRequired){showMsg(f,false,"Spam protection is still verifying — please wait a moment and submit again.");}
else{reportFail("relay_error",0,fident);showMsg(f,false,errMsg||undefined);}
};

var vid="";try{vid=localStorage.getItem("_hp_vid")||"";}catch(e){}
parent.postMessage({type:"htmlpub-form-submit",correlationId:id,pageId:pageId,pageSlug:slug,formIdentifier:fident,formKey:fkey,data:data,visitorId:vid,eventId:eventId,captchaToken:(_hpCapTok||captchaToken),formNeedsCaptcha:!!(f.hasAttribute&&f.hasAttribute("data-persist-captcha"))},"*");
}else{
// Send in top-level (direct fetch) mode. Wrapped in _hpProceed so it can be deferred
// until the Turnstile token has had a bounded chance to arrive (see _hpWaitForCaptcha).
function _hpProceed(){
var vid2="";try{vid2=localStorage.getItem("_hp_vid")||"";}catch(e){}
// The captcha token rides EVERY attempt (not just the first). It is single-use,
// but the server checks its verified-eventId cache before re-verifying, so a
// re-sent already-spent token short-circuits to pass, while a token that was never
// spent (attempt 0 failed at the transport / rate-limit layer before verify) still
// verifies on retry. Stripping it from retries silently dropped leads on captcha
// forms whenever attempt 0 hit a 429/5xx/network error. [HP-2467] Prefer the per-form
// Turnstile token; fall back to the reCAPTCHA token (a form uses one mechanism). Read
// the FRESHEST token here — it may have arrived via postMessage during the bounded wait,
// after _hpCapTok was snapshotted at the top of submitData.
var _capTok=(f.__hpCaptchaToken||"")||_hpCapTok||captchaToken;
var _hpBody=JSON.stringify({pageId:pageId,pageSlug:slug,formIdentifier:fident,formKey:fkey,data:data,visitorId:vid2,eventId:eventId,captchaToken:_capTok});
// Retry transient failures so a network blip / 5xx / 429 doesn't silently lose
// a lead. Every attempt reuses the same eventId, so the server dedups to
// exactly one row + one set of side-effects. Backoff: immediate, 15s, 30s,
// 2min, then give up. Failed submits don't redirect, so the page stays open
// and the delayed retries can run. The submit button stays disabled across
// retries to prevent a manual re-submit (which would mint a new eventId). [HP-2467]
var _hpDelays=[0,15000,30000,120000];
function _hpAttempt(n){
fetch(api,{method:"POST",keepalive:true,headers:{"Content-Type":"application/json"},body:_hpBody}).then(function(r){
if(r.ok){if(btn)btn.disabled=false;setEmailCookie(data);_hpFireLead(fident,data,eventId);showMsg(f,true);if(typeof f.reset==="function")f.reset();
// The Turnstile token is single-use; after a successful submit clear it and reload the bridge
// iframe so a subsequent submit on the same (non-reset / non-redirected) form gets a FRESH token
// instead of re-sending the spent one and being rejected.
try{f.__hpCaptchaToken="";f.__hpCaptchaError=false;var _hpCf=f.querySelector("iframe[data-hp-turnstile]");if(_hpCf)_hpCf.src=_hpCf.src;}catch(e){}
maybeRedirect(f);return;}
if((r.status>=500||r.status===429)&&n<_hpDelays.length){setTimeout(function(){_hpAttempt(n+1);},_hpDelays[n]);return;}
if(btn)btn.disabled=false;reportFail("http_error",r.status,fident);
// A 403 on a captcha-protected form means the token was spent/invalid — refresh the widget
// so a retry gets a fresh one (otherwise "try again" resends the same rejected token).
try{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){}
// Surface the server's specific message (e.g. "CAPTCHA verification failed") instead of a
// generic one; fall back to the generic message when the body isn't JSON.
try{r.json().then(function(d){showMsg(f,false,(d&&d.error)||undefined);},function(){showMsg(f,false);});}catch(e){showMsg(f,false);}
}).catch(function(){
if(n<_hpDelays.length){setTimeout(function(){_hpAttempt(n+1);},_hpDelays[n]);return;}
if(btn)btn.disabled=false;reportFail("network",0,fident);showMsg(f,false);
});
}
_hpAttempt(0);
}
// Spam-protected form whose Turnstile widget exists but hasn't produced a token yet
// (fast submit, or the challenge is still loading). Per the decided fail policy, give the
// widget a bounded window plus ONE re-challenge, then submit anyway (the server accepts +
// flags unverified — never lose a lead) instead of the old "please submit again" dead end.
// Skip the wait when: owner preview (widget is owner-gated), a token is already in hand, the
// bridge reported a TERMINAL error (no token could ever come), or no widget is present (site
// key unconfigured → nothing to wait for, server fails open). The button stays disabled
// during the (sub-second-typical, 6s-worst-case) wait so the visitor can't double-submit.
if(_hpIsOwner||_hpCapTok!==""||f.__hpCaptchaError||!f.querySelector("iframe[data-hp-turnstile]")){
_hpProceed();
}else{
_hpWaitForCaptcha(f,_hpProceed);
}
}
}

// Snapshot forms in the server-rendered HTML at init; third-party embeds (ConvertKit/ActiveCampaign) inject their <form> later, so they're excluded.
var _hpNativeForms=(function(){try{return new WeakSet([].slice.call(document.querySelectorAll("form")));}catch(e){return null;}})();
document.addEventListener("submit",function(e){
var f=e.target;
if(f.tagName!=="FORM")return;
if(_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]
e.preventDefault();
// Multi-step: if this form is split into steps and the visitor isn't on the last
// one yet, advance (validate current step + Next) instead of submitting. Captures
// Enter-in-field and any early submit so a partial lead is never sent. On the last
// step, validate it in JS first (the form is novalidate, so nothing else does). [P3]
if(f.__hpMultiStep){
if(!f.__hpMultiStep.isLast()){f.__hpMultiStep.advance();return;}
if(f.__hpMultiStep.validate&&!f.__hpMultiStep.validate())return;
}
// "Select at least one" enforcement for required checkbox groups (single-step forms have no
// __hpMultiStep validate pass, and it's a cheap final guard for multi-step too).
if(!_hpGroupReqOK(f))return;
var data=collect(f);
if(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]
submitData(f,data);
},true);

// --- Fake form interception (inputs outside <form>) ---
var lastFakeSubmit=0;
// Walk up from the clicked element to find a "form-like" container:
//   - has 1-20 inputs/textareas/selects (excluding hidden/submit/button types)
//   - has ≤4 button-like descendants (more = page scaffolding, not a form)
//   - the clicked element sits AFTER the last input in document order
//     (real submits sit below fields; uses DOCUMENT_POSITION_PRECEDING = 2)
// Walk depth capped at 3 — further up is <section>, <main>, or <body>.
function findFormLikeContainer(t){
var cur=t.parentElement;
for(var depth=0;cur&&depth<3;depth++){
if(cur.tagName==="FORM")return null;// real form, skip
var inputs=cur.querySelectorAll("input:not([type=hidden]):not([type=submit]):not([type=button]),textarea,select");
if(inputs.length>=1&&inputs.length<=20){
var ctas=cur.querySelectorAll("button,a[href],[role=button],input[type=submit]");
if(ctas.length<=4){
return (t.compareDocumentPosition(inputs[inputs.length-1])&2)?cur:null;
}
}
cur=cur.parentElement;
}
return null;
}
// <a> with a real href is navigation, never a submit. Allow empty href / "#" /
// no href (button-styled anchors). <button type="button"> is explicitly non-submit.
function isPlausibleSubmit(t){
if(t.tagName==="A"){
var href=t.getAttribute("href");
if(href!==null){
href=(href+"").trim();
if(href!==""&&href!=="#")return false;
}
}
if(t.tagName==="BUTTON"&&t.getAttribute("type")==="button")return false;
return true;
}
document.addEventListener("click",function(e){
var t=e.target.closest?e.target.closest("button,a,[role=button],input[type=submit]"):null;
if(!t)return;
if(t.closest("form"))return;// inside a real form, native handler covers it
// Third-party consent-management (CMP) dialogs (Cookiebot/OneTrust/Osano/
// TrustArc/CookieYes) inject a body-level dialog whose "Accept" button + single
// pre-checked "necessary" checkbox otherwise look form-like. Cheap early-exit
// for known vendors; the lead-field guard below is the load-bearing check.
// Vendor tokens are boundary-anchored (CookieYes cky-* matched as whole class
// tokens; TrustArc truste-/truste_/trustarc) so they never collide with
// sticky-*/trusted* names. [HP-3292]
try{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){}
if(!isPlausibleSubmit(t))return;
var now=Date.now();
if(now-lastFakeSubmit<2000)return;
var container=findFormLikeContainer(t);
if(!container)return;
// A genuine lead always carries at least one typed (non checkbox/radio) field
// with a value. A form-like container whose only populated field is a
// checkbox/radio is a consent/cookie dialog, not a form — never fire a lead for
// it. Keys off DOM field TYPE, never payload values, so real leads whose data
// merely contains vendor-name substrings are unaffected. [HP-3292]
var _leadFields=container.querySelectorAll("input:not([type=hidden]):not([type=submit]):not([type=button]):not([type=checkbox]):not([type=radio]),textarea,select");
var _hasLeadField=false;
for(var _lf=0;_lf<_leadFields.length;_lf++){if((_leadFields[_lf].value||"").trim()){_hasLeadField=true;break;}}
if(!_hasLeadField)return;
var data=collect(container);
if(Object.keys(data).length===0)return;
// Skip if all field values are empty (button click near empty form fields)
var hasAnyValue=Object.keys(data).some(function(k){return data[k]&&String(data[k]).trim();});
if(!hasAnyValue)return;
lastFakeSubmit=now;
container.setAttribute("data-htmlpub-fake-form","");
submitData(container,data);
// Notify analytics script about this fake form submission
try{document.dispatchEvent(new CustomEvent("htmlpub:form-submit",{detail:{formIdentifier:formId(container)}}));}catch(e){}
},true);

// Always report form count to parent (even for non-owner views)
function reportFormCount(){
var forms=document.querySelectorAll("form");
var count=forms.length;
var labels=[];
for(var i=0;i<forms.length;i++){
var ff=forms[i];
var label=ff.getAttribute("aria-label")||ff.getAttribute("name")||ff.getAttribute("id")||"";
if(!label){
var heading=ff.querySelector("h1,h2,h3,h4,h5,h6,label,legend");
if(heading) label=heading.textContent.trim().substring(0,50);
}
labels.push(label||("Form "+(i+1)));
}
// needsCaptcha tells the parent shell (sandbox path) to render a Turnstile
// widget on its own origin: the sandboxed iframe has an opaque origin so it
// can't run Turnstile itself. Owner previews never require the token.
var _hpNeedsCaptcha=!_hpIsOwner&&!!document.querySelector("form[data-persist-captcha]");
if(inSandbox) parent.postMessage({type:"htmlpub-form-count",count:count,labels:labels,needsCaptcha:_hpNeedsCaptcha},"*");
}
function initFormCount(){
reportFormCount();
if(typeof MutationObserver!=="undefined"){
new MutationObserver(function(){reportFormCount();}).observe(document.body||document.documentElement,{childList:true,subtree:true});
}
}
if(document.readyState==="loading"){document.addEventListener("DOMContentLoaded",initFormCount);}
else{initFormCount();}

// Leadbox/leadbar iframe form bridge. SYNC with worker-pages/src/scripts/forms.ts.
window.addEventListener("message",function(e){
if(!e||!e.data||e.data.type!=="htmlpub:iframe-form-submit")return;
var fid=String(e.data.formIdentifier||"iframe-form").slice(0,255);
var fsid=e.data.formSourceId?String(e.data.formSourceId).slice(0,255):"";
var raw=e.data.data;
if(!raw||typeof raw!=="object")return;
var clean={};
for(var k in raw){
if(!Object.prototype.hasOwnProperty.call(raw,k))continue;
if(looksLikeRawId(k))continue;
if(_blocklist.test(k))continue;
clean[k]=raw[k];
}
var redirect=typeof e.data.redirect==="string"?e.data.redirect:"";
var relayCaptcha=typeof e.data.captchaToken==="string"?e.data.captchaToken:undefined;
fetch(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);});
},false);
})();
</script><script data-htmlpub-popups>
(function(){
"use strict";

var SCOPE = "pathname";

var DEFAULTS = {
  overlay:   { color: "#000000", opacity: 0.5,  clickToClose: true },
  close:     { enabled: true, position: "top-right", escapeToClose: true },
  animation: { open: "fade", close: "fade", durationMs: 250 },
  frequency: { visitsBefore: 0, daysCooldown: 0 }
};

function parseJsonAttr(raw, fallback) {
  if (!raw) return fallback;
  try {
    var p = JSON.parse(raw);
    if (p && typeof p === "object") return p;
  } catch(e) {}
  return fallback;
}

// Back-compat: pages published before the data-persist-popup* rename carry the
// legacy data-htmlpub-popup* names. Read canonical first, fall back to legacy.
// suffix is "" for the marker, "-name", "-overlay", "-target", etc.
function pAttr(el, suffix) {
  var v = el.getAttribute("data-persist-popup" + suffix);
  return v !== null ? v : el.getAttribute("data-htmlpub-popup" + suffix);
}

function readConfig(el) {
  var overlay   = parseJsonAttr(pAttr(el, "-overlay"),   {});
  var closeCfg  = parseJsonAttr(pAttr(el, "-close"),     {});
  var anim      = parseJsonAttr(pAttr(el, "-animation"), {});
  var triggers  = parseJsonAttr(pAttr(el, "-triggers"),  {});
  var frequency = parseJsonAttr(pAttr(el, "-frequency"), {});
  return {
    overlay: {
      color:        typeof overlay.color === "string"   ? overlay.color   : DEFAULTS.overlay.color,
      opacity:      typeof overlay.opacity === "number" ? overlay.opacity : DEFAULTS.overlay.opacity,
      clickToClose: overlay.clickToClose !== false
    },
    close: {
      enabled:        closeCfg.enabled !== false,
      position:       closeCfg.position === "top-left" ? "top-left" : "top-right",
      escapeToClose:  closeCfg.escapeToClose !== false
    },
    animation: {
      open:        typeof anim.open === "string"  ? anim.open       : DEFAULTS.animation.open,
      close:       typeof anim.close === "string" ? anim.close      : DEFAULTS.animation.close,
      durationMs:  typeof anim.durationMs === "number" ? anim.durationMs : DEFAULTS.animation.durationMs
    },
    triggers: triggers,
    frequency: {
      visitsBefore: typeof frequency.visitsBefore === "number" ? frequency.visitsBefore : 0,
      daysCooldown: typeof frequency.daysCooldown === "number" ? frequency.daysCooldown : 0
    },
    // "none" = the marker's content owns its own chrome, so this runtime paints none of it.
    // Migrated Classic pop-ups: the marker holds an <iframe> whose document already draws the
    // mask, centring, card and close button. Nova-authored pop-ups never set it.
    chrome: pAttr(el, "-chrome") === "none" ? "none" : "default"
  };
}

// ── LocalStorage helpers — scoped per pathname so two pages on the same
// origin track their popups independently. Wrapped in try/catch because
// Safari private mode + iframe sandboxing can throw on access.
var STORE_PREFIX = SCOPE === "popup-id" ? "_hpup:popup:" : ("_hpup:" + (location.pathname || "/") + ":");
function lsGet(key) {
  try { return localStorage.getItem(STORE_PREFIX + key); } catch(e) { return null; }
}
function lsSet(key, value) {
  try { localStorage.setItem(STORE_PREFIX + key, value); } catch(e) {}
}

// Bump the visit counter once per pageload. Used by visitsBefore. In embed
// (popup-id) scope callers pass the popup id as a suffix so each standalone
// popup tracks its own visit count — a busy host page with two embedded popups
// must not let one popup's pageviews satisfy another's visitsBefore gate.
function recordVisit(suffix) {
  var key = "visits" + (suffix ? ":" + suffix : "");
  var prev = parseInt(lsGet(key) || "0", 10);
  if (!isFinite(prev) || prev < 0) prev = 0;
  lsSet(key, String(prev + 1));
  return prev + 1;
}

function isEligible(cfg, popupId, currentVisit) {
  // visitsBefore: 0 = show on first visit. So eligible iff currentVisit > visitsBefore.
  if (cfg.frequency.visitsBefore > 0 && currentVisit <= cfg.frequency.visitsBefore) return false;
  // daysCooldown: don't reshow until N days have passed since last close.
  if (cfg.frequency.daysCooldown > 0) {
    var last = parseInt(lsGet("close:" + popupId) || "0", 10);
    if (last > 0) {
      var elapsedMs = Date.now() - last;
      if (elapsedMs < cfg.frequency.daysCooldown * 86400000) return false;
    }
  }
  return true;
}

function composeRgba(color, opacity) {
  var hex = color.match(/^#([0-9a-f]{6})$/i);
  if (hex) {
    var n = parseInt(hex[1], 16);
    return "rgba(" + ((n >> 16) & 0xff) + "," + ((n >> 8) & 0xff) + "," + (n & 0xff) + "," + opacity + ")";
  }
  var rgb = color.match(/^rgb(?:a)?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
  if (rgb) return "rgba(" + rgb[1] + "," + rgb[2] + "," + rgb[3] + "," + opacity + ")";
  return "rgba(0,0,0," + opacity + ")";
}

function animStart(kind) {
  if (kind === "slide-up")   return { opacity: 0, transform: "translate(-50%, -40%)" };
  if (kind === "slide-down") return { opacity: 0, transform: "translate(-50%, -60%)" };
  if (kind === "zoom")       return { opacity: 0, transform: "translate(-50%, -50%) scale(0.92)" };
  if (kind === "none")       return { opacity: 1, transform: "translate(-50%, -50%)" };
  return { opacity: 0, transform: "translate(-50%, -50%)" }; // fade
}
function animEnd() {
  return { opacity: 1, transform: "translate(-50%, -50%) scale(1)" };
}

// Body scroll lock — refcounted because two popups can technically be in
// the open state at once (different triggers firing back-to-back). The
// first lock saves the body's prior overflow + padding-right and replaces
// them; subsequent locks just bump the count. We only restore once every
// popup that locked has closed, so a still-open popup keeps the page held.
// padding-right compensates for the scrollbar disappearing — without it,
// the dimmed page behind the overlay shifts right at the moment of open.
var _hpupLockCount = 0;
var _hpupSavedOverflow = "";
var _hpupSavedPaddingRight = "";
function lockBodyScroll() {
  if (_hpupLockCount === 0) {
    var scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
    _hpupSavedOverflow = document.body.style.overflow || "";
    _hpupSavedPaddingRight = document.body.style.paddingRight || "";
    if (scrollbarWidth > 0) document.body.style.paddingRight = scrollbarWidth + "px";
    document.body.style.overflow = "hidden";
  }
  _hpupLockCount++;
}
function unlockBodyScroll() {
  if (_hpupLockCount === 0) return;
  _hpupLockCount--;
  if (_hpupLockCount === 0) {
    if (_hpupSavedOverflow) document.body.style.overflow = _hpupSavedOverflow;
    else document.body.style.removeProperty("overflow");
    if (_hpupSavedPaddingRight) document.body.style.paddingRight = _hpupSavedPaddingRight;
    else document.body.style.removeProperty("padding-right");
  }
}

// Build the overlay backdrop. Returns the element. Wired with the right
// pointer behaviour for the popup's clickToClose setting.
function buildOverlay(cfg, onOverlayClick) {
  var ov = document.createElement("div");
  ov.setAttribute("data-htmlpub-popup-backdrop", "");
  ov.style.cssText = "position:fixed;inset:0;background:" + composeRgba(cfg.overlay.color, cfg.overlay.opacity) +
    ";z-index:2147483646;opacity:0;transition:opacity " + cfg.animation.durationMs + "ms ease;";
  if (cfg.overlay.clickToClose) ov.addEventListener("click", onOverlayClick);
  return ov;
}

// Inject a close ✕ button if the popup doesn't already have one. We mark our
// injected button so re-show calls don't duplicate it.
function ensureCloseButton(el, cfg, onClose) {
  if (el.querySelector("[data-htmlpub-popup-close-btn]")) return;
  if (!cfg.close.enabled) return;
  var btn = document.createElement("button");
  btn.setAttribute("type", "button");
  btn.setAttribute("aria-label", "Close pop-up");
  btn.setAttribute("data-htmlpub-popup-close-btn", "");
  var posStyle = cfg.close.position === "top-left" ? "left:12px;" : "right:12px;";
  btn.style.cssText = "position:absolute;top:12px;" + posStyle +
    "width:28px;height:28px;border-radius:9999px;border:0;background:rgba(0,0,0,0.45);color:#fff;" +
    "font:16px/1 system-ui,-apple-system,sans-serif;cursor:pointer;display:flex;align-items:center;" +
    "justify-content:center;z-index:1;padding:0;";
  btn.textContent = "×";
  btn.addEventListener("click", function(e){ e.preventDefault(); e.stopPropagation(); onClose(); });
  // Ensure the popup itself is a positioning context so absolute close button
  // anchors to it. Cheap: only sets if not already positioned.
  var pos = getComputedStyle(el).position;
  if (pos === "static" || !pos) el.style.position = "relative";
  el.appendChild(btn);
}

// ── Per-popup wiring ──────────────────────────────────────────────────────
// Each popup gets its own controller closing over its element + config +
// trigger cleanup so we can tear down listeners on close.

// HP-1557 — fire an analytics event for this popup. Hands off to the
// analytics.ts buffer via window.__hpupPushEvent (registered there). If
// analytics isn't loaded for some reason we silently no-op rather than
// breaking the popup runtime.
function emitPopupEvent(eventType, popup, extras) {
  if (typeof window.__hpupPushEvent !== "function") return;
  var payload = {
    popupId: popup.getAttribute("id") || "",
    popupName: (pAttr(popup, "-name") || "").replace(/^\s+|\s+$/g, "")
  };
  // Standalone-embed pop-ups carry their page id via data-hp-popup-cuid (set by
  // the embed bundle) so analytics can attribute to a specific pop-up; the DOM
  // id in popupId is a shared name-slug. In-page pop-ups have no cuid so this
  // stays absent.
  var _hpCuid = popup.getAttribute("data-hp-popup-cuid");
  if (_hpCuid) payload.popupCuid = _hpCuid;
  if (extras && typeof extras === "object") {
    for (var k in extras) {
      if (Object.prototype.hasOwnProperty.call(extras, k)) payload[k] = extras[k];
    }
  }
  window.__hpupPushEvent(eventType, payload);
}

// Single-open invariant: only one popup may be visible at a time.
// show() force-closes whatever is up before painting itself.
var _hpupCurrentOpen = null;

function wirePopup(el, currentVisit) {
  var cfg = readConfig(el);
  var popupId = el.getAttribute("id") || "";
  if (!popupId) return;
  // The editor stores popups hidden via inline display:none. We never strip
  // that — we toggle it at show/close time so a never-shown popup stays
  // invisible (matching its rendered state in the DOM).
  var originalDisplay = el.style.display || "";
  var isOpen = false;
  var overlay = null;
  var keyListener = null;
  var sizeObserver = null;
  var cleanups = []; // trigger listener removers; called on first show

  function cleanupTriggers() {
    for (var i = 0; i < cleanups.length; i++) {
      try { cleanups[i](); } catch(e) {}
    }
    cleanups = [];
  }

  // Why a popup was opened — informs the parent analytics event so the
  // dashboard can distinguish "fired by timer" vs. "explicit user click".
  // Updated by callers before show() and reset to null after emit.
  var lastShowReason = null;

  // HP-4147 — the overflow choice in show() is a measurement, and measurements
  // go stale. Content can grow AFTER open: an inline checkout's Stripe Payment
  // Element progressively reveals card fields and then a "Save your info"
  // section as the buyer types. A popup that opened short (overflow:visible, so
  // its shadow paints) and then grows past maxHeight spills below the viewport
  // instead of scrolling, and because show() locked the body scroll there is
  // nothing left for a touch drag to move — the Pay button becomes unreachable
  // and closing + reopening is the only way out (reopening re-runs show(), which
  // re-measures against the now-tall content and lands on "auto"). So re-decide
  // on every resize — but only ever upgrade to "auto", never back to "visible".
  //
  // One-way on purpose. Reverting to "visible" when content shrinks back under
  // maxHeight sounds harmless (it hands HP-3771 its shadow back) but it
  // re-releases horizontal spill that "auto" had been containing, so a wide
  // child paints outside the popup over the page, and it discards scrollTop,
  // bouncing the buyer to the top of the form. Both were measured in Chromium
  // at a Pixel viewport. Staying in "auto" costs only the shadow, and only for
  // the rest of an open in which content already exceeded the viewport: "auto"
  // paints no scrollbar once content fits again, it merely clips. close()
  // strips every inline style, so the next open re-decides from scratch.
  function syncOverflow() {
    if (!isOpen) return;
    // Guarded on a real change. Toggling overflow lays out a scrollbar, which
    // resizes content, which re-enters this observer — an unguarded write there
    // is a feedback loop that never settles.
    if (el.scrollHeight > el.clientHeight + 1 && el.style.overflow !== "auto") {
      el.style.overflow = "auto";
    }
  }

  function observeSize() {
    if (sizeObserver || typeof ResizeObserver === "undefined") return;
    sizeObserver = new ResizeObserver(syncOverflow);
    // Observing el catches the growth that crosses maxHeight, since its own box
    // tracks content right up to the clamp. Observing its element children
    // catches growth that continues past the clamp, where el's box is pinned and
    // reports no resize at all — content 1px under maxHeight that then expands
    // would otherwise never re-decide.
    sizeObserver.observe(el);
    for (var childIndex = 0; childIndex < el.children.length; childIndex++) {
      sizeObserver.observe(el.children[childIndex]);
    }
  }

  function unobserveSize() {
    if (!sizeObserver) return;
    sizeObserver.disconnect();
    sizeObserver = null;
  }

  function show(reason) {
    if (isOpen) return;
    if (_hpupCurrentOpen && _hpupCurrentOpen !== el && typeof _hpupCurrentOpen._hpupClose === "function") {
      _hpupCurrentOpen._hpupClose("replaced", true);
    }
    _hpupCurrentOpen = el;
    isOpen = true;
    lastShowReason = reason || "explicit";
    cleanupTriggers();
    lockBodyScroll();
    // chrome:none — the content paints its own ✕ (Classic's .js-close, inside the iframe).
    var bare = cfg.chrome === "none";
    if (!bare) ensureCloseButton(el, cfg, function(){ close("close_button"); });

    var start = animStart(cfg.animation.open);
    var end   = animEnd();

    // Centered fixed positioning with a high z-index so we float above any
    // page content + checkout / chat widgets.
    el.style.display      = "block";
    el.style.position     = "fixed";
    el.style.zIndex       = "2147483647";

    if (bare) {
      // Full-viewport shell. An <iframe> has no intrinsic content height (~150px, and it never
      // reports its content size to the parent), so the shrink-wrapped-card model below cannot
      // measure it — centring, the maxHeight/maxWidth clamps and the overflow logic all assume a
      // box that sizes to its content. Reproduce CDA's model instead: the frame fills the
      // viewport and the Classic document inside owns mask, centring and card.
      //
      // No transform either: every animStart transform carries translate(-50%,-50%), which
      // centres a card and would shove a full-viewport shell off screen. Opacity alone.
      el.style.top        = "0";
      el.style.left       = "0";
      el.style.width      = "100%";
      el.style.height     = "100%";
      el.style.opacity    = String(start.opacity);
      el.style.transition = "opacity " + cfg.animation.durationMs + "ms ease";
      openClassicFrame(el);
    } else {
    el.style.top          = "50%";
    el.style.left         = "50%";
    el.style.transform    = start.transform;
    el.style.opacity      = String(start.opacity);
    el.style.maxHeight    = "calc(100vh - 32px)";
    el.style.maxWidth     = "calc(100vw - 32px)";
    // HP-3771 — "auto" clips any box-shadow set on the popup's own content:
    // box-shadow doesn't affect layout size, so this element shrink-wraps to its
    // content and its own overflow then clips the shadow it should cast.
    //
    // But "visible" unconditionally is not safe either. This element is
    // position:fixed and centred, so content taller than maxHeight does not
    // clip — it spills past the viewport with no scrollbar and nothing can
    // reach it. A lead-capture form taller than the viewport would lose its
    // submit button. So decide per popup, after layout: overflow stays "auto"
    // only when the content genuinely overflows (scrolling wins), and is
    // "visible" otherwise (shadow wins). Short popups are the overwhelmingly
    // common case, so the shadow renders for almost everyone.
    //
    // syncOverflow re-applies this same rule for the rest of the open (HP-4147).
    el.style.overflow     = "visible";
    syncOverflow();
    el.style.transition   = "opacity " + cfg.animation.durationMs + "ms ease, transform " + cfg.animation.durationMs + "ms ease";
    }

    if (!el.parentNode) document.body.appendChild(el);
    if (!bare) {
      // Backdrop. Inserted before the popup so the popup naturally paints on top.
      // chrome:none skips it — the Classic document paints its own .js-leadbox-mask, and a second
      // backdrop would darken the page twice.
      overlay = buildOverlay(cfg, function(){ close("overlay_click"); });
      (el.parentNode || document.body).insertBefore(overlay, el);

      // Started only once el is attached, so the observer's first delivery measures
      // real geometry rather than the 0x0 of a detached node.
      observeSize();
    }

    // Force reflow so the next style change animates from start, not jumps.
    /* eslint-disable-next-line no-unused-expressions */ void el.offsetWidth;

    el.style.opacity   = String(end.opacity);
    if (!bare) el.style.transform = end.transform;
    if (overlay) overlay.style.opacity = "1";

    if (cfg.close.escapeToClose) {
      keyListener = function(e) { if (e.key === "Escape") close("escape"); };
      document.addEventListener("keydown", keyListener);
    }

    emitPopupEvent("popup_view", el, { reason: lastShowReason });
    lastShowReason = null;
  }

  function close(reason, hard) {
    if (!isOpen) return;
    isOpen = false;
    emitPopupEvent("popup_dismiss", el, { reason: reason || "unknown" });
    var start = animStart(cfg.animation.close);
    el.style.opacity   = String(start.opacity);
    // Same reason as show(): the close transform centres a card, and applying it to a
    // full-viewport shell would slide it off screen instead of fading it out.
    if (cfg.chrome !== "none") el.style.transform = start.transform;
    if (overlay) overlay.style.opacity = "0";

    if (keyListener) {
      document.removeEventListener("keydown", keyListener);
      keyListener = null;
    }

    var afterAnim = function() {
      if (_hpupCurrentOpen === el) _hpupCurrentOpen = null;
      unobserveSize();
      el.style.display = originalDisplay || "none";
      el.style.removeProperty("position");
      el.style.removeProperty("top");
      el.style.removeProperty("left");
      el.style.removeProperty("transform");
      el.style.removeProperty("opacity");
      el.style.removeProperty("z-index");
      el.style.removeProperty("max-height");
      el.style.removeProperty("max-width");
      // Only the chrome:none branch sets these, and teardown must remove exactly what this show
      // set — a marker can legitimately carry an author's own inline width/height, and stripping
      // it on close would resize the pop-up the second time it opens.
      if (cfg.chrome === "none") {
        el.style.removeProperty("width");
        el.style.removeProperty("height");
      }
      el.style.removeProperty("overflow");
      el.style.removeProperty("transition");
      if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay);
      overlay = null;
      // Tear down the close button — its click handler closes over THIS
      // show's overlay reference. Leaving it across opens means a second
      // show's X would call the previous close() with a stale overlay and
      // leave the current overlay floating.
      var existingBtn = el.querySelector("[data-htmlpub-popup-close-btn]");
      if (existingBtn && existingBtn.parentNode) existingBtn.parentNode.removeChild(existingBtn);
      // Release the scroll lock only after the popup is fully torn down so
      // the page doesn't pop back to scrollable while the close animation
      // is still painting.
      unlockBodyScroll();
      lsSet("close:" + popupId, String(Date.now()));
    };
    if (hard || cfg.animation.close === "none") afterAnim();
    else setTimeout(afterAnim, cfg.animation.durationMs);
  }

  // ── Triggers ────────────────────────────────────────────────────────────
  // Only wire if the popup is eligible to show this pageload (frequency
  // rules). Click-to-open via [data-htmlpub-popup-target] always works,
  // regardless of eligibility — it's an explicit user action.
  var eligible = isEligible(cfg, popupId, currentVisit);

  if (eligible) {
    var t = cfg.triggers || {};

    if (t.timed && typeof t.timed.delaySec === "number") {
      var delayMs = Math.max(0, t.timed.delaySec) * 1000;
      var timer = setTimeout(function(){ show("timed"); }, delayMs);
      cleanups.push(function(){ clearTimeout(timer); });
    }

    if (t.exitIntent === true) {
      // Mobile / touch devices don't have a meaningful "exit toward chrome"
      // gesture. Standard heuristic: skip if no mouse pointer is available.
      if (!matchMedia("(pointer: coarse)").matches) {
        var onMove = function(e) {
          if (e.clientY < 8) show("exit_intent");
        };
        document.addEventListener("mousemove", onMove);
        cleanups.push(function(){ document.removeEventListener("mousemove", onMove); });
      }
    }

    if (typeof t.scrollPercent === "number" && t.scrollPercent > 0) {
      var threshold = Math.max(1, Math.min(100, t.scrollPercent));
      var onScroll = function() {
        var doc = document.documentElement;
        var scrolled = (window.scrollY || doc.scrollTop || 0);
        var max = (doc.scrollHeight - doc.clientHeight) || 1;
        var pct = (scrolled / max) * 100;
        if (pct >= threshold) show("scroll");
      };
      window.addEventListener("scroll", onScroll, { passive: true });
      cleanups.push(function(){ window.removeEventListener("scroll", onScroll); });
    }
  }

  // Note: in-popup form submits and CTA clicks are NOT emitted as separate
  // popup_* events. They already fire as form_submit / element_click via
  // analytics.ts's global listeners, which now attach popupId + popupName
  // to the payload so customers can still see which pop-up the conversion
  // came from while continuing to track the standard goal types.

  // Expose the show function on the element so [data-htmlpub-popup-target]
  // clicks (wired globally below) can find and trigger it without re-parsing
  // the config. _hpupShow / _hpupClose are deliberately namespaced + ugly to
  // avoid colliding with user code.
  el._hpupShow = show;
  el._hpupClose = close;
  // Returned so boot() can flush unfired trigger listeners on pagehide.
  return cleanupTriggers;
}

// A served Classic leadbox renders INACTIVE: its controller only adds js-active to the dialog
// and mask when it receives an "open" message, so a frame nobody tells to open is a full-viewport
// sheet showing nothing. Its handleMessage accepts the bare string "open" or {type:"open"}.
//
// Posted on both paths because the ordering is not ours to control: if the frame is already
// loaded the immediate post lands, and if it is not, the load listener catches it. Opening twice
// is harmless — open() just re-adds the class and refocuses.
function openClassicFrame(el) {
  var frame = el.querySelector("iframe");
  if (!frame) return;
  var post = function() {
    // targetOrigin "*": the frame is served sandboxed, so its origin is opaque and cannot be
    // named. The payload is a bare open signal carrying nothing worth protecting.
    try { frame.contentWindow && frame.contentWindow.postMessage({ type: "open" }, "*"); } catch(e) {}
  };
  post();
  frame.addEventListener("load", post);
}

// Global close relay. A chrome:none pop-up's ✕ lives INSIDE its iframe (Classic's .js-close), so
// the click is invisible to this document — the frame posts htmlpub:leadbox-close instead and we
// close the marker holding it. Without this the Classic card hides itself and the full-viewport
// iframe stays put, covering the page with an invisible sheet that swallows every click.
//
// Identify the marker by matching iframe.contentWindow against e.source, NOT by the message's
// uuid: a page can carry several migrated pop-ups, and a uuid can be placed more than once. This
// is the technique CDA's own injected handler used.
function handleCloseRelay(e) {
  if (!e || !e.data) return;
  // Classic's own leadbox controller posts the bare STRING "close leadbox" to window.parent when
  // it closes (300ms after the fade, from its close() method) — read out of the served bundle, not
  // guessed. It is already the wire format, so nothing needs injecting into the frame. The
  // namespaced form is accepted too, for anything we serve ourselves later.
  var type = typeof e.data === "string" ? e.data : e.data.type;
  if (type !== "close leadbox" && type !== "htmlpub:leadbox-close") return;
  var frames = document.querySelectorAll("[data-persist-popup] iframe, [data-htmlpub-popup] iframe");
  for (var i = 0; i < frames.length; i++) {
    if (frames[i].contentWindow !== e.source) continue;
    var marker = frames[i].closest("[data-persist-popup], [data-htmlpub-popup]");
    if (marker && typeof marker._hpupClose === "function") marker._hpupClose("close_relay");
    return;
  }
}

// Global click-to-open dispatcher. Targets carry [data-persist-popup-target]
// (or legacy [data-htmlpub-popup-target]) or href="#popup-…" (for plain anchors
// that the link picker auto-generated).
function handleTargetClick(e) {
  var t = e.target;
  if (!t || !t.closest) return;
  var hit = t.closest("[data-persist-popup-target], [data-htmlpub-popup-target], a[href^='#popup-'], a[href^='#modal-'], a[href^='#dialog-']");
  if (!hit) return;
  var id = pAttr(hit, "-target");
  if (!id) {
    var href = hit.getAttribute("href") || "";
    if (href.charAt(0) === "#") id = href.slice(1);
  }
  if (!id) return;
  var popup = document.getElementById(id);
  if (!popup || pAttr(popup, "") === null) return;
  e.preventDefault();
  if (typeof popup._hpupShow === "function") popup._hpupShow("click_target");
}

// ── Boot ──────────────────────────────────────────────────────────────────

function flushTeardowns(list) {
  for (var j = 0; j < list.length; j++) {
    try { list[j](); } catch(e) {}
  }
}

function boot() {
  if (SCOPE === "popup-id") {
    // Embed mode: each standalone popup ships its OWN copy of this runtime, so
    // a single global boot guard would make a second embedded popup bail. Wire
    // each popup once via a per-element marker, count visits per popup id, and
    // attach the global click / pagehide listeners only once across copies.
    var list = document.querySelectorAll("[data-persist-popup],[data-htmlpub-popup]");
    if (!list.length) return;
    if (!window.__hpupTeardowns) window.__hpupTeardowns = [];
    for (var n = 0; n < list.length; n++) {
      var pel = list[n];
      if (pel.__hpupWired) continue;
      pel.__hpupWired = true;
      var pt = wirePopup(pel, recordVisit(pel.getAttribute("id") || ""));
      if (typeof pt === "function") window.__hpupTeardowns.push(pt);
    }
    if (!window.__hpupClickWired) {
      window.__hpupClickWired = true;
      document.addEventListener("click", handleTargetClick, false);
      window.addEventListener("message", handleCloseRelay, false);
      window.addEventListener("pagehide", function(){ flushTeardowns(window.__hpupTeardowns || []); });
    }
    return;
  }

  // In-page mode: one runtime per page wires every popup. Guard against
  // double-injection (HMR, accidental double-include).
  if (window.__hpupBooted) return;
  window.__hpupBooted = true;
  var popups = document.querySelectorAll("[data-persist-popup],[data-htmlpub-popup]");
  if (!popups.length) return;
  var visit = recordVisit();
  var teardowns = [];
  for (var i = 0; i < popups.length; i++) {
    var t = wirePopup(popups[i], visit);
    if (typeof t === "function") teardowns.push(t);
  }
  document.addEventListener("click", handleTargetClick, false);
  window.addEventListener("message", handleCloseRelay, false);
  // Flush unfired trigger listeners on pagehide.
  window.addEventListener("pagehide", function(){ flushTeardowns(teardowns); });
}

if (document.readyState === "loading") {
  document.addEventListener("DOMContentLoaded", boot);
} else {
  boot();
}

})();
</script><script data-htmlpub-countdown-runtime>
(function(){
var nodes=document.querySelectorAll('[data-htmlpub-countdown]');
if(!nodes.length)return;
function pad(n){n=String(n);return n.length<2?'0'+n:n;}
function setDigit(root,sel,val){var el=root.querySelector(sel);if(el)el.textContent=pad(val);}
function tick(node,endMs){
var diff=Math.max(0,endMs-Date.now());
var s=Math.floor(diff/1000);
var d=Math.floor(s/86400);s-=d*86400;
var h=Math.floor(s/3600);s-=h*3600;
var m=Math.floor(s/60);s-=m*60;
setDigit(node,'.countdown-days',d);
setDigit(node,'.countdown-hours',h);
setDigit(node,'.countdown-minutes',m);
setDigit(node,'.countdown-seconds',s);
if(diff===0){
var redirect=node.getAttribute('data-redirect');
var inFrame=false;try{inFrame=window!==window.top;}catch(e){inFrame=true;}
if(redirect&&!inFrame){try{window.location.href=redirect;}catch(e){}}
return true;
}
return false;
}
function endMsFor(node){
var mode=node.getAttribute('data-htmlpub-countdown');
if(mode==='fixed'){
var target=node.getAttribute('data-target');
var t=target?Date.parse(target):NaN;
return isNaN(t)?0:t;
}
if(mode==='evergreen'){
var dur=parseInt(node.getAttribute('data-duration')||'0',10);
if(!dur)return 0;
var idx=Array.prototype.indexOf.call(nodes,node);
var key='htmlpub_cd_'+(node.getAttribute('data-persist-countdown-id')||node.id||(location.pathname+'|'+dur+'|'+idx));
var first=null;
try{first=localStorage.getItem(key);}catch(e){}
if(!first){first=String(Date.now());try{localStorage.setItem(key,first);}catch(e){}}
return parseInt(first,10)+dur*1000;
}
return 0;
}
for(var i=0;i<nodes.length;i++){(function(node){
var endMs=endMsFor(node);
if(!endMs)return;
if(tick(node,endMs))return;
var iv=setInterval(function(){if(tick(node,endMs))clearInterval(iv);},1000);
})(nodes[i]);}
})();
</script><script data-htmlpub-nav>
(function(){
"use strict";

function closeAllSubmenus(except) {
  var panels = document.querySelectorAll('[data-persist-nav-submenu-panel]');
  for (var i = 0; i < panels.length; i++) {
    var panel = panels[i];
    if (panel === except) continue;
    panel.classList.remove('is-open');
    var trigger = panel.__hpNavTrigger;
    if (trigger) trigger.setAttribute('aria-expanded', 'false');
  }
}

function wireSubmenus() {
  var triggers = document.querySelectorAll('[data-persist-nav-submenu]');
  if (!triggers.length) return;
  for (var i = 0; i < triggers.length; i++) {
    (function(trigger) {
      if (trigger.__hpNavWired) return;
      trigger.__hpNavWired = true;
      var panelId = trigger.getAttribute('data-persist-nav-submenu');
      var panel = panelId ? document.getElementById(panelId) : (trigger.parentElement ? trigger.parentElement.querySelector('[data-persist-nav-submenu-panel]') : null);
      if (!panel) return;
      panel.__hpNavTrigger = trigger;
      trigger.setAttribute('aria-haspopup', 'true');
      trigger.setAttribute('aria-expanded', 'false');
      trigger.addEventListener('click', function(e) {
        var isOpen = panel.classList.contains('is-open');
        e.preventDefault();
        e.stopPropagation();
        closeAllSubmenus(isOpen ? null : panel);
        panel.classList.toggle('is-open', !isOpen);
        trigger.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
      });
    })(triggers[i]);
  }
  document.addEventListener('click', function(e) {
    var t = e.target;
    if (t && t.closest && t.closest('[data-persist-nav-submenu-panel], [data-persist-nav-submenu]')) return;
    closeAllSubmenus(null);
  });
}

function wireMobileMenu() {
  var toggles = document.querySelectorAll('[data-persist-nav-mobile-toggle], [data-persist-nav-tablet-toggle]');
  if (!toggles.length) return;
  for (var i = 0; i < toggles.length; i++) {
    (function(toggle) {
      if (toggle.__hpNavWired) return;
      toggle.__hpNavWired = true;
      var isTablet = toggle.hasAttribute('data-persist-nav-tablet-toggle');
      var targetId = isTablet ? toggle.getAttribute('data-persist-nav-tablet-toggle') : toggle.getAttribute('data-persist-nav-mobile-toggle');
      var panelSelector = isTablet ? '[data-persist-nav-tablet-panel]' : '[data-persist-nav-mobile-panel]';
      var panel = targetId ? document.getElementById(targetId) : (toggle.parentElement ? toggle.parentElement.querySelector(panelSelector) : null);
      if (!panel) return;
      toggle.setAttribute('aria-expanded', 'false');
      toggle.addEventListener('click', function(e) {
        var isOpen = panel.classList.contains('is-open');
        e.preventDefault();
        e.stopPropagation();
        if (isOpen) panel.classList.remove('is-open'); else panel.classList.add('is-open');
        toggle.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
      });
    })(toggles[i]);
  }
}

function wireScrollBackground() {
  // Every nav this system manages, not just ones with scroll-bg/blur
  // currently set — a page published while an OLDER build still applied
  // blur directly to nav carries that inline backdrop-filter baked into
  // its served HTML even after blur is later turned off, so the self-heal
  // below has to run regardless of the CURRENT attribute state.
  var navs = document.querySelectorAll('[data-persist-nav-id]');
  if (!navs.length) return;
  for (var i = 0; i < navs.length; i++) {
    (function(nav) {
      // Self-heal: strip a stale inline backdrop-filter baked into a page
      // saved before this fix (HP-3558 round 15 follow-up) — see the
      // dedicated blur-layer comment below for why it must never live on
      // nav itself.
      if (nav.style.getPropertyValue('backdrop-filter') || nav.style.getPropertyValue('-webkit-backdrop-filter')) {
        nav.style.removeProperty('backdrop-filter');
        nav.style.removeProperty('-webkit-backdrop-filter');
      }
      // "Transparent" (data-persist-nav-always-transparent, HP-3558 round
      // 16) — the non-sticky counterpart of scroll-bg above. "Useful over a
      // hero image" only holds if nav visually overlaps whatever follows
      // it, so lift it out of flow the same way Sticky's own
      // position:fixed/sticky already does, rather than leaving it
      // position:static (which just reveals blank page background above
      // the hero, not the hero itself — see iframe-script/utils.ts's
      // ensureNavScrollBackground for the matching editor-side comment).
      var alwaysTransparent = nav.hasAttribute('data-persist-nav-always-transparent');
      if (alwaysTransparent) {
        var navCsForOverlay = window.getComputedStyle(nav);
        if (!navCsForOverlay || (navCsForOverlay.position !== 'fixed' && navCsForOverlay.position !== 'sticky')) {
          nav.style.position = 'absolute';
          nav.style.top = '0px';
          nav.style.left = '0px';
          nav.style.right = '0px';
          if (!nav.style.zIndex) nav.style.zIndex = '1000';
        }
      }
      // Blur needs no scroll-reactivity — a blurred OPAQUE background
      // looks identical to an unblurred one, so it's simplest and correct
      // to apply it once, unconditionally, regardless of scroll state.
      // Applied to a dedicated decorative layer BEHIND nav's real
      // content, never to nav itself — backdrop-filter promotes whatever
      // element carries it into a new containing block for any
      // position:fixed descendant, which silently shrank the full-screen/
      // bottom-drawer hamburger panel (both position:fixed) down to nav's
      // own small box instead of the viewport when it lived on nav
      // directly (HP-3558 round 15 follow-up: "the full-screen hamburger
      // menu... ended up being covered by the next section").
      var blur = nav.getAttribute('data-persist-nav-scroll-blur');
      if (blur) {
        var blurLayer = nav.querySelector('[data-persist-nav-scroll-blur-layer]');
        if (!blurLayer) {
          blurLayer = document.createElement('div');
          blurLayer.setAttribute('data-persist-nav-scroll-blur-layer', '');
          // z-index:-1 required — without it, backdrop-filter's own
          // compositing samples nav's LATER siblings (logo/links/CTA) too,
          // visibly blurring them despite painting after this layer in
          // plain DOM order. Safe from escaping behind nav's own box into
          // the page below because Sticky header always sets an explicit,
          // non-auto nav.style.zIndex — position:fixed/sticky + a non-auto
          // z-index already makes nav its own stacking context.
          blurLayer.style.cssText = 'position:absolute;inset:0;pointer-events:none;z-index:-1;';
          nav.insertBefore(blurLayer, nav.firstChild);
          var navCs = window.getComputedStyle(nav);
          if (!navCs || navCs.position === 'static' || !navCs.position) nav.style.position = 'relative';
        }
        // Self-heal a layer saved before the z-index fix landed (see
        // iframe-script/utils.ts's ensureNavScrollBackground for the
        // matching editor-side comment).
        if (blurLayer.style.zIndex !== '-1') blurLayer.style.zIndex = '-1';
        blurLayer.style.setProperty('backdrop-filter', 'blur(' + blur + ')', 'important');
        blurLayer.style.setProperty('-webkit-backdrop-filter', 'blur(' + blur + ')', 'important');
      }
      if (alwaysTransparent) {
        nav.style.setProperty('background-color', 'transparent', 'important');
        return;
      }
      var color = nav.getAttribute('data-persist-nav-scroll-bg');
      if (!color || nav.__hpNavScrollWired) return;
      nav.__hpNavScrollWired = true;
      function sync() {
        nav.style.setProperty('background-color', window.scrollY > 24 ? color : 'transparent', 'important');
      }
      window.addEventListener('scroll', sync, { passive: true });
      sync();
    })(navs[i]);
  }
}

function boot() {
  wireSubmenus();
  wireMobileMenu();
  wireScrollBackground();
}

if (document.readyState === "loading") {
  document.addEventListener("DOMContentLoaded", boot);
} else {
  boot();
}

})();
</script><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:'a339aeb18d6f3cb3',t:'MTc4ODE1NDYzNw=='};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></body></html>