1<!doctype html> 2<html lang="en"><head><meta charset="utf-8"> 3<meta name="viewport" content="width=device-width,initial-scale=1"> 4<title>Pricing: Ari from $99 a month | Ari</title><meta name="description" content="Ari costs $99 a month per Magento store with unlimited seats. Add Ari engineers for review and release, or ask for an Enterprise plan with custom volumes."> 5<link rel="stylesheet" href="style.css?v=a80afb6b"><link rel="stylesheet" href="hero.css?v=a80afb6b"> 6<link rel="stylesheet" href="pages.css?v=a80afb6b"> 7<link rel="stylesheet" href="mobile.css?v=a80afb6b"> 8<!--seo--><link rel="canonical" href="https://www.withari.ai/pricing"> 9<meta name="robots" content="index,follow,max-image-preview:large"> 10<meta name="theme-color" content="#14130F"> 11<link rel="icon" href="/favicon.ico" sizes="48x48"> 12<link rel="icon" href="/favicon.svg" type="image/svg+xml"> 13<link rel="icon" href="/icon-192.png" type="image/png" sizes="192x192"> 14<link rel="apple-touch-icon" href="/apple-touch-icon.png"> 15<link rel="manifest" href="/site.webmanifest"> 16<meta property="og:site_name" content="Ari"> 17<meta property="og:type" content="website"> 18<meta property="og:url" content="https://www.withari.ai/pricing"> 19<meta property="og:title" content="Pricing: Ari from $99 a month | Ari"> 20<meta property="og:description" content="Ari costs $99 a month per Magento store with unlimited seats. Add Ari engineers for review and release, or ask for an Enterprise plan with custom volumes."> 21<meta property="og:image" content="https://www.withari.ai/img/og/pricing.jpg"> 22<meta property="og:image:width" content="1200"><meta property="og:image:height" content="630"> 23<meta property="og:image:alt" content="A month of Ari for less than 2 developer hours"> 24<meta property="og:locale" content="en_US"> 25<meta name="twitter:card" content="summary_large_image"> 26<meta name="twitter:title" content="Pricing: Ari from $99 a month | Ari"> 27<meta name="twitter:description" content="Ari costs $99 a month per Magento store with unlimited seats. Add Ari engineers for review and release, or ask for an Enterprise plan with custom volumes."> 28<meta name="twitter:image" content="https://www.withari.ai/img/og/pricing.jpg"> 29<style> 30.cc{position:fixed;left:16px;bottom:16px;z-index:200;width:min(420px,calc(100vw - 32px));background:rgba(20,19,15,.96);color:#F3F1EA;border:1px solid rgba(243,241,234,.18);box-shadow:0 30px 60px -20px rgba(0,0,0,.6);padding:20px 20px 18px;
30font:14px/1.5 Inter,-apple-system,sans-serif;backdrop-filter:blur(10px);transform:translateY(20px);opacity:0;transition:transform .35s cubic-bezier(.2,.7,.2,1),opacity .35s} 31.cc.on{transform:none;opacity:1} 32.cc b{display:flex;align-items:center;gap:10px;font-weight:500;font-size:15px;margin-bottom:6px} 33.cc b svg{width:22px;height:19px;flex:none} 34.cc p{margin:0;color:rgba(243,241,234,.72)} 35.cc p a{color:#F3F1EA;text-decoration:underline;text-underline-offset:3px} 36.cc-b{display:flex;gap:10px;margin-top:16px} 37.cc-b button{flex:1;font:inherit;font-weight:500;padding:10px 12px;cursor:pointer;border:1px solid rgba(243,241,234,.4);background:transparent;color:#F3F1EA;border-radius:0} 38.cc-b button.y{background:#2D572F;border-color:#2D572F} 39.cc-b button:hover{border-color:#F3F1EA}.cc-b button.y:hover{background:#346637} 40@media(max-width:560px){.cc{left:12px;right:12px;bottom:12px;width:auto}} 41</style>
42<script> 43(function(){ 44 var K='ari-cc', v=null; try{ v=localStorage.getItem(K); var at=+localStorage.getItem(K+'-at')||0; if(v&&Date.now()-at>365*864e5){ v=null; } }catch(e){} 45 function load(){ if(document.getElementById('hs-script-loader')) return; 46 var s=document.createElement('script'); s.id='hs-script-loader'; s.async=true; s.defer=true; s.src='//js-eu1.hs-scripts.com/25724996.js'; document.head.appendChild(s); } 47 function drop(){ (window._hsq=window._hsq||[]).push(['doNotTrack']); 48 ['__hstc','hubspotutk','__hssc','__hssrc'].forEach(function(n){ document.cookie=n+'=; Max-Age=0; path=/; domain=.'+location.hostname.replace(/^www\./,''); document.cookie=n+'=; Max-Age=0; path=/'; }); } 49 if(v==='yes') load(); 50 function show(){ 51 var el=document.getElementById('cc'); if(el){ el.classList.add('on'); return; } 52 el=document.createElement('div'); el.id='cc'; el.className='cc'; el.setAttribute('role','dialog'); el.setAttribute('aria-label','Cookie choice'); 53 el.innerHTML='<b><svg viewBox="103.37 131.83 290.55 248.34" aria-hidden="true"><g transform="translate(118.08,146.54) scale(1.0946)"><path d="M0,0 L100,100 L0,200" fill="none" stroke="#F3F1EA" stroke-width="38"/><path d="M140,194.44 L252,194.44" fill="none" stroke="#9DC49F" stroke-width="38"/></g></svg>We use cookies</b>' 54 +'<p>Analytics cookies help us understand how the site is used. We only set them if you accept. Read the <a href="/cookies">cookie policy</a>.</p>' 55 +'<div class="cc-b"><button type="button" class="n">Decline</button><button type="button" class="y">Accept</button></div>'; 56 document.body.appendChild(el); 57 el.querySelector('.y').onclick=function(){ try{localStorage.setItem(K,'yes');localStorage.setItem(K+'-at',Date.now());}catch(e){} load(); hide(); }; 58 el.querySelector('.n').onclick=function(){ var was=v==='yes'; try{localStorage.setItem(K,'no');localStorage.setItem(K+'-at',Date.now());}catch(e){} v='no'; if(was||document.getElementById('hs-script-loader')) drop(); hide(); }; 59 requestAnimationFrame(function(){ requestAnimationFrame(function(){ el.classList.add('on'); }); }); 60 } 61 function hide(){ var el=document.getElementById('cc'); if(el){ el.classList.remove('on'); setTimeout(function(){ el.remove(); },400); } } 62 document.addEventListener('click',function(e){ var a=e.target.closest&&e.target.closest('[data-cc-open]'); if(a){ e.preventDefault(); try{ v=localStorage.getItem(K); }catch(x){} show(); } }); 63 if(v!=='yes'&&v!=='no'){ if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',show); else show(); } 64})(); 65</script>
vendor: 1 bytes, line 65
65
66<script type="application/ld+json">{"@context":"https://schema.org","@graph":[{"@type":"WebPage","@id":"https://www.withari.ai/pricing#page","url":"https://www.withari.ai/pricing","name":"Pricing: Ari from $99 a month | Ari","description":"Ari costs $99 a month per Magento store with unlimited seats. Add Ari engineers for review and release, or ask for an Enterprise plan with custom volumes.","isPartOf":{"@id":"https://www.withari.ai/#site"},"about":{"@id":"https://www.withari.ai/#app"},"primaryImageOfPage":"https://www.withari.ai/img/og/pricing.jpg","inLanguage":"en-US"},{"@type":"SoftwareApplication","@id":"https://www.withari.ai/#app","name":"Ari","alternateName":"Agent for Magento","applicationCategory":"DeveloperApplication","operatingSystem":"Web","url":"https://www.withari.ai/","description":"An AI agent that makes changes to Magento and Adobe Commerce stores from a chat: fixes, extensions, upgrades, storefront changes and Hyvä rebuilds, built on a copy of the store and approved by a person.","publisher":{"@id":"https://www.withari.ai/#org"},"offers":[{"@type":"Offer","name":"Ari","price":"99","priceCurrency":"USD","priceSpecification":{"@type":"UnitPriceSpecification","price":"99","priceCurrency":"USD","unitText":"month"}},{"@type":"Offer","name":"Ari + human","price":"990","priceCurrency":"USD","priceSpecification":{"@type":"UnitPriceSpecification","price":"990","priceCurrency":"USD","unitText":"month"}}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://www.withari.ai/"},{"@type":"ListItem","position":2,"name":"A month of Ari for less than 2 developer hours","item":"https://www.withari.ai/pricing"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"Can we use Ari without your team?","acceptedA
66nswer":{"@type":"Answer","text":"Yes. That is the $99 plan. You run Ari, and your own developers read the pull request and put it live. The $990 plan is for when you would rather ours did that part."}},{"@type":"Question","name":"Who puts the change live?","acceptedAnswer":{"@type":"Answer","text":"You do, on your own release process. Ari has no access to your servers, your pipeline or your live database. On the Ari plus human plan Ari engineers deploy, so they get access to your release process. Ari still does not."}},{"@type":"Question","name":"What happens if something breaks?","acceptedAnswer":{"@type":"Answer","text":"Ari never touches your live store. Work comes back as a pull request, so a change nobody merges changes nothing. There are no restore points and no one click undo: a change that is already live is reversed the same way you reverse any release."}},{"@type":"Question","name":"How fast do you answer when we need help?","acceptedAnswer":{"@type":"Answer","text":"24/7 with a one hour response is Enterprise. Every other plan agrees a response time with you in writing when you sign."}},{"@type":"Question","name":"What do we have to give you to set it up?","acceptedA
66nswer":{"@type":"Answer","text":"We need three things from you: a cleaned database dump, your codebase in a fresh repository with its history, and composer credentials. We run a copy of your store on our own machines. Nothing runs against your live one."}},{"@type":"Question","name":"Can you take over a store from another agency?","acceptedAnswer":{"@type":"Answer","text":"Yes. Ari reads the codebase you actually have, not the one somebody wrote a document about. Your code stays in your own repository the whole time."}},{"@type":"Question","name":"Which AI model is behind it?","acceptedAnswer":{"@type":"Answer","text":"We do not name it, and it changes as better ones arrive. What you can check is the output: every change comes back as a pull request with every line it changed in it."}},{"@type":"Question","name":"Can we try it before paying?","acceptedAnswer":{"@type":"Answer","text":"Yes. The demo store is ready the moment you ask."}},{"@type":"Question","name":"Can we cancel?","acceptedAnswer":{"@type":"Answer","text":"Monthly. Your code stays in your own repository, so leaving costs you nothing but the subscription."}}]}]}</script>
66<!--/seo--> 67</head><body class="sharp"> 68<header class="nav" id="nav"><div class="nav-i"> 69 <a class="brand" href="/" aria-label="Ari, agent for Magento"><svg class="bm-i" viewBox="103.37 131.83 290.55 248.34" width="291" height="248" fill="none" aria-hidden="true"><g transform="translate(118.08,146.54) scale(1.0946)"><path d="M0,0 L100,100 L0,200" fill="none" stroke="currentColor" stroke-width="38" stroke-linecap="butt" stroke-linejoin="miter"/><path d="M140,194.44 L252,194.44" fill="none" stroke="var(--brand-accent,#2D572F)" stroke-width="38" stroke-linecap="butt"/></g></svg><b class="bm-t">Ari</b></a> 70 <button class="navtog" id="navtog" type="button" aria-label="Menu" aria-expanded="false"> 71 <span></span><span></span><span></span></button> 72 <nav> 73 <button class="navitem" data-menu="prod" aria-expanded="false">Product <svg viewBox="0 0 10 6" fill="none"><path d="M1 1l4 4 4-4" stroke="currentColor" stroke-width="1.4"/></svg></button> 74 <div class="mega mega-s" id="m-prod"><div class="mega-i"><div class="dd"><a href="/how-it-works"><b>How it works</b><span>From a sentence to a change you approve</span></a><a href="/security"><b>Security and access</b><span>What it can read, what it can write, and who approves changes</span></a><a href="/your-team"><b>Teams and roles</b><span>Who asks, who approves, and who releases</span></a></div></div></div> 75 <button class="navitem" data-menu="use" aria-expanded="false">Use cases <svg viewBox="0 0 10 6" fill="none"><path d="M1 1l4 4 4-4" stroke="currentColor" stroke-width="1.4"/></svg></button> 76 <div class="mega mega-s" id="m-use"><div class="mega-i"><div class="dd"><span class="dd-lab">Who it is for</span><a href="/merchants"><b>For merchants</b><span>Your store, your team, and the small work off the backlog</span></a><a href="/agencies"><b>For agencies</b><span>Client stores, without the small work in your backlog</span></a><div class="dd-sep"></div><a href="/technical-improvements"><b>Technical improvements</b><span>Upgrades, patches, integrations, debugging, performance</span></a><a href="/maintenance"><b>Day-to-day maintenance</b><span>Extensions, configuration, the jobs too small for a ticket</span></a><a href="/storefront-changes"><b>Storefront changes</b><span>Layouts, mobile UI, campaign pages from your designs</span></a><a href="/content-merchandising"><b>Content and merchandising</b><span>Promo bars, CMS blocks, badges, catalog changes</span></a><a href="/hyva"><b>Hyvä</b><span>Hyvä Theme, Checkout and extensions, installed for you</span></a></div></div></div> 77 <a href="/pricing" aria-current="page">Pricing</a> 78 <a href="/news">News</a> 79 <a href="/contact">Contact us</a> 80 <a class="btn btn-s" href="/signup">Connect your store</a> 81 <a class="btn btn-o btn-s" href="https://demo.agentformagento.com/">Try demo</a> 82 </nav> 83</div> 84</header> 85<section class="sec tall" style="border-top:0;padding-top:clamp(30px,3.6vw,52px)"><div class="wrap"><div class="hd"><h1 class="pp-h">Pick a plan</h1></div><div class="pp-plans" id="ppPlans"><article class="pp-card"><div class="pp-box"><div class="pp-head"><h3>Ari</h3></div><p class="pp-c">Self-managed</p><div class="pp-p"><b>$99</b><span>a month, with 2,000 credits included</span></div><a class="pp-go" href="https://buy.stripe.com/eVq6oGfrYgcz49EbuY0gw00">Get started</a></div><ul class="pp-f"><li><b>✓</b><span>More credits can be purchased any time</span></li><li><b>✓</b><span>Frontend and backend development</span></li><li><b>✓</b><span>Extensions, integrations and version upgrades</span></li><li><b>✓</b><span>Hyvä themes, checkout and migrations</span></li><li><b>✓</b><span>Content, merchandising and SEO changes</span></li><li><b>✓</b><span>A preview store for every change</span></li><li><b>✓</b><span>A QA layer checks every change before you see it</span></li><li><b>✓</b><span>Unlimited seats for your PM, developers and agency</span></li><li><b>✓</b><span>You or your developers release it, on your schedule</span></li></ul></article><article class="pp-card on"><div class="pp-box"><div class="pp-head"><h3>Ari + human</h3><span class="pp-star">Highest value</span></div><p class="pp-c">Managed by Ari engineers</p><div class="pp-p"><b>$990</b><span>a month, with 2,000 credits included</span></div><a class="pp-go" href="https://buy.stripe.com/bJe00i6Vs3pN49Ecz20gw02">Get started</a></div><ul class="pp-f"><li><b>✓</b><span>Everything in Ari</span></li><li><b>✓</b><span>16 developer hours a month</span></li><li><b>✓</b><span>More credits can be purchased any time</span></li><li><b>✓</b><span>Development, QA and deployment</span></li><li><b>✓</b><span>Additional developer hours at $85 an hour</span></li></ul></article><article class="pp-card"><div class="pp-box"><div class="pp-head"><h3>Enterprise or Agency</h3></div><p class="pp-c">Custom volumes and terms: we handle onboarding, integrations and
85SSO.</p><div class="pp-p"><b class="pp-custom">Custom</b></div><a class="pp-go" href="/contact">Talk to us</a></div><ul class="pp-f"><li><b>✓</b><span>Custom volumes and invoicing</span></li><li><b>✓</b><span>Custom integrations, roles and permissions</span></li><li><b>✓</b><span>SSO, and 24/7 support with a one hour response</span></li><li><b>✓</b><span>One account for every client store, for agencies</span></li></ul></article></div>
85<script> 86(function(){ 87 var tog=document.getElementById('ppTog'), plans=document.getElementById('ppPlans'); 88 if(!tog||!plans) return; 89 var btns=[].slice.call(tog.querySelectorAll('button')), 90 cards=[].slice.call(plans.querySelectorAll('.pp-card')), yr=false; 91 function money(v){ return '$'+(+v).toLocaleString('en-US'); } 92 function render(){ 93 cards.forEach(function(card){ 94 var sel=card.querySelector('.pp-sel'), 95 b=card.querySelector('.pp-p b'), 96 src=sel ? sel.options[sel.selectedIndex] : b; 97 if(!b || src.getAttribute('data-m')===null) return; 98 b.textContent=money(src.getAttribute(yr?'data-y':'data-m')); 99 if(sel){ 100 card.querySelector('.pp-c').textContent=src.getAttribute('data-cr'); 101 card.querySelector('.pp-eq').textContent='\u2248 '+src.getAttribute('data-eq'); 102 } 103 }); 104 } 105 btns.forEach(function(x){ 106 x.addEventListener('click',function(){ 107 yr = x.getAttribute('data-v')==='y'; 108 btns.forEach(function(o){ o.setAttribute('aria-pressed',String(o===x)); }); 109 render(); 110 }); 111 }); 112 cards.forEach(function(card){ 113 var sel=card.querySelector('.pp-sel'); 114 if(sel) sel.addEventListener('change',render); 115 }); 116 render(); 117})(); 118</script>
118</div></section><section class="sec dark"><div class="wrap"><div class="hd"><h2>When a job needs a person</h2></div><div class="pp-sup"><div class="pp-sup-r"><div class="pp-head"><h3>Included hours</h3></div><p class="pp-c">They come with the Ari + human plan.</p><div class="pp-p"><b>16</b><span>hours a month</span></div><ul class="pp-f"><li><b>✓</b><span>Our own Magento developers, on your store</span></li><li><b>✓</b><span>Development, QA and deployment</span></li><li><b>✓</b><span>Asked for in the same chat you ask Ari</span></li><li><b>✓</b><span>Every change comes back as a pull request</span></li></ul></div><div class="pp-sup-r"><div class="pp-head"><h3>Additional hours</h3></div><p class="pp-c">They start once the 16 are spent.</p><div class="pp-p"><b>$85</b><span>an hour</span></div><ul class="pp-f"><li><b>✓</b><span>The same developers</span></li><li><b>✓</b><span>Asked for in the same chat</span></li><li><b>✓</b><span>Development, QA and deployment, the same as the 16</span></li></ul></div></div></div></section><section class="sec"><div class="wrap"><div class="hd"><h2>Questions</h2></div><div class="faqs"><details><summary>Can we use Ari without your team?</summary><p>Yes. That is the $99 plan. You run Ari, and your own developers read the pull request and put it live. The $990 plan is for when you would rather ours did that part.</p></details><details><summary>Who puts the change live?</summary><p>You do, on your own release process. Ari has no access to your servers, your pipeline or your live database. On the Ari plus human plan Ari engineers deploy, so they get access to your release process. Ari still does not.</p></details><details><summary>What happens if something breaks?</summary><p>Ari never touches your live store. Work comes back as a pull request, so a change nobody merges changes nothing. There are no restore points and no one click undo: a change that is already live is reversed the same way you reverse any release.</p></details><details><summary>How fast do you answer when we need help?</summary><p>24/7 with a one hour response is Enterprise. Every other plan agrees a response time with you in writing when you sign.</p></details><details><summary>What do we have to give you to set it up?</summary><p>We need three things from you: a cleaned database dump, your codebase in a fresh repository with its history, and composer credentials. We run a copy of your store on our own machines. Nothing runs against your live one.</p></details><details><summary>Can you take over a store from another agency?</summary><p>Yes. Ari reads the codebase you actually have, not the one somebody wrote a document about. Your code stays in your own repository the whole time.</p></details><details><summary>Which AI model is behind it?</summary><p>We do not name it, and it changes as better ones arrive. What you can check is the output: every change comes back as a pull request with every line it changed in it.</p></details><details><summary>Can we try it before paying?</summary><p>Yes. The demo store is ready the moment you ask.</p></details><details><summary>Can we cancel?</summary><p>Monthly. Your code stays in your own repository, so leaving costs you nothing but the subscription.</p></details></div></div></section> 119<footer class="foot2"><canvas class="foot2-cv" id="footcv" aria-hidden="true"></canvas><div class="wrap"> 120 <span class="kick">Ari, the first agent for Magento, by scandiweb</span> 121 <p class="foot2-big">Make changes to your Magento store in a chat</p> 122 <div class="foot2-rule"></div> 123 <div class="foot2-cols"> 124 <div><h4>Product</h4> 125 <a href="/how-it-works">How it works</a> 126 <a href="/security">Security and access</a> 127 <a href="/your-team">Teams and roles</a> 128 </div> 129 <div><h4>Use cases</h4> 130 <a href="/technical-improvements">Technical improvements</a> 131 <a href="/maintenance">Day-to-day maintenance</a> 132 <a href="/storefront-changes">Storefront changes</a> 133 <a href="/content-merchandising">Content and merchandising</a> 134 <a href="/hyva">Hyvä</a> 135 </div> 136 <div><h4>Start</h4> 137 <a href="/signup">Connect your store</a> 138 <a href="/contact">Contact us</a> 139 <a href="/pricing">Pricing</a> 140 </div> 141 <div><h4>Company</h4> 142 <a href="/news">News</a> 143 <a href="/merchants">For merchants</a> 144 <a href="/agencies">For agencies</a> 145 <a href="/scandiweb">scandiweb</a> 146 </div> 147 </div> 148 <div class="foot2-base"> 149 <span>© 2026 Ari.</span> 150 <a href="/privacy">Privacy</a> 151 <a href="/terms">Terms</a> 152 <a href="/cookies">Cookies</a>
153 <a href="#cookie-settings" data-cc-open>Cookie settings</a> 154 <span class="foot2-badges"> 155 <img src="img/logos/adobe-solution-partner-gold.svg?v=c38e0462" alt="Adobe Solution Partner Gold"> 156 <img src="img/logos/cert-3.svg?v=def9175e" alt="Anthropic partner"> 157 <img src="img/logos/hyva-most-certified.svg?v=41b8f8de" alt="Hyvä Platinum Partner"> 158 <img src="img/logos/openai-select-partner.svg?v=484ff256" alt="OpenAI Select Partner"> 159 </span> 160 </div> 161</div>
162<script data-pv-js> 163/* The preview store frames: the change arrives once the frame is looked at, 164 replays every few seconds while it stays in view, and inside the How it 165 works stage it plays when its step is the one showing. */ 166(function(){ 167 var ps=[].slice.call(document.querySelectorAll('.pv')); if(!ps.length) return; 168 var CALM=matchMedia('(prefers-reduced-motion:reduce)').matches; 169 if(CALM||!('IntersectionObserver' in window)){ ps.forEach(function(p){ p.classList.add('on'); }); return; } 170 function pane(p){ return p.closest('.hiw-p'); } 171 function active(p){ var q=pane(p); return !q || q.getAttribute('data-on')==='true'; } 172 function set(p){ p.classList.toggle('on', !!p._vis && active(p)); } 173 var io=new IntersectionObserver(function(es){ es.forEach(function(e){ e.target._vis=e.isIntersecting; set(e.target); }); },{threshold:.35}); 174 ps.forEach(function(p){ io.observe(p); var q=pane(p); if(q){ new MutationObserver(function(){ set(p); }).observe(q,{attributes:true,attributeFilter:['data-on']}); } }); 175 setInterval(function(){ ps.forEach(function(p){ if(p.classList.contains('on')){ p.classList.remove('on'); setTimeout(function(){ set(p); },420); } }); }, 7600); 176})(); 177</script>
vendor: 1 bytes, line 177
177
178<script data-stage-js> 179/* Mobile demo stages. 180 A phone cannot show a conversation and the storefront it changed at the same 181 time: side by side squeezes both, and a swipe rail still puts two panels on 182 screen at once. So below 940px a multi panel demo becomes a stage sequence, 183 the way the hero film already works. One panel at a time, cross faded, on a 184 timer, and it only runs while the frame is actually on screen. 185 Above 940px nothing here applies and the panels sit side by side as built. */ 186(function(){ 187 var MQ = window.matchMedia('(max-width:940px)'); 188 /* On a phone the showcase shows chat and preview side by side, at phone 189 scale, so it is not staged there; the other frames still are. */ 190 var PHONE = window.matchMedia('(max-width:700px)'); 191 function wants(r){ return MQ.matches && !(PHONE.matches && r.frame.classList.contains('shc')); } 192 /* Someone who has asked for less motion still gets every panel, just not on 193 a timer: the first one shows and the rest are a scroll away on desktop. */ 194 var CALM = window.matchMedia('(prefers-reduced-motion:reduce)'); 195 var FRAMES = '.shc,.dmo-in.dm-two,.dmo-in.dm-three,.dmo-in.dm-side'; 196 var PANE = {'shc': '.sh-l,.sh-r'}; 197 var rigs = []; 198 199 function panes(f){ 200 var sel = f.classList.contains('shc') ? PANE.shc : '.dw'; 201 return [].filter.call(f.children, function(c){ return c.matches(sel); }); 202 } 203 204 function teardown(r){ 205 if (r.timer) { clearInterval(r.timer); r.timer = null; } 206 r.frame.removeAttribute('data-stages'); 207 r.frame.style.height = ''; 208 r.panes.forEach(function(p){ p.classList.remove('stg-pane','on'); }); 209 } 210 211 /* Size the frame to the tallest panel it holds, measured, not guessed. 212 A single hard coded height was wrong in both directions at once: it clipped 213 the security panel through the middle of its last line, and left a third of 214 the box empty under the shorter one. Panels differ per page and per width, 215 so the only honest number is the one measured here. */ 216 function fit(r, reset){ 217 var f = r.frame, tallest = 0; 218 if (reset) r.h = 0; 219 f.style.height = 'auto'; 220 r.panes.forEach(function(p){ 221 var d = p.style.display, v = p.style.visibility; 222 p.style.visibility = 'hidden'; 223 p.style.display = 'block'; 224 tallest = Math.max(tallest, p.scrollHeight); 225 p.style.display = d; 226 p.style.visibility = v; 227 }); 228 /* Only ever grow. One panel fills its log line by line, so measuring once at 229 setup catches it half empty and the frame would clip it later. Taking the 230 tallest seen means the box settles after the first pass and then holds, 231 instead of breathing in and out with the animation. */ 232 r.h = Math.max(r.h || 0, tallest); 233 if (r.h) f.style.height = r.h + 'px'; 234 } 235 236 function setup(r){ 237 r.frame.setAttribute('data-stages', r.map.length); 238 r.panes.forEach(function(p, i){ 239 p.classList.add('stg-pane'); 240 p.classList.toggle('on', i === r.map[r.i]); 241 }); 242 fit(r); 243 } 244 245 function show(r, reason){ 246 var pane = r.map[r.i]; 247 r.panes.forEach(function(p, i){ p.classList.toggle('on', i === pane); }); 248 fit(r); 249 /* The panels are only half the story: a demo may have its own animation to 250 run when its panel is the one on screen. Announce the change and let the 251 page decide, rather than the cycler knowing what any demo contains. */ 252 r.frame.dispatchEvent(new CustomEvent('stagechange', 253 { detail: { index: r.i, pane: pane, stages: r.map.length, 254 reason: reason || 'advance' } })); 255 } 256 257 function step(r){ 258 r.i = (r.i + 1) % r.map.length; 259 show(r); 260 } 261 262 function sync(){ 263 rigs.forEach(function(r){ 264 if (wants(r)) { if (!r.frame.hasAttribute('data-stages')) setup(r); } 265 else teardown(r); 266 }); 267 } 268 269 [].forEach.call(document.querySelectorAll(FRAMES), function(f){ 270 var p = panes(f); 271 if (p.length < 2) return; 272 /* A demo can want more stages than it has panels: the storefront is shown 273 once as it was and again with the change applied, which is two stages on 274 one panel. data-stage-map says which panel each stage shows. */ 275 var map = (f.getAttribute('data-stage-map') || '').split(',') 276 .map(function(n){ return parseInt(n, 10); }) 277 .filter(function(n){ return n >= 0 && n < p.length; }); 278 if (!map.length) map = p.map(function(_, i){ return i; }); 279 rigs.push({ frame: f, panes: p, map: map, i: 0, timer: null }); 280 }); 281 if (!rigs.length) return; 282 283 /* A stage that advances off screen means arriving at a demo already halfway 284 through it, so only what is in view runs. Leaving also rewinds to the 285 first stage: resuming wherever it happened to stop is what made the 286 sequence read store, then chat, then store again. 287 288 The first stage dwells longer than the rest because the panel it shows is 289 playing its own animation underneath, and cutting away mid sentence is the 290 other half of the same complaint. */ 291 var FIRST = 5040, NEXT = 3640; 292 /* the showcase runs twice as fast as the other frames, at Kristaps's request */ 293 function spd(r){ return r.frame.classList.contains('shc') ? 0.5 : 1; } 294 295 function run(r){ 296 clearTimeout(r.timer); 297 r.timer = setTimeout(function tick(){ 298 step(r); 299 r.timer = setTimeout(tick, NEXT * spd(r)); 300 }, (r.i === 0 ? FIRST : NEXT) * spd(r)); 301 } 302 303 function stop(r, rewind){ 304 clearTimeout(r.timer); 305 r.timer = null; 306 /* Rewinding is not the same as reaching stage 0 by playing through it. 307 Saying so matters: the demo advances its use case when the sequence 308 comes back around, and a rewind that looked identical was advancing it 309 again mid sequence, which canceled the change that was about to be 310 applied. That is the "sometimes nothing happens" case. */ 311 if (rewind && r.i !== 0) { r.i = 0; setup(r); show(r, 'rewind'); } 312 } 313 314 var rt; 315 window.addEventListener('resize', function(){ 316 clearTimeout(rt); 317 rt = setTimeout(function(){
318 rigs.forEach(function(r){ if (MQ.matches && r.frame.hasAttribute('data-stages')) fit(r, true); }); 319 }, 180); 320 }); 321 322 var io = new IntersectionObserver(function(es){ 323 es.forEach(function(e){ 324 var r = rigs.filter(function(x){ return x.frame === e.target; })[0]; 325 if (!r) return; 326 if (e.isIntersecting && wants(r) && !CALM.matches) { if (!r.timer) run(r); } 327 else stop(r, !e.isIntersecting); 328 }); 329 }, { threshold: 0.35 }); 330 331 rigs.forEach(function(r){ io.observe(r.frame); }); 332 333 /* The log fills in after the panel is already up, so watch the panels 334 themselves rather than only measuring at each swap. */ 335 if (window.ResizeObserver) { 336 var ro = new ResizeObserver(function(entries){ 337 entries.forEach(function(e){ 338 var r = rigs.filter(function(x){ return x.panes.indexOf(e.target) > -1; })[0]; 339 if (r && MQ.matches && r.frame.hasAttribute('data-stages')) fit(r); 340 }); 341 }); 342 rigs.forEach(function(r){ r.panes.forEach(function(p){ ro.observe(p); }); }); 343 } 344 sync(); 345 346 /* Belt and braces, the way the page's own animations already do it. The 347 observer is the right trigger, but if it never fires the demo sits frozen 348 on its first panel forever, which looks exactly like a broken component. 349 After a few seconds, start anything that has not started. */ 350 setTimeout(function(){ 351 if (!MQ.matches || CALM.matches) return; 352 rigs.forEach(function(r){ if (wants(r) && !r.timer) run(r); }); 353 }, 2600); 354 if (PHONE.addEventListener) PHONE.addEventListener('change', sync); else PHONE.addListener(sync); 355 (MQ.addEventListener ? MQ.addEventListener.bind(MQ, 'change') : MQ.addListener.bind(MQ))(function(){ 356 sync(); 357 if (!MQ.matches) rigs.forEach(function(r){ stop(r, true); }); 358 }); 359})();</script>
vendor: 1 bytes, line 359
359
360<script> 361/* A figure arrives when it is looked at. Nothing moves until then, and nothing 362 moves at all for anyone who asked for less motion. */ 363(function(){ 364 var plates = [].slice.call(document.querySelectorAll('.plate')); 365 if(!plates.length) return; 366 /* A figure is visible unless the script says otherwise. If anything here 367 fails, or the observer never fires, the page still has its pictures. */ 368 document.documentElement.classList.add('js-reveal'); 369 function show(p, d){ setTimeout(function(){ p.classList.add('in'); }, d || 0); } 370 if(window.matchMedia('(prefers-reduced-motion:reduce)').matches || !window.IntersectionObserver){ 371 plates.forEach(function(p){ show(p); }); 372 return; 373 } 374 var io = new IntersectionObserver(function(es){ 375 es.forEach(function(e){ 376 if(!e.isIntersecting) return; 377 show(e.target, (plates.indexOf(e.target) % 2) * 110); 378 io.unobserve(e.target); 379 }); 380 }, {threshold:.2, rootMargin:'0px 0px -8% 0px'}); 381 plates.forEach(function(p){ io.observe(p); }); 382 setTimeout(function(){ plates.forEac
382h(function(p){ show(p); }); }, 2500); 383})(); 384</script>
vendor: 1 bytes, line 384
384
385<script> 386/* The mark, large and faint, at the right of the footer: the Watermark 387 field from the Ari Hero Fields artifact. One chevron and its green 388 underscore at three percent ink, drifting a few pixels against the 389 pointer and pulsing once on a click. The loop runs only while something 390 moves and the footer is on screen; with reduced motion it draws once. */ 391(function(){ 392 var cv=document.getElementById('footcv'); if(!cv||!cv.getContext) return; 393 var sec=cv.parentNode, cx=cv.getContext('2d'); 394 var FINE=matchMedia('(hover:hover) and (pointer:fine)').matches, CALM=matchMedia('(prefers-reduced-motion:reduce)').matches; 395 var INK=[20,19,15], GREEN=[45,87,47]; 396 var W=0,H=0,dpr=1,px=-1e4,py=-1e4,ox=0,oy=0,tx=0,ty=0,pulse=0,raf=0,last=0,vis=false,rt=0; 397 function rgba(c,a){ if(a<0) a=0; return 'rgba('+c[0]+','+c[1]+','+c[2]+','+a.toFixed(3)+')'; } 398 function mix(a,b,t){ return [Math.round(a[0]+(b[0]-a[0])*t),Math.round(a[1]+(b[1]-a[1])*t),Math.round(a[2]+(b[2]-a[2])*t)]; } 399 /* the mark: a chevron and an underscore, centered on x,y, height u */ 400 function mark(x,y,u,a,g){ 401 var mx=x-u*.575, my=y-u*.455; 402 cx.lineCap='butt'; cx.lineJoin='miter'; cx.lineWidth=u*.17; cx.strokeStyle=rgba(INK,a); 403 cx.beginPath(); cx.moveTo(mx,my); cx.lineTo(mx+u*.46,my+u*.46); cx.lineTo(mx,my+u*.91); cx.stroke(); 404 cx.strokeStyle=rgba(mix(INK,GREEN,g),a*(1+.5*g)); 405 cx.beginPath(); cx.moveTo(mx+u*.64,my+u*.89); cx.lineTo(mx+u*1.15,my+u*.89); cx.stroke(); 406 } 407 function draw(){ 408 cx.setTransform(dpr,0,0,dpr,0,0); cx.clearRect(0,0,W,H); 409 var u=W<760?Math.min(H*.5,W*.52):Math.min(H*.5,W*.16); 410 mark(W-u*.66+ox, H*.5+oy, u, .032+pulse*.03, .8); 411 } 412 function step(t){ 413 var dt=Math.min((t-last)/1000,.05); last=t; 414 if(FINE&&px>-1e3){ tx=-(px-W/2)*.022; ty=-(py-H/2)*.022; } else { tx=ty=0; } 415 ox+=(tx-ox)*Math.min(1,3*dt); oy+=(ty-oy)*Math.min(1,3*dt); 416 if(pulse>0){ pulse-=dt*1.2; if(pulse<0) pulse=0; } 417 draw(); 418 var more=Math.abs(tx-ox)>.05||Math.abs(ty-oy)>.05||pulse>0; 419 if(more&&vis) raf=requestAnimationFrame(step); else raf=0; 420 } 421 function wake(){ if(!CALM&&!raf&&vis&&!document.hidden){ last=performance.now(); raf=requestAnimationFrame(step); } } 422 function layout(){ var r=sec.getBoundingClientRect(); W=r.width; H=r.height; dpr=Math.min(window.devicePixelRatio||1,2); cv.width=Math.round(W*dpr); cv.height=Math.round(H*dpr); draw(); } 423 function at(e){ var r=cv.getBoundingClientRect(); px=e.clientX-r.left; py=e.clientY-r.top; } 424 if(!CALM){ 425 sec.addEventListener('pointermove',function(e){ at(e); wake(); }); 426 sec.addEventListener('pointerleave',function(){ px=py=-1e4; wake(); }); 427 sec.addEventListener('pointerdown',function(){ pulse=1; wake(); }); 428 } 429 if('IntersectionObserver' in window) new IntersectionObserver(function(en){ vis=en[0].isIntersecting; if(vis) wake(); else if(raf){ cancelAnimationFrame(raf); raf=0; } },{rootMargin:'60px'}).observe(sec); else vis=true; 430 document.addEventListener('visibilitychange',function(){ if(document.hidden){ if(raf){ cancelAnimationFrame(raf); raf=0; } } else wake(); }); 431 function later(){ clearTimeout(rt); rt=setTimeout(layout,120); } 432 if('ResizeObserver' in window) new ResizeObserver(later).observe(sec); else addEventListener('resize',later); 433 layout(); 434})(); 435</script>
vendor: 1 bytes, line 435
435
436<script> 437/* ---------- the light behind every closing panel ---------- 438 The same eleven bands of green light the homepage hero carries, drawn 439 from the same seed, behind each dark closing panel on every page. A 440 canvas is put into each panel here, so the hand-written pages get it 441 without an edit. Thirty frames a second, only while the panel is on 442 screen and the tab is visible; with reduced motion, drawn once. */ 443(function(){ 444 var calm=matchMedia('(prefers-reduced-motion:reduce)').matches; 445 var LIT=[157,196,159], GREEN=[45,87,47], RES=.4; 446 function rgba(c,a){ return 'rgba('+c[0]+','+c[1]+','+c[2]+','+Math.max(0,Math.min(1,a)).toFixed(3)+')'; } 447 function rng(seed){ return function(){ seed|=0; seed=seed+0x6D2B79F5|0; var x=Math.imul(seed^seed>>>15,1|seed); x=x+Math.imul(x^x>>>7,61|x)^x; return ((x^x>>>14)>>>0)/4294967296; }; } 448 function light(sec){ 449 var cv=document.createElement('canvas'); cv.className='panel-bg'; cv.setAttribute('aria-hidden','true'); sec.insertBefore(cv,sec.firstChild); 450 var cx=cv.getContext('2d'); if(!cx) return; 451 var W=0,H=0,bands=[],raf=0,last=0,acc=0,t=0,vis=false,rt=0; 452 function layout(){ 453 var r=sec.getBoundingClientRect(); W=r.width; H=r.height; cv.width=Math.round(W*RES); cv.height=Math.round(H*RES); 454 var g=rng(80); bands=[]; for(var i=0;i<11;i++) bands.push({o:g()*W*1.6,w:14+g()*70,a:.06+g()*.2,sp:(6+g()*22)*(g()<.5?1:-1),c:i%3?LIT:GREEN}); 455 draw(); 456 } 457 function draw(){ 458 if(!(W>0&&H>0)) return; cx.setTransform(RES,0,0,RES,0,0); cx.clearRect(0,0,W,H); cx.save(); cx.translate(W/2,H/2); cx.rotate(-.35); 459 bands.forEach(function(b){ var x=((b.o+t*b.sp)%(W*1.6)+W*1.6)%(W*1.6)-W*.8, gr=cx.createLinearGradient(x,0,x+b.w,0); gr.addColorStop(0,rgba(b.c,0)); gr.addColorStop(.5,rgba(b.c,b.a)); gr.addColorStop(1,rgba(b.c,0)); cx.fillStyle=gr; cx.fillRect(x,-H,b.w,H*2); }); 460 cx.restore(); 461 } 462 function step(now){ var dt=Math.min((now-last)/1000,.05); last=now; acc+=dt; if(acc>=1/30){ t+=acc; acc=0; draw(); } raf=vis&&!document.hidden?requestAnimationFrame(step):0; } 463 function wake(){ if(calm) return; if(!raf&&vis&&!document.hidden){ last=performance.now(); raf=requestAnimationFrame(step); } } 464 function sleep(){ if(raf){ cancelAnimationFrame(raf); raf=0; } } 465 if(window.IntersectionObserver){ new IntersectionObserver(function(en){ vis=en[0].isIntersecting; if(vis) wake(); else sleep(); },{rootMargin:'40px'}).observe(sec); } else { vis=true; wake(); } 466 document.addEventListener('visibilitychange',function(){ if(document.hidden) sleep(); else wake(); }); 467 window.addEventListener('resize',function(){ clearTimeout(rt); rt=setTimeout(layout,120); }); 468 layout(); 469 } 470 [].slice.call(document.querySelectorAll('.panel, .fb-r')).forEac
470h(light); 471})(); 472</script>
472 473</footer>
474<script> 475/* over a dark hero the bar is see-through with light text, and turns solid once the page scrolls */ 476(function(){var nav=document.getElementById('nav');if(!nav)return; 477 var nh=nav.offsetHeight, x=Math.round(innerWidth/2), el=document.elementFromPoint(x,nh+4), hero=null; 478 function dark(c){var m=c.match(/[\d.]+/g);if(!m||m.length<3||(m[3]!==undefined&&+m[3]<.5))return null;return (.299*m[0]+.587*m[1]+.114*m[2])<70;} 479 while(el&&el!==document.body){var d=dark(getComputedStyle(el).backgroundColor);if(d===true){hero=el;break;}if(d===false)return;el=el.parentElement;} 480 if(!hero)return; 481 var h0=hero.offsetHeight, cs=getComputedStyle(hero); 482 hero.style.paddingTop=(parseFloat(cs.paddingTop)+nh)+'px'; 483 if(hero.offsetHeight<h0+nh-2) hero.style.minHeight=(h0+nh)+'px'; 484 nav.style.marginBottom=(-nh)+'px'; document.body.classList.add('nav-over'); 485 function upd(){var t=document.getElementById('navtog');document.body.classList.toggle('nav-top',scrollY<8&&!(t&&t.getAttribute('aria-expanded')==='true'));} 486 addEventListener('scroll',upd,{passive:true});upd(); 487 var tg=document.getElementById('navtog');if(tg)tg.addEventListener('click',function(){setTimeout(upd,30)});})(); 488 489/* looped videos play only while they are on screen */ 490(function(){var v=[].slice.call(document.querySelectorAll('video[data-lazyplay]'));if(!v.length)return; 491 if(!('IntersectionObserver' in window)){v.forEach(function(x){x.play&&x.play().catch(function(){})});return;} 492 var io=new IntersectionObserver(function(es){es.forEach(function(e){var x=e.target;if(e.isIntersecting){x.preload='auto';var p=x.play();if(p&&p.catch)p.catch(function(){});}else x.pause();})},{threshold:.35}); 493 v.forEach(function(x){io.observe(x)});})(); 494 495/* sections wake up when they reach the screen: CSS animations inside stay paused until then */ 496(function(){var q=[].slice.call(document.querySelectorAll('.sec,.sc-sec,.hw-sec')); 497 if(!('IntersectionObserver' in window)){q.forEach(function(s){s.classList.add('in-view')});return;} 498 var io=new IntersectionObserver(function(es){es.forEach(function(e){if(e.isIntersecting){e.target.classList.add('in-view');io.unobserve(e.target);}})},{threshold:.15}); 499 q.forEach(function(s){io.observe(s)});})(); 500 501(function(){ 502 var hdr = document.getElementById('nav'), 503 items = [].slice.call(document.querySelectorAll('.navitem')), 504 WIDE = window.matchMedia('(min-width:941px)'), 505 HOVER = window.matchMedia('(hover:hover)'), 506 CALM = window.matchMedia('(prefers-reduced-motion:reduce)'), 507 open = null, openT = null, shutT = null; 508 509 function panel(k){ return document.getElementById('m-' + k); } 510 function card(k){ var p = panel(k); return p && p.querySelector('.dd'); } 511 function button(k){ return document.querySelector('[data-menu="' + k + '"]'); } 512 513 /* where the card sits: under its own item, pulled in at the window edge */ 514 function xFor(k){ 515 var b = button(k), c = card(k); 516 if(!b || !c) return 0; 517 var r = b.getBoundingClientRect(), w = c.offsetWidth || 440; 518 return Math.max(16, Math.min(r.left, window.innerWidth - w - 16)); 519 } 520 function place(k){ var c = card(k); if(c) c.style.marginLeft = xFor(k) + 'px'; } 521 function placeAll(){ items.forEach(function(b){ place(b.dataset.menu); }); } 522 523 function shut(k, quick){ 524 var p = panel(k), b = button(k), c = card(k); 525 if(!p) return; 526 p.dataset.open = 'false'; 527 p.dataset.quick = quick ? 'true' : 'false'; 528 if(b) b.setAttribute('aria-expanded', 'false'); 529 if(c){ c.style.height = ''; } 530 } 531 function shutAll(){ if(open){ shut(open); open = null; } } 532 533 /* Opening from nothing: the card arrives. Moving from one menu to another: 534 the card that is already there travels to the new item and resizes to the 535 new content, which is one movement rather than a close and an open. */ 536 function show(k){ 537 if(open === k) return; 538 var from = open, p = panel(k), b = button(k), c = card(k); 539 if(!p || !c) return; 540 open = k; 541 if(b) b.setAttribute('aria-expanded', 'true'); 542 543 if(from && !CALM.matches && c.animate){ 544 var pc = card(from), 545 px = pc ? parseFloat(pc.style.marginLeft) || 0 : 0, 546 ph = pc ? pc.offsetHeight : 0; 547 shut(from, true); 548 /* Put it where it belongs, then animate the difference. Transitions here
549 depend on a style recalculation landing between two writes in the same 550 block, which is not something to rely on; an animation is explicit. */ 551 var x = xFor(k); 552 c.style.marginLeft = x + 'px'; 553 p.dataset.open = 'true'; 554 p.dataset.quick = 'true'; 555 var h = c.offsetHeight, EASE = 'cubic-bezier(.22,1,.36,1)'; 556 if(px !== x){ 557 c.animate([{transform: 'translateX(' + (px - x) + 'px)'}, 558 {transform: 'translateX(0)'}], {duration: 260, easing: EASE}); 559 } 560 if(ph && Math.abs(ph - h) > 2){ 561 c.animate([{height: ph + 'px'}, {height: h + 'px'}], 562 {duration: 260, easing: EASE}); 563 } 564 clearTimeout(show.t); 565 show.t = setTimeout(function(){ 566 if(open === k){ p.dataset.quick = 'false'; } 567 }, 280); 568 } else { 569 if(from) shut(from, true); 570 place(k); 571 p.dataset.open = 'true'; 572 p.dataset.quick = 'false'; 573 } 574 } 575 576 function later(fn, ms){ return setTimeout(fn, CALM.matches ? 0 : ms); } 577 function cancel(){ clearTimeout(openT); clearTimeout(shutT); } 578 579 items.forEach(function(b){ 580 var k = b.dataset.menu; 581 b.addEventListener('click', function(e){ 582 e.stopPropagation(); cancel(); 583 if(open === k){ shutAll(); } else { show(k); } 584 }); 585 /* Pointer: a short wait before opening from nothing so a pointer crossing 586 the bar does not fire every menu, and none at all when one is already 587 open, which is what makes switching feel immediate. */ 588 b.addEventListener('pointerenter', function(){ 589 if(!WIDE.matches || !HOVER.matches) return; 590 cancel(); 591 if(open){ show(k); } else { openT = later(function(){ show(k); }, 70); } 592 }); 593 b.addEventListener('pointerleave', function(){ 594 if(!WIDE.matches || !HOVER.matches) return; 595 clearTimeout(openT); 596 shutT = later(function(){ if(open === k) shutAll(); }, 180); 597 }); 598 b.addEventListener('focus', function(){ if(WIDE.matches) show(k); }); 599 }); 600 601 /* the card keeps the menu open while the pointer is inside it */ 602 items.forEach(function(b){ 603 var k = b.dataset.menu, p = panel(k); 604 if(!p) return; 605 p.addEventListener('pointerenter', function(){ if(WIDE.matches) cancel(); }); 606 p.addEventListener('pointerleave', function(){ 607 if(!WIDE.matches || !HOVER.matches) return; 608 shutT = later(function(){ if(open === k) shutAll(); }, 160); 609 }); 610 }); 611 612 document.addEventListener('click', function(e){ if(!e.target.closest('.mega')) shutAll(); }); 613 document.addEventListener('keydown', function(e){ 614 if(e.key === 'Escape'){ cancel(); shutAll(); } 615 }); 616 document.addEventListener('focusin', function(e){ 617 if(open && !e.target.closest('.nav')) shutAll(); 618 }); 619 window.addEventListener('resize', function(){ cancel(); shutAll(); placeAll(); }); 620 window.addEventListener('load', placeAll); 621 placeAll(); 622 623 /* ---------------------------------------------------------- the phone menu */ 624 var tog = document.getElementById('navtog'); 625 if(tog){ 626 tog.addEventListener('click', function(e){ 627 e.stopPropagation(); 628 var on = hdr.getAttribute('data-nav') === 'open'; 629 hdr.setAttribute('data-nav', on ? 'shut' : 'open'); 630 tog.setAttribute('aria-expanded', String(!on)); 631 if(on) shutAll(); 632 document.body.style.overflow = on ? '' : 'hidden'; 633 }); 634 document.addEventListener('click', function(e){ 635 if(hdr.getAttribute('data-nav') === 'open' && !e.target.closest('.nav')){ 636 hdr.setAttribute('data-nav', 'shut'); 637 tog.setAttribute('aria-expanded', 'false'); 638 document.body.style.overflow = ''; 639 } 640 }); 641 } 642})(); 643</script>
643 644 645</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.