PageSourceSearch

https://www.withari.ai/pricing

html withari.ai collected 2026-10-05 13:19:28 UTC 46,108 bytes, 645 lines download raw bytes

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&auml;</b><span>Hyv&auml; 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>&check;</b><span>More credits can be purchased any time</span></li><li><b>&check;</b><span>Frontend and backend development</span></li><li><b>&check;</b><span>Extensions, integrations and version upgrades</span></li><li><b>&check;</b><span>Hyv&auml; themes, checkout and migrations</span></li><li><b>&check;</b><span>Content, merchandising and SEO changes</span></li><li><b>&check;</b><span>A preview store for every change</span></li><li><b>&check;</b><span>A QA layer checks every change before you see it</span></li><li><b>&check;</b><span>Unlimited seats for your PM, developers and agency</span></li><li><b>&check;</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>&check;</b><span>Everything in Ari</span></li><li><b>&check;</b><span>16 developer hours a month</span></li><li><b>&check;</b><span>More credits can be purchased any time</span></li><li><b>&check;</b><span>Development, QA and deployment</span></li><li><b>&check;</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>&check;</b><span>Custom volumes and invoicing</span></li><li><b>&check;</b><span>Custom integrations, roles and permissions</span></li><li><b>&check;</b><span>SSO, and 24/7 support with a one hour response</span></li><li><b>&check;</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>&check;</b><span>Our own Magento developers, on your store</span></li><li><b>&check;</b><span>Development, QA and deployment</span></li><li><b>&check;</b><span>Asked for in the same chat you ask Ari</span></li><li><b>&check;</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>&check;</b><span>The same developers</span></li><li><b>&check;</b><span>Asked for in the same chat</span></li><li><b>&check;</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&auml;</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>&copy; 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&auml; 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.