1<!DOCTYPE html><html lang="en" data-critters-container><head> 2 <meta charset="UTF-8"> 3 <link rel="icon" type="image/svg+xml" href="/images/logo.svg"> 4 <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> 5 <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> 6 <link rel="icon" href="/favicon.ico" sizes="any"> 7 <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> 8 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 9 <meta name="color-scheme" content="only light"> 10 <meta name="theme-color" content="#ffffff"> 11 <meta name="darkreader-lock"> 12 <link rel="preload" as="image" href="/images/phone-base-frame-no-lookup-mobile.webp" fetchpriority="high" media="(max-width: 639px)"> 13 <link rel="preload" as="image" href="/images/phone-base-frame-no-lookup.webp" fetchpriority="high" media="(min-width: 640px)"> 14 <link rel="preconnect" href="https://fonts.googleapis.com"> 15 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 16 <link rel="dns-prefetch" href="https://va.vercel-scripts.com"> 17 <!-- Critical above-fold fonts: preloaded. Figtree/Protest are used everywhere; 18 Plus Jakarta Sans and Nunito are used above the fold in HeroSection/MatchCard. --> 19 <link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Figtree:wght@400;600&family=Nunito:wght@500;700&
19family=Plus+Jakarta+Sans:wght@300;500;600;800&family=Protest+Strike&display=swap" onload="this.onload=null;this.rel='stylesheet'"> 20 <noscript> 21 <link href="https://fonts.googleapis.com/css2?family=Figtree:wght@400;600&family=Nunito:wght@500;700&family=Plus+Jakarta+Sans:wght@300;500;600;800&family=Protest+Strike&display=swap" rel="stylesheet"> 22 </noscript> 23 <!-- Below-fold fonts: deferred (no preload, just async stylesheet). Inter/Manrope 24 only appear in lazy-loaded sections and non-home pages. --> 25 <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&family=Manrope:wght@400;500;600;700;800&display=swap"> 26 <noscript> 27 <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&family=Manrope:wght@400;500;600;700;800&display=swap" rel="stylesheet"> 28 </noscript> 29 <style>nav.site-navbar{position:sticky;top:0;z-index:100;width:100%}.download-nav-cta{background:linear-gradient(143.74deg, hsl(82 85% 44%) 21.16%, hsl(75 92% 29%) 68.87%);box-shadow:0 4px 11px hsl(0 0% 77% / 0.57), inset 0 4px 5.7px hsl(77 87% 31%), inset 2px 9px 7.5px hsl(0 0% 93% / 0.25)}body{transition:opacity 0.25s ease}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-content:""}}}@layer theme{:root{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-300:oklch(87.2% .01 258.338);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-3xl:48rem;--text-3xl:1.875rem;--text-3xl--line-height:1.2 ;--font-weight-light:300;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-normal:0em;--tracking-wide:.025em;--tracking-widest:.1em;--leading-tight:1.25;--leading-snug:1.375;--drop-shadow-2xl:0 25px 25px #00000026;--ease-out:cubic-bezier(0,0,.2,1);--ease-in-out:cubic-bezier(.4,0,.2,1);--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite;--blur-md:12px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-brand-green-light:#8db511;--color-badge-green:#91b640;--color-card-green:#eef5e3;--color-text-dark:#161616;--color-text-body:#303030;--color-text-muted:#949494;--color-text-subtitle:#a6a6a6;--font-bradley:"Bradley Hand","Bradley Hand ITC",cursive;--font-jakarta:"Plus Jakarta Sans",sans-serif;--font-protest:"Protest Strike",system-ui;
29--font-figtree:"Figtree",sans-serif;--font-inter:"Inter",sans-serif;--font-nunito:"Nunito",sans-serif;--font-manrope:"Manrope",sans-serif}}@layer base{*,:after,:before{box-sizing:border-box;border:0 solid;margin:0;padding:0}html{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}h1{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}img,svg{vertical-align:middle;display:block}img{max-width:100%;height:auto}button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}button{appearance:button}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing)*0)}.top-0{top:calc(var(--spacing)*0)}.z-10{z-index:10}.z-\[5\]{z-index:5}.z-\[100\]{z-index:100}.mx-auto{margin-inline:auto}.mt-\[16px\]{margin-top:16px}.mt-\[28px\]{margin-top:28px}.-mb-\[17px\]{margin-bottom:-17px}.ml-3{margin-left:calc(var(--spacing)*3)}.block{display:block}.flex{display:flex}.hidden{display:none}.inline-flex{display:inline-flex}.h-0\.5{height:calc(var(--spacing)*.5)}.h-\[18px\]{height:18px}.h-\[20px\]{height:20px}.h-\[28px\]{height:28px}.h-\[32px\]{height:32px}.h-\[40px\]{height:40px}.h-\[52px\]{height:52px}.min-h-\[60vh\]{min-height:60vh}.min-h-screen{min-height:100vh}.w-6{width:calc(var(--spacing)*6)}.w-\[20px\]{width:20px}.w-\[32px\]{width:32px}.w-\[40px\]{width:40px}.w-\[150px\]{width:150px}.w-\[158px\]{width:158px}.w-auto{width:auto}.w-full{width:100%}.max-w-0{max-width:calc(var(--spacing)*0)}.max-w-\[480px\]{max-width:480px}.max-w-\[1440px\]{max-width:1440px}.flex-shrink-0{flex-shrink:0}.cursor-pointer{cursor:pointer}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-3{gap:calc(var(--spacing)*3)}.gap-\[46px\]{gap:46px}.overflow-hidden{overflow:hidden}.overflow-x-clip{overflow-x:clip}.rounded-\[26px\]{border-radius:26px}.rounded-full{border-radius:3.40282e38px}.bg-text-body{background-color:var(--color-text-body)}.bg-transparent{background-color:#0000}.bg-white{background-color:var(--color-white)}.p-2{padding:calc(var(--spacing)*2)}.px-\[10px\]{padding-inline:10px}.px-\[20px\]{padding-inline:20px}.px-\[24px\]{padding-inline:24px}.py-\[16px\]{padding-block:16px}.py-\[100px\]{padding-block:100px}.pt-\[16px\]{padding-top:16px}.pb-\[17px\]{padding-bottom:17px}.text-center{text-align:center}.font-figtree{font-family:var(--font-figtree)}.font-protest{font-family:var(--font-protest)}.text-\[16px\]{font-size:16px}.text-\[18px\]{font-size:18px}.text-\[42px\]{font-size:42px}.leading-\[1\.05\]{--tw-leading:1.05;line-height:1.05}.leading-\[1\.6\]{--tw-leading:1.6;line-height:1.6}.leading-\[18px\]{--tw-leading:18px;line-height:18px}.leading-\[24px\]{--tw-leading:24px;line-height:24px}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.whitespace-nowrap{white-space:nowrap}.text-text-dark{color:var(--color-text-dark)}.text-text-muted{color:var(--color-text-muted)}.text-text-subtitle{color:var(--color-text-subtitle)}.text-white{color:var(--color-white)}.opacity-0{opacity:0}.opacity-100{opacity:1}.shadow-\[0_4px_11px_rgba\(0\,0\,0\,0\.12\)\]{--tw-shadow:0 4px 11px var(--tw-sha
29dow-color,#0000001f);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.brightness-0{--tw-brightness:brightness(0%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}@media(hover:hover){.group-hover\/apple\:opacity-100:is(:where(.group\/apple):hover *){opacity:1}.group-hover\/apple\:invert:is(:where(.group\/apple):hover *){--tw-invert:invert(100%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.group-hover\/cta\:ml-0:is(:where(.group\/cta):hover *){margin-left:calc(var(--spacing)*0)}.group-hover\/cta\:ml-3:is(:where(.group\/cta):hover *){margin-left:calc(var(--spacing)*3)}.group-hover\/cta\:w-0:is(:where(.group\/cta):hover *){width:calc(var(--spacing)*0)}.group-hover\/cta\:max-w-\[100px\]:is(:where(.group\/cta):hover *){max-width:100px}.group-hover\/cta\:opacity-0:is(:where(.group\/cta):hover *){opacity:0}.group-hover\/cta\:opacity-100:is(:where(.group\/cta):hover *){opacity:1}.group-hover\/play\:opacity-0:is(:where(.group\/play):hover *){opacity:0}.group-hover\/play\:opacity-100:is(:where(.group\/play):hover *){opacity:1}}@media(hover:hover){.hover\:text-text-body:hover{color:var(--color-text-body)}.hover\:brightness-110:hover{--tw-brightness:brightness(110%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}}@media(min-width:40rem){.sm\:h-\[33px\]{height:33px}.sm\:w-\[190px\]{width:190px}.sm\:px-6{padding-inline:calc(var(--spacing)*6)}.sm\:pt-\[17px\]{padding-top:17px}.sm\:text-\[56px\]{font-size:56px}}@media(min-width:64rem){.lg\:px-10{padding-inline:calc(var(--spacing)*10)}}@media(min-width:80rem){.xl\:block{display:block}.xl\:flex{display:flex}.xl\:hidden{display:none}.xl\:px-\[150px\]{padding-inline:150px}.xl\:pt-\[17px\]{padding-top:17px}}}:root{color-scheme:light only}html{-ms-overflow-style:none;scrollbar-width:none;filter:none!important}body{font-family:var(--font-figtree);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;color:var(--color-text-body)!important;background-color:#fff!important}.download-nav-cta{background:linear-gradient(143.74deg,#8ad011 21.16%,#6c8e06 68.87%);box-shadow:0 4px 11px #c4c4c491,inset 0 4px 5.7px #6d940a,inset 2px 9px 7.5px #ededed40}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"<length-percentage>";inherits:false;initial-value:0%}
29@property --tw-gradient-via-position{syntax:"<length-percentage>";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"<length-percentage>";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-content{syntax:"*";inherits:false;initial-value:""}</style> 30
30<script> 31 // Inject the hide rule at runtime (survives the critical-CSS prune), disable the 32 // browser's native scroll restoration (it jumps to a stale, clamped position before 33 // lazy chunks load), and â on a full load/refresh with a saved position â hide the 34 // page before first paint. Safety timeout reveals regardless if JS stalls. 35 (function () { 36 try { 37 var s = document.createElement('style'); 38 s.textContent = 'html.restoring-scroll body{opacity:0!important}'; 39 document.head.appendChild(s); 40 if ('scrollRestoration' in history) history.scrollRestoration = 'manual'; 41 var y = sessionStorage.getItem('homeScrollY'); 42 if (location.pathname === '/' && y && parseInt(y, 10) > 0) { 43 document.documentElement.classList.add('restoring-scroll'); 44 setTimeout(function () { 45 document.documentElement.classList.remove('restoring-scroll'); 46 }, 2500); 47 } 48 } catch (e) {} 49 })(); 50 </script>
50 51
51<script> 52 // PostHog â production-only, pageview-scoped, deferred past window.load, 53 // attribution-parity across the deferral window. 54 // Hostname gate: matches the App.jsx / DownloadRedirectPage.jsx idiom. 55 // Keeps localhost, Vercel preview URLs, and the ReactSnap prerender 56 // crawl (runs against a local file server) out of the prod project. 57 // Landing snapshot: captures location.href / pathname / referrer sync 58 // in <head>, before any client-side navigation can rewrite them. 59 // After the deferred SDK loads, we emit the initial $pageview using 60 // this snapshot so the report shows the true landing URL even when 61 // the user has already navigated to another SPA route by the time 62 // init completes. Every emitted pageview also explicitly passes 63 // $session_entry_url / $session_entry_pathname from the snapshot so 64 // PostHog's auto-derived (post-nav) values never override them. 65 // Pre-init history queue: we wrap history.pushState / replaceState 66 // and listen for popstate synchronously in <head>. Before init the 67 // handler appends the current route to a queue; after init the 68 // queue is flushed as $pageview events (deduped by path+search) 69 // and the handler switches to direct capture. This means SPA 70 // navigations that happen during the deferral window (e.g. / to 71 // /about before the SDK loaded) are still captured, in order. 72 // Deferred load: window.load THEN requestIdleCallback (setTimeout 73 // fallback, 3s timeout) â RIC alone can fire mid-parse before LCP, 74 // so we wait for load first, then let the browser pick an idle 75 // slice. array.js (~90 kB gzip) never competes with the app module. 76 // Locked-down init: every remotely-configurable event source is 77 // explicitly disabled â autocapture, heatmaps, web vitals 78 // (capture_performance), page leave, session replay, surveys. 79 // capture_pageview: false suppresses the SDK's own init-time 80 // pageview so it can't fire against the post-navigation URL. 81 // before_send: the SDK's calculateEventProperties() runs its 82 // sessionPropsManager AFTER caller-supplied capture properties, 83 // so passing $session_entry_url/$session_entry_pathname in the 84 // capture call gets overwritten with the post-navigation URL 85 // before the event leaves the client. before_send runs after 86 // that enrichment, so we set the session-entry fields here from 87 // the snapshot â this is the final wire value. The override is 88 // scoped by session boundaries: we only apply it when this page 89 // load actually started the current PostHog session. Ownership 90 // is determined from the session id attached to the FIRST 91 // captured event, because PostHog's capture() runs the 92 // non-read-only session path (which does inactivity rotation) 93 // while ph.get_session_id() uses the read-only path and skips 94 // it. On the first event we snapshot its $session_id as 95 // `initialSessionId` and compute 96 // `hadPriorSession = (preInitSessionId === initialSessionId)`. 97 // If PostHog rotated the persisted session at init because it 98 // was idle-expired, the ids differ and we own the fresh 99 // session (override applies). If PostHog kept the persisted 100 // id, we joined an existing session (override skipped). Later 101 // events check parity against `initialSessionId`; a subsequent 102 // rotation makes the ids diverge and we stop overriding. The 103 // handler then generic-scans every string property for 104 // /invite/<code> and redacts it unconditionally, defense-in-depth 105 // against family invite codes leaking via any URL-shaped 106 // property (existing or future). Mirrored in 107 // src/utils/posthogBeforeSend.js and covered by 108 // scripts/posthog-before-send.test.js, which tests both the 109 // module and this inline copy (via string extraction) so drift 110 // between them fails a test. 111 // See PrivacyPolicyPage.jsx §"Analytics and Session Replay" for the 112 // disclosure that mirrors this configuration. 113 (function () { 114 var h = window.location.hostname; 115 if (h !== 'ingredicheck.app' && h !== 'www.ingredicheck.app') return; 116 var POSTHOG_KEY = 'phc_BFYelq2GeyigXBP3MgML57wKoWfLe5MW7m6HMYhtX8m'; 117 var initialHref = window.location.href; 118 var initialPathname = window.location.pathname; 119 var initialSearch = window.location.search || ''; 120 var initialReferrer = document.referrer; 121 var INVITE_RE = /\/invite\/[^/?#]+/g; 122 // Mirror of redactInvites in src/utils/posthogBeforeSend.js. Walks a 123 // properties bag and rewrites any string containing /invite/<code> 124 // in place. Called in before_send on event.properties, event.$set, 125 // event.$set_once, and the nested properties.$set / $set_once â with 126 // person_profiles: 'always', the SDK writes $initial_current_url / 127 // $initial_pathname into $set_once on the first event of a fresh 128 // person, sitting outside event.properties. Without this the invite 129 // code leaks in person properties even when pageview properties are 130 // clean. 131 var redactInvites = function (bag) { 132 if (!bag || typeof bag !== 'object') return; 133 for (var key in bag) { 134 var v = bag[key]; 135 if (typeof v === 'string' && v.indexOf('/invite/') !== -1) { 136 bag[key] = v.replace(INVITE_RE, '/invite/[redacted]'); 137 } 138 } 139 }; 140 // Mirror of extractPersistedSessionId in src/utils/posthogBeforeSend.js 141 // (this snippet must run synchronously in <head> and therefore can't 142 // import). Reads the raw stored value from `ph_<token>_posthog`, 143 // returns the persisted session id string, or null if no valid 144 // session state is present. $sesid layout is 145 // [lastActivityTimestamp, sessionId, sessionStartTimestamp]. 146 var extractPersistedSessionId = function (storedValue) { 147 if (typeof storedValue !== 'string') return null; 148 try { 149 var parsed = JSON.parse(storedValue); 150 if (parsed && Array.isArray(parsed.$sesid) && typeof parsed.$sesid[1] === 'string') { 151 return parsed.$sesid[1]; 152 } 153 } catch (e) {} 154 return null; 155 }; 156 // Capture the persisted session id from localStorage BEFORE
157 // posthog.init runs. In the loaded callback we compare it to 158 // ph.get_session_id() to determine whether PostHog continued the 159 // pre-existing session (hadPriorSession = true â skip the 160 // session-entry override) or started a fresh one (hadPriorSession 161 // stays false â override is safe until the session rotates again). 162 // Delegating the validity check to PostHog avoids hard-coding its 163 // inactivity + max-lifetime timeout semantics here. 164 var preInitSessionId = null; 165 try { 166 preInitSessionId = extractPersistedSessionId(window.localStorage.getItem('ph_' + POSTHOG_KEY + '_posthog')); 167 } catch (e) {} 168 var hadPriorSession = false; 169 // Populated in the loaded callback via ph.get_session_id(). before_send 170 // reads this at event time â if PostHog later rotates the session 171 // due to inactivity, event.$session_id will differ from this value 172 // and we stop overriding the session-entry fields. 173 var initialSessionId = null; 174 var lastPath = initialPathname + initialSearch; 175 var pendingNavs = []; 176 var phReady = false; 177 // Tracking-parameter keys stripped by CanonicalUrlNormalizer 178 // (mirror of CRAWL_NOISE_PARAMS + the 'utm_' prefix rule in 179 // src/utils/urlNormalization.js). When the ONLY difference 180 // between lastPath and the current URL is these params being 181 // removed (or added), the "navigation" is the normalizer running, 182 // not a real route change â skip the $pageview so campaign-tagged 183 // landings don't get double-counted. 184 var TRACKING_PARAM_KEYS = ['utm_source','utm_medium','utm_campaign','utm_term','utm_content','fbclid','from','gbraid','gclid','mc_cid','mc_eid','ref','wbraid']; 185 var isTrackingParam = function (name) { 186 var lower = String(name).toLowerCase(); 187 if (lower.indexOf('utm_') === 0) return true; 188 for (var ti = 0; ti < TRACKING_PARAM_KEYS.length; ti++) { 189 if (TRACKING_PARAM_KEYS[ti] === lower) return true; 190 } 191 return false; 192 }; 193 var isTrackingOnlyDiff = function (prevSearch, nextSearch) { 194 // Return true when every param that differs between the two 195 // search strings is a tracking param. Same pathname + this diff 196 // == the normalizer stripping crawl noise, not a real nav. 197 try { 198 var prev = new URLSearchParams(prevSearch || ''); 199 var next = new URLSearchParams(nextSearch || ''); 200 var seen = {}; 201 var keys = []; 202 var addKey = function (k) { if (!seen[k]) { seen[k] = true; keys.push(k); } }; 203 var pk = prev.keys(); 204 for (var pStep = pk.next(); !pStep.done; pStep = pk.next()) addKey(pStep.value); 205 var nk = next.keys(); 206 for (var nStep = nk.next(); !nStep.done; nStep = nk.next()) addKey(nStep.value); 207 for (var ki = 0; ki < keys.length; ki++) { 208 var key = keys[ki]; 209 if (prev.get(key) !== next.get(key) && !isTrackingParam(key)) return false; 210 } 211 return true; 212 } catch (e) { return false; } 213 }; 214 var splitPath = function (p) { 215 var qi = p.indexOf('?'); 216 return qi === -1 ? { pathname: p, search: '' } : { pathname: p.substring(0, qi), search: p.substring(qi) }; 217 }; 218 var handleNav = function () { 219 if (!phReady) { 220 if (pendingNavs && pendingNavs.length < 50) { 221 pendingNavs.push({ 222 href: window.location.href, 223 pathname: window.location.pathname, 224 search: window.location.search || '' 225 }); 226 } 227 return; 228 } 229 var currentPathname = window.location.pathname; 230 var currentSearch = window.location.search || ''; 231 var current = currentPathname + currentSearch; 232 if (current === lastPath) return; 233 var last = splitPath(lastPath); 234 if (last.pathname === currentPathname && isTrackingOnlyDiff(last.search, currentSearch)) { 235 // Absorb the URL change without emitting â same page, just the 236 // crawl-noise stripper running.
237 lastPath = current; 238 return; 239 } 240 lastPath = current; 241 window.posthog.capture('$pageview'); 242 }; 243 var _push = history.pushState; 244 var _replace = history.replaceState; 245 history.pushState = function () { 246 var r = _push.apply(this, arguments); 247 handleNav(); 248 return r; 249 }; 250 history.replaceState = function () { 251 var r = _replace.apply(this, arguments); 252 handleNav(); 253 return r; 254 }; 255 window.addEventListener('popstate', handleNav); 256 var loadPh = function () { 257 var s = document.createElement('script'); 258 s.src = 'https://us-assets.i.posthog.com/static/array.js'; 259 s.async = true; 260 s.crossOrigin = 'anonymous'; 261 s.onerror = function () { pendingNavs = null; }; 262 s.onload = function () { 263 window.posthog.init(POSTHOG_KEY, { 264 api_host: 'https://us.i.posthog.com', 265 autocapture: false, 266 capture_pageview: false, 267 capture_pageleave: false, 268 capture_heatmaps: false, 269 capture_performance: false, 270 disable_session_recording: true, 271 disable_surveys: true, 272 person_profiles: 'always', 273 before_send: function (event) { 274 if (!event || !event.properties) return event; 275 var currentSid = event.properties.$session_id; 276 // Snapshot ownership on the first event with a session id. 277 // PostHog's non-read-only session path runs inside capture(), 278 // so the id on the outbound event reflects any rotation 279 // triggered at init (idle-expired persisted session). We 280 // trust the event over ph.get_session_id(), which uses the 281 // read-only path and skips rotation. 282 if (initialSessionId == null && currentSid != null) { 283 initialSessionId = currentSid; 284 hadPriorSession = (preInitSessionId != null && preInitSessionId === currentSid); 285 } 286 var isOwnSession = !hadPriorSession && initialSessionId != null && currentSid === initialSessionId; 287 if (isOwnSession) { 288 event.properties.$session_entry_url = initialHref; 289 event.properties.$session_entry_pathname = initialPathname; 290 } 291 redactInvites(event.properties); 292 redactInvites(event.$set); 293 redactInvites(event.$set_once); 294 redactInvites(event.properties.$set); 295 redactInvites(event.properties.$set_once); 296 return event; 297 }, 298 loaded: function (ph) { 299 // Snapshot campaign params from the landing URL BEFORE any React 300 // effects run â CanonicalUrlNormalizer strips utm_*/fbclid/gclid 301 // etc. from the visible URL as crawl noise, so we capture them 302 // here (from initialSearch, taken sync in <head>). 303 // 304 // Persistence scope matters: ph.register() persists forever, so 305 // registering utm_source on visit A would carry over into 306 // visit B (different or no campaign) and pollute attribution. 307 // We (1) explicitly unregister every campaign key first â clears 308 // any residue from a prior visit â then (2) register the 309 // current-URL params for the CURRENT SESSION only via 310 // register_for_session (available since posthog-js 1.152.0). 311 // Fallback path uses register() when register_for_session 312 // isn't available; the explicit unregister above still keeps 313 // it visit-scoped in practice because the next visit will 314 // clear it before registering anything new. 315 try { 316 var campaignKeys = ['utm_source','utm_medium','utm_campaign','utm_term','utm_content','fbclid','gclid','gbraid','wbraid','mc_cid','mc_eid','ref']; 317 var campaignProps = {}; 318 var hasCampaign = false; 319 if (initialSearch) { 320 var params = new URLSearchParams(initialSearch); 321 for (var ci = 0; ci < campaignKeys.length; ci++) { 322 var ck = campaignKeys[ci]; 323 var cv = params.get(ck); 324 if (cv) {
325 campaignProps[ck] = cv; 326 hasCampaign = true; 327 } 328 } 329 } 330 if (typeof ph.unregister === 'function') { 331 for (var ucIdx = 0; ucIdx < campaignKeys.length; ucIdx++) { 332 ph.unregister(campaignKeys[ucIdx]); 333 } 334 } 335 if (hasCampaign) { 336 if (typeof ph.register_for_session === 'function') { 337 ph.register_for_session(campaignProps); 338 } else if (typeof ph.register === 'function') { 339 ph.register(campaignProps); 340 } 341 } 342 } catch (e) {} 343 ph.capture('$pageview', { 344 $current_url: initialHref, 345 $pathname: initialPathname, 346 $referrer: initialReferrer 347 }); 348 for (var i = 0; i < pendingNavs.length; i++) { 349 var n = pendingNavs[i]; 350 var pathKey = n.pathname + n.search; 351 if (pathKey === lastPath) continue; 352 var lastForFlush = splitPath(lastPath); 353 if (lastForFlush.pathname === n.pathname && isTrackingOnlyDiff(lastForFlush.search, n.search)) { 354 // Queued replaceState from CanonicalUrlNormalizer stripping 355 // tracking params â same page, absorb without emitting. 356 lastPath = pathKey; 357 continue; 358 } 359 lastPath = pathKey; 360 ph.capture('$pageview', { 361 $current_url: n.href, 362 $pathname: n.pathname 363 }); 364 } 365 pendingNavs = null; 366 // Drain the pending-events queue populated by utils/analytics.js 367 // captureEvent() calls that fired before window.posthog existed 368 // (user clicks between page paint and deferred SDK init). 369 try { 370 var pendingEvents = window.__phPendingEvents; 371 if (pendingEvents && pendingEvents.length) { 372 for (var pi = 0; pi < pendingEvents.length; pi++) { 373 var ev = pendingEvents[pi]; 374 if (ev && ev.name) ph.capture(ev.name, ev.props); 375 } 376 } 377 } catch (e) {} 378 window.__phPendingEvents = null; 379 phReady = true; 380 } 381 }); 382 }; 383 document.head.appendChild(s); 384 }; 385 window.addEventListener('load', function () { 386 if ('requestIdleCallback' in window) { 387 window.requestIdleCallback(loadPh, { timeout: 3000 }); 388 } else { 389 setTimeout(loadPh, 1000); 390 } 391 }); 392 })(); 393 </script>
393 394
394<script type="module" crossorigin src="/assets/main-CNs67Yuc.js"></script>
394 395 <link rel="stylesheet" crossorigin href="/assets/main-Cg1r_LRd.css"><noscript><link rel="stylesheet" crossorigin href="/assets/main-Cg1r_LRd.css"></noscript> 396 <title>Redirecting to App Store | IngrediCheck</title><meta data-ingredicheck-seo="true" name="description" content="Redirecting to IngrediCheck on App Store."><meta data-ingredicheck-seo="true" property="og:type" content="website"><meta data-ingredicheck-seo="true" property="og:title" content="Redirecting to App Store | IngrediCheck"><meta data-ingredicheck-seo="true" property="og:description" content="Redirecting to IngrediCheck on App Store."><meta data-ingredicheck-seo="true" property="og:site_name" content="IngrediCheck"><meta data-ingredicheck-seo="true" property="og:locale" content="en_US"><meta data-ingredicheck-seo="true" name="twitter:card" content="summary"><meta data-ingredicheck-seo="true" name="twitter:title" content="Redirecting to App Store | IngrediCheck"><meta data-ingredicheck-seo="true" name="twitter:description" content="Redirecting to IngrediCheck on App Store."><meta data-ingredicheck-seo="true" name="twitter:site" content="@ingredicheck"><meta data-ingredicheck-seo="true" name="robots" content="noindex, nofollow"></head> 397 <body> 398 <div id="root"><div class="min-h-screen bg-white relative overflow-x-clip"><div class="xl:hidden fixed inset-0 pointer-events-none" style="z-index: 0; background-image: repeating-linear-gradient(to right, transparent, transparent 47px, rgb(224, 224, 224) 47px, rgb(224, 224, 224) 48px), repeating-linear-gradient(transparent, transparent 47px, rgb(224, 224, 224) 47px, rgb(224, 224, 224) 48px); opacity: 0.5;"></div><div class="hidden xl:block fixed inset-0 pointer-events-none" style="z-index: 2; background-image: repeating-linear-gradient(to right, transparent, transparent 47px, rgb(224, 224, 224) 47px, rgb(224, 224, 224) 48px), repeating-linear-gradient(transparent, transparent 47px, rgb(224, 224, 224) 47px, rgb(224, 224, 224) 48px); opacity: 0; transition: opacity 0.3s ease 0s;"></div><div class="relative z-[5]"><nav class="site-navbar w-full pt-[16px] sm:pt-[17px] xl:pt-[17px] sticky top-0 z-[100] pb-[17px] -mb-[17px] transition-all duration-300 bg-transparent"><div class="max-w-[1440px] mx-auto px-[10px] sm:px-6 lg:px-10 xl:px-[150px]"><div class="flex items-center justify-between h-[52px]"><a href="/" class="flex-shrink-0" aria-label="IngrediCheck home"><img alt="IngrediCheck" width="190" height="33" class="h-[28px] w-[150px] sm:h-[33px] sm:w-[190px]" src="/images/ingredicheck.svg"></a><div class="hidden xl:flex items-center gap-[46px]"><a class="font-figtree text-[16px] leading-[24px] transition-colors duration-200 font-normal text-text-muted hover:text-text-body" href="/" data-discover="true">Home</a><a href="/#about" class="font-figtree text-[16px] leading-[24px] transition-colors duration-200 font-normal text-text-muted hover:text-text-body">About</a><a class="font-figtree text-[16px] leading-[24px] transition-colors duration-200 font-normal text-text-muted hover:text-text-body" href="/blog" data-discover="true">Blog</a><a href="/#why-ingredicheck" class="font-figtree text-[16px] leading-[24px] transition-colors duration-200 font-normal text-text-muted hover:text-text-body">Features</a><a href="/#faqs" class="font-figtree text-[16px] leading-[24px] transition-colors duration-200 font-normal text-text-muted hover:text-text-body">FAQs</a></div><div class="hidden xl:block opacity-100 transition-opacity duration-300 w-[158px] flex-shrink-0"><div class="download-nav-cta group/cta inline-flex items-center rounded-[26px] h-[52px] px-[24px] py-[16px] text-white font-figtree text-[16px] font-semibold leading-[18px] cursor-pointer hover:brightness-110"><span class="whitespace-nowrap">Get the app</span><img alt class="ml-3 flex-shrink-0 w-[20px] h-[20px] transition-all duration-300 group-hover/cta:opacity-0 group-hover/cta:w-0 group-hover/cta:ml-0" src="/images/download-icon.svg"><div class="flex items-center gap-1.5 max-w-0 opacity-0 overflow-hidden group-hover/cta:max-w-[100px] group-hover/cta:opacity-100 group-hover/cta:ml-3 transition-all duration-300"><a href="/download/ios?source=%2Fdownload%2Fios" target="_blank" rel="noopener noreferrer" class="group/apple relative flex items-center justify-center w-[32px] h-[32px] rounded-full overflow-hidden flex-shrink-0 "><div class="absolute inset-0 rounded-full bg-white opacity-0 group-hover/apple:opacity-100 transition-opacity duration-300"></div><img alt="App Store" class="relative z-10 h-[18px] w-auto transition-all duration-300 group-hover/apple:invert" src="/images/apple.webp"></a>
398<a href="/download/android?source=%2Fdownload%2Fios" target="_blank" rel="noopener noreferrer" class="group/play relative flex items-center justify-center w-[32px] h-[32px] rounded-full overflow-hidden flex-shrink-0 "><div class="absolute inset-0 rounded-full bg-white opacity-0 group-hover/play:opacity-100 transition-opacity duration-300"></div><svg viewBox="0 0 24 24" width="18" height="18" class="relative z-10 transition-opacity duration-300 group-hover/play:opacity-0" aria-hidden="true"><path d="M3.6 1.8C3.2 2.1 3 2.6 3 3.2v17.6c0 .6.2 1.1.6 1.4l10-10.1L3.6 1.8z" fill="#ffffff"/><path d="M17 8.8l-2.6 2.6-10.4-10c.5-.3 1.1-.3 1.6 0L17 8.8z" fill="#ffffff" fill-opacity="0.85"/><path d="M17 15.2L5.6 22.6c-.5.3-1.1.3-1.6 0l10.4-10 2.6 2.6z" fill="#ffffff" fill-opacity="0.7"/><path d="M20.8 10.9c.7.4.7 1.4 0 1.8L17.4 14.6l-2.8-2.6 2.8-2.6 3.4 1.5z" fill="#ffffff" fill-opacity="0.55"/></svg><img alt="Google Play" class="absolute z-10 h-[18px] w-auto opacity-0 group-hover/play:opacity-100 transition-opacity duration-300" src="/images/playstore.webp"></a></div></div></div><div class="xl:hidden flex items-center gap-3"><a href="/download/ios?source=%2Fdownload%2Fios" target="_blank" rel="noopener noreferrer" aria-label="Download on the App Store" class="flex items-center justify-center w-[40px] h-[40px] rounded-full flex-shrink-0 bg-white shadow-[0_4px_11px_rgba(0,0,0,0.12)]"><img alt class="h-[20px] w-auto brightness-0" src="/images/apple.webp"></a><button class="flex flex-col gap-1.5 p-2" aria-label="Toggle menu"><span class="block w-6 h-0.5 bg-text-body transition-transform "></span><span class="block w-6 h-0.5 bg-text-body transition-opacity "></span><span class="block w-6 h-0.5 bg-text-body transition-transform "></span></button></div></div></div></nav><main class="min-h-[60vh] flex items-center justify-center px-[20px] py-[100px]"><div class="max-w-[480px] text-center"><h1 class="font-protest text-[42px] sm:text-[56px] leading-[1.05] text-text-dark">Opening App Store</h1><p class="mt-[16px] font-figtree text-[18px] leading-[1.6] text-text-subtitle">If the store does not open, use the button below to keep going.</p><a href="https://apps.apple.com/app/id6477521615" class="inline-flex items-center justify-center mt-[28px] px-[24px] h-[52px] rounded-full text-white font-figtree text-[16px] font-semibold" style="background: linear-gradient(143.74deg, rgb(138, 208, 17) 21.16%, rgb(108, 142, 6) 68.87%);">Get the app on App Store</a></div></main></div></div></div> 399 400 401</body></html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.