PageSourceSearch

https://outgoingapp.net/hospitality

html outgoingapp.net collected 2026-10-07 16:10:54 UTC 154,464 bytes, 2,011 lines download raw bytes

1<!DOCTYPE html>
2<html lang="en">
3<head>
4<meta charset="UTF-8" />
5<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" />
6<meta name="theme-color" content="#14172B" />
7<meta name="description" content="AI agents for hospitality teams: one concierge for every sponsor, member and guest, on WhatsApp, email and phone." />
8<style>:root{color-scheme:light;box-sizing:border-box;padding-top:env(safe-area-inset-top,0px);padding-bottom:env(safe-area-inset-bottom,0px)}html{scroll-padding-top:env(safe-area-inset-top,0px)}body{margin:0;padding:0;font:14px -apple-system,BlinkMacSystemFont,sans-serif;background:#faf9f5;color:#141413}img{max-width:100%}[hidden]:not([hidden=until-found i]){display:none!important}</style>
9<title>Outgoing Hospitality</title>
10<link rel="preconnect" href="https://fonts.googleapis.com">
11<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
12<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&family=Pixelify+Sans:wght@400;600&display=swap">
13<style>
14/* Layout: a blueprint grid (dotted hairlines, tiny blue markers, numbered tags) that starts as a misty light page and deepens into Outgoing navy below the hero. Tight Geist headlines, mono labels, frosted square panels. */
15:root{
16  color-scheme: light;
17  --mist:#F2F5F9;
18  --mist-2:#DCE6F2;
19  --navy:#14172B;
20  --deep:#0C0F1F;
21  --ink:#14172B;
22  --soft:#565C73;
23  --blue:#0A7AFF;
24  --blue-l:#7DC3F0;
25  --green:#22D39A;
26  --warn:#F08A5D;
27  --line:rgba(20,23,43,.16);
28  --line-d:rgba(238,241,248,.14);
29  --fg-d:#EEF1F8;
30  --soft-d:#9DA6C0;
31  --f-head:"Geist", "Helvetica Neue", Helvetica, Arial, sans-serif;
32  --f-body:"Geist", "Helvetica Neue", Helvetica, Arial, sans-serif;
33  --f-mono:"Geist Mono", ui-monospace, "SFMono-Regular", Menlo, monospace;
34  --f-pixel:"Pixelify Sans", ui-monospace, monospace;
35  --wrap:1240px;
36}
37*{box-sizing:border-box}
38[hidden]{display:none!important}
39html{scroll-behavior:smooth;touch-action:manipulation}
40body{margin:0;background:var(--deep);color:var(--ink);font-family:var(--f-body);font-size:16px;line-height:1.5;-webkit-font-smoothing:antialiased}
41a{color:inherit}
42:focus-visible{outline:2px solid var(--blue);outline-offset:3px}
43@media (prefers-reduced-motion: reduce){html{scroll-behavior:auto}*{animation:none!important;transition:none!important}}
44.sr{position:absolute!important;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
45
46/* ---------- Loader ---------- */
47#loader{position:fixed;inset:0;z-index:100;background:var(--mist);display:grid;place-items:center;transition:transform .8s cubic-bezier(.7,0,.2,1) .12s;animation:ldGone .6s ease 4.5s forwards}
48#loader.out{transform:translateY(-100%)}
49.ld-mark{position:relative;width:240px;height:41.1px;transition:opacity .35s ease,transform .45s cubic-bezier(.7,0,.2,1)}
50#loader.out .ld-mark{opacity:0;transform:translateY(-18px)}
51.ld-dots{position:absolute;inset:0;width:100%;height:100%}
52@keyframes ldGone{to{transform:translateY(-100%);visibility:hidden}}
53
54/* ---------- Shared type ---------- */
55.label{font-family:var(--f-mono);font-size:12px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;margin:0;display:inline-flex;align-items:center;gap:10px}
56.label::before{content:"";width:6px;height:6px;background:var(--blue);flex:none}
57.h1{font-family:var(--f-head);font-weight:500;font-size:clamp(38px,4.3vw,62px);line-height:.98;letter-spacing:-.045em;margin:0;text-wrap:balance}
58.h1 em{font-style:normal;color:var(--blue)}
59.h2{font-family:var(--f-head);font-weight:500;font-size:clamp(32px,3.6vw,52px);line-height:1;letter-spacing:-.04em;margin:0;text-wrap:balance}
60.lede{font-size:clamp(16px,1.3vw,18px);color:var(--soft);margin:0;max-width:40ch;line-height:1.5}
61.tag{align-self:flex-start;font-family:var(--f-mono);font-size:11px;font-weight:500;letter-spacing:.04em;display:inline-flex;align-items:center;gap:5px;padding:3px 6px;background:var(--navy);color:var(--fg-d);line-height:1}
62.tag::before{content:"";width:4px;height:4px;background:currentColor}
63.dark .tag{background:rgba(238,241,248,.12)}
64
65/* ---------- Buttons (segmented, square) ---------- */
66.btn{font-family:var(--f-mono);font-weight:500;font-size:13px;letter-spacing:.06em;text-transform:uppercase;display:inline-flex;align-items:stretch;min-height:44px;border:0;cursor:pointer;text-decoration:none;color:var(--fg-d);background:transparent;padding:0;gap:2px;white-space:nowrap}
67.btn >
67 span{display:flex;align-items:center;padding:0 18px;background:var(--navy);transition:background .15s}
68.btn > i{display:grid;place-items:center;width:44px;background:var(--navy);font-style:normal;transition:background .15s}
69.btn:hover > span,.btn:hover > i{background:#252B4A}
70.btn.go > span,.btn.go > i{background:var(--green);color:var(--navy)}
71.btn.go:hover > span,.btn.go:hover > i{background:#4BE0AE}
72.btn.go:disabled{cursor:progress;opacity:.6}
73.btn.ghost > span{background:rgba(20,23,43,.07);color:var(--ink)}
74.btn.ghost:hover > span{background:rgba(20,23,43,.13)}
75.dark .btn.ghost > span{background:rgba(238,241,248,.08);color:var(--fg-d)}
76.dark .btn.ghost:hover > span{background:rgba(238,241,248,.16)}
77.row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
78.hint{font-family:var(--f-mono);font-size:12px;letter-spacing:.04em;text-transform:uppercase;margin:0;display:flex;gap:6px;flex-wrap:wrap;align-items:center;color:var(--soft-d)}
79.hint kbd{font-family:var(--f-mono);font-size:11px;padding:2px 6px;border:1px solid var(--line-d);color:var(--fg-d)}
80
81/* ---------- Logo lockup ---------- */
82.logo{display:block;height:22px;width:128px;flex:none;fill:currentColor}
83.lockup{display:inline-flex;flex-direction:column;align-items:flex-start;gap:6px;color:var(--navy);text-decoration:none}
84.lockup .sub{font-family:var(--f-mono);font-size:9.5px;font-weight:500;line-height:1;text-transform:uppercase;letter-spacing:.66em;margin-right:-.66em;white-space:nowrap}
85
86/* ---------- Nav (segmented) ---------- */
87.nav{position:sticky;top:env(safe-area-inset-top,0px);z-index:20;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px clamp(16px,3vw,32px);background:rgba(242,245,249,.78);-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px);border-bottom:1px dashed var(--line)}
88.seg{display:flex;gap:2px;align-items:stretch}
89.seg a,.seg button{font-family:var(--f-mono);font-size:12px;font-weight:500;letter-spacing:.06em;text-transform:uppercase;min-height:36px;padding:0 14px;display:flex;align-items:center;border:0;background:rgba(20,23,43,.07);color:var(--ink);text-decoration:none;cursor:pointer}
90.seg a:hover,.seg button:hover{background:rgba(20,23,43,.13)}
91.seg .lang{display:flex;gap:2px}
92.seg .lang button{padding:0 10px}
93.seg .lang button[aria-pressed="true"]{background:var(--navy);color:var(--fg-d)}
94.seg a.cta{background:var(--navy);color:var(--fg-d)}
95.seg a.cta:hover{background:#252B4A}
96.seg a.arrow{background:var(--navy);color:var(--fg-d);padding:0 12px}
97@media (max-width:760px){.seg .opt{display:none}}
98@media (max-width:420px){.seg a.arrow{display:none}.logo{height:19px;width:110px}.lockup .sub{font-size:8.2px}}
99
100/* ---------- Blueprint frame ---------- */
101.frame{max-width:var(--wrap);margin:0 auto;border-inline:1px dashed var(--line);position:relative}
102.dark .frame{border-color:var(--line-d)}
103.mk{position:relative}
104.mk::before{content:"";position:absolute;left:-4px;top:-4px;width:7px;height:7px;background:var(--blue);z-index:2}
105.sec-head{display:flex;flex-direction:column;gap:18px;padding:clamp(48px,6vw,84px) clamp(16px,3vw,32px) clamp(28px,3vw,40px)}
106.dark{color:var(--fg-d)}
107.dark .lede{color:var(--soft-d)}
108
109/* ---------- Hero ---------- */
110.hero{background:radial-gradient(1100px 520px at 70% 0%, #DCE9F8 0%, rgba(220,233,248,0) 70%),linear-gradient(180deg,var(--mist) 0%,#E7EEF7 100%)}
111.hero .frame{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));border-color:var(--line)}
112.hero-copy{grid-column:1 / span 9;display:flex;flex-direction:column;gap:22px;padding:clamp(40px,6vw,88px) clamp(16px,3vw,32px) clamp(40px,5vw,72px);min-width:0}
113.hero-copy .lede{margin-top:2px}
114.hero-photo{grid-column:6 / span 4;border-inline:1px dashed var(--line);padding:clamp(24px,4vw,64px) 0 clamp(24px,4vw,56px);display:flex;align-items:stretch;min-width:0}
115.panel-photo{position:relative;margin:0;width:100%;background:var(--navy);display:flex;flex-direction:column}
116.panel-photo .ph-top{display:flex;justify-content:space-between;align-items:center;padding:12px 14px;font-family:var(--f-mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--fg-d)}
117.panel-photo .ph-top b{font-weight:500;display:flex;align-items:center;gap:8px}
118.panel-photo .ph-top b::before{content:"";width:6px;height:6px;background:var(--green)}
119.panel-photo img{display:block;width:100%;flex:1;min-height:340px;max-width:100%;object-fit:cover;object-position:56% 50%}
120.panel-photo .ph-bot{padding:12px 14px;font-family:var(--f-mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--soft-d);
120display:flex;justify-content:space-between}
121.hero-boxes{grid-column:9 / span 4;display:flex;flex-direction:column;border-left:1px dashed var(--line)}
122.hbox{flex:1;padding:22px clamp(14px,1.6vw,22px);border-bottom:1px dashed var(--line);display:flex;flex-direction:column;gap:12px;justify-content:center;background:rgba(255,255,255,.35);position:relative;overflow:hidden;min-height:140px}
123.hbox:last-child{border-bottom:0}
124.hbox p{margin:0;font-size:17px;line-height:1.3;letter-spacing:-.01em;color:var(--ink);max-width:16ch}
125.hbox svg.line{position:absolute;right:-8px;bottom:-8px;width:96px;height:96px;opacity:.28}
126@media (max-width:1000px){
127  .hero-copy{grid-column:1 / -1}
128  .hero-boxes{grid-column:1 / -1;flex-direction:row;border-left:0;border-top:1px dashed var(--line)}
129  .hero-boxes .hbox{border-bottom:0;border-right:1px dashed var(--line)}
130  .hero-boxes .hbox:last-child{border-right:0}
131}
132@media (max-width:640px){
133  .hero-boxes{flex-direction:column;padding:0 16px 32px;border-top:0}
134  .hero-boxes .hbox{border-right:1px dashed var(--line)}
135  .hbox{min-height:0;border:1px dashed var(--line);border-bottom:0}
136  .hbox:last-child{border-bottom:1px dashed var(--line)}
137  .panel-photo img{min-height:220px}
138}
139
140/* ---------- Game band ---------- */
141.band{background:linear-gradient(180deg,#E7EEF7 0%,#9FB6D6 9%,#24345F 28%,var(--navy) 46%,var(--deep) 100%)}
142.band .sec-head{color:var(--fg-d)}
143.band .frame{border-color:var(--line-d)}
144.band .sec-head .h2{color:var(--navy)}
145.band .sec-head .lede{color:var(--soft)}
146.stage-wrap{padding:0 clamp(16px,3vw,32px) clamp(48px,6vw,88px)}
147.stage{scroll-margin-top:84px;position:relative;height:clamp(440px,66svh,700px);overflow:hidden;background:#1E3A31;user-select:none;-webkit-user-select:none;outline:1px solid var(--line-d)}
148.anchor{position:absolute;top:-80px}
149#cv{position:absolute;inset:0;width:100%;height:100%;display:block;touch-action:none}
150/* results screen: the stage grows to fit the panel instead of clipping it (phones, short laptops) */
151.stage.ended{height:auto;min-height:clamp(440px,66svh,700px)}
152.stage.ended #ovEnd{position:relative;min-height:inherit}
153.stage.ended #ovEnd .panel{max-height:none;overflow:visible}
154.stage.playing #cv{cursor:none}
155.glass{background:rgba(12,15,31,.78);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);border:1px solid var(--line-d);color:var(--fg-d)}
156.hud .chip.score{font-size:13px;padding:9px 16px 9px 14px;gap:10px;align-items:center;color:#fff;transition:background .35s ease,color .35s ease;box-shadow:0 2px 0 rgba(0,0,0,.25)}
157.hud .chip.score b{font-size:28px;line-height:1;color:inherit;font-family:var(--f-head);font-weight:600;letter-spacing:-.02em;min-width:1.2ch}
158.hud .chip.score span{color:inherit;opacity:.85}
159.hud{position:absolute;z-index:4;top:14px;left:0;right:0;display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:8px;padding:0 14px;pointer-events:none}
160.chips{display:flex;flex-wrap:wrap;gap:2px}
161.chip,.meter{font-family:var(--f-mono);font-size:12px;letter-spacing:.04em;text-transform:uppercase;padding:7px 10px;display:flex;gap:7px;align-items:baseline;font-variant-numeric:tabular-nums;background:rgba(12,15,31,.78);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);color:var(--soft-d)}
162.chip b{font-weight:600;font-size:14px;color:var(--fg-d)}
163.chip.og b{color:var(--blue-l)}
164.chip.warn b{color:var(--warn)}
165.meter{align-items:center;gap:10px}
166.meter .phase{min-width:10ch;color:var(--fg-d);font-weight:600}
167.bar{width:clamp(100px,16vw,180px);height:4px;background:rgba(238,241,248,.14);position:relative}
168.bar i{position:absolute;left:0;top:0;bottom:0;width:0;background:var(--green);transition:width .15s linear}
169.help{display:none}
170.swarm-badge{position:absolute;z-index:4;right:14px;bottom:14px;font-family:var(--f-mono);font-size:12px;letter-spacing:.06em;text-transform:uppercase;padding:9px 12px;display:flex;gap:8px;align-items:center;font-variant-numeric:tabular-nums}
171.swarm-badge i{width:6px;height:6px;background:var(--blue-l);display:inline-block}
172.toast{background:rgba(12,15,31,.86);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);border:1px solid var(--line-d);color:var(--fg-d);position:absolute;z-index:6;left:50%;
172top:60px;transform:translateX(-50%);width:min(500px,calc(100% - 28px));padding:12px 16px;pointer-events:none;transition:opacity .25s ease}
173.toast.pop{animation:toastPop .45s cubic-bezier(.2,1.4,.4,1)}
174@keyframes toastPop{0%{transform:translateX(-50%) translateY(-8px) scale(.92);opacity:0}100%{transform:translateX(-50%) translateY(0) scale(1);opacity:1}}
175.toast.green{border-color:rgba(34,211,154,.7);box-shadow:0 0 0 1px rgba(34,211,154,.25),0 10px 30px rgba(34,211,154,.18)}
176.toast.green .t{color:#22D39A}
177.toast.green .t::after{content:"+3";margin-left:auto;font-weight:600;color:#22D39A}
178.toast .t{font-family:var(--f-mono);font-size:12px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;display:flex;align-items:center;gap:8px}
179.toast .t::before{content:"";width:6px;height:6px;background:var(--green);flex:none}
180.toast.og .t::before{background:var(--blue-l)}
181.toast .b{font-size:15px;color:var(--soft-d);display:block;margin-top:4px;line-height:1.4}
182.overlay{position:absolute;inset:0;z-index:7;display:flex;align-items:flex-end;justify-content:flex-start;padding:20px;background:linear-gradient(90deg,rgba(12,15,31,.55),rgba(12,15,31,0) 70%)}
183.overlay.center{align-items:center;justify-content:center;background:rgba(12,15,31,.55)}
184.panel{max-width:520px;max-height:100%;overflow:auto;display:flex;flex-direction:column;gap:16px;padding:clamp(20px,3vw,30px)}
185.stage-title{font-family:var(--f-head);font-weight:500;font-size:clamp(30px,3.4vw,44px);line-height:1;letter-spacing:-.04em;margin:0}
186.panel .lede{color:var(--soft-d);font-size:16px}
187.help-panel{align-items:center;text-align:center;max-width:440px}
188.help-panel .lede{margin-inline:auto}
189.help-panel .lockup{color:var(--fg-d)}
190.lockup.big .logo{height:28px;width:163px}
191.lockup.big .sub{font-size:12px;letter-spacing:.66em;margin-right:-.66em}
192.legend{display:flex;gap:10px;align-items:center;margin:0;font-size:15px;line-height:1.4;color:var(--soft-d)}
193.legend b{color:#22D39A;font-weight:600}
194.legend .cube{width:16px;height:16px;flex:none;background:#22D39A;box-shadow:inset -3px -3px 0 rgba(0,0,0,.28),inset 3px 3px 0 rgba(255,255,255,.3)}
195.help-panel{position:relative;padding:56px clamp(24px,4vw,44px)!important;gap:26px!important;max-width:520px;align-items:center!important;text-align:center!important;justify-content:center}
196.help-panel .help-title{margin-inline:auto;text-align:center}
197.help-logo{position:absolute;top:18px;right:20px;height:14px;width:82px;fill:var(--fg-d);opacity:.85}
198.help-kicker{color:var(--soft-d)}
199.help-title{font-size:clamp(30px,3.6vw,46px)!important;line-height:1.02;text-wrap:balance}
200.help-title-hl{color:var(--green)}
201#helpCall{min-height:56px;font-size:15px}
202#helpCall > span{padding:0 26px;gap:10px}
203#helpCall kbd{font-family:var(--f-mono);font-size:14px;padding:5px 14px;border:2px solid currentColor;margin-left:4px;box-shadow:0 3px 0 currentColor}
204.k-touch{display:none!important}
205@media (hover:none) and (pointer:coarse){.k-touch{display:flex!important}.k-desk{display:none!important}}
206.btn.call kbd{font-family:var(--f-mono);font-size:10px;padding:2px 5px;border:1px solid currentColor;margin-left:10px}
207.drone-ic{width:26px;height:24px;margin-right:10px}
208.stats{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1px;background:var(--line-d);border:1px solid var(--line-d)}
209.stat{background:var(--deep);padding:12px 14px;display:flex;flex-direction:column;gap:4px}
210.stat b{font-family:var(--f-head);font-weight:500;font-size:30px;line-height:1;letter-spacing:-.03em;font-variant-numeric:tabular-nums}
211.stat span{font-family:var(--f-mono);font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--soft-d)}
212.stat.og b{color:var(--blue-l)} .stat.green b{color:var(--green)} .stat.warn b{color:var(--warn)}
213/* end-of-shift panel: compact so it fits the shorter stage without scrolling */
214#ovEnd .panel{max-width:600px;gap:12px;padding:clamp(18px,2.4vw,24px)}
215#ovEnd .stage-title{font-size:clamp(24px,2.6vw,34px)}
216#ovEnd .lede{font-size:15px;line-height:1.45}
217#ovEnd .stat{padding:10px 12px}
218#ovEnd .stat b{font-size:24px}
219@media (min-width:640px){#ovEnd .stats{grid-template-columns:repeat(4,minmax(0,1fr))}}
220@media (max-width:560px){
221  .chip,.meter{font-size:10.5px;padding:5px 7px}
222  .chip b{font-size:12px}
223  .bar{width:70px}
224  .toast{top:96px}
225  .overlay{padding:12px}
226  .swarm-badge{left:14px;justify-content:center}
227}
227
228
229/* ---------- Forms ---------- */
230.form{display:flex;flex-direction:column;gap:12px}
231.form .fields{display:flex;flex-wrap:wrap;gap:2px}
232.form input[type=email]{flex:1 1 220px;min-width:0;min-height:44px;padding:10px 14px;font:inherit;font-size:16px;color:var(--fg-d);background:rgba(238,241,248,.06);border:0;border-bottom:1px solid var(--line-d);border-radius:0}
233.form input[type=email]::placeholder{color:#7D86A3}
234.form input[type=email]:focus{outline:none;border-bottom-color:var(--green);background:rgba(238,241,248,.1)}
235.form .consent{display:flex;gap:10px;align-items:flex-start;font-size:13px;color:var(--soft-d);line-height:1.45}
236.form .consent input{width:16px;height:16px;margin:2px 0 0;accent-color:var(--green);flex:none}
237.form .ok{font-family:var(--f-mono);font-size:13px;letter-spacing:.06em;text-transform:uppercase;color:var(--green);margin:0}
238.form .err{font-size:14px;color:var(--warn);margin:0}
239
240/* ---------- Dark sections ---------- */
241.deep{background:var(--deep)}
242.boxes{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));border-top:1px dashed var(--line-d)}
243.box{padding:24px clamp(16px,2vw,28px) 28px;border-right:1px dashed var(--line-d);display:flex;flex-direction:column;gap:16px;min-height:220px;position:relative;overflow:hidden;background:rgba(238,241,248,.02)}
244.box:last-child{border-right:0}
245.box h3{font-family:var(--f-head);font-weight:500;font-size:22px;line-height:1.15;letter-spacing:-.025em;margin:auto 0 0;max-width:18ch}
246.box svg.line{position:absolute;right:16px;top:18px;width:92px;height:92px;opacity:.5}
247@media (max-width:820px){.boxes{grid-template-columns:minmax(0,1fr)}.box{border-right:0;border-bottom:1px dashed var(--line-d);min-height:170px}}
248
249/* Stack rows (Sharplink pattern) */
250.stack{display:grid;grid-template-columns:minmax(0,7fr) minmax(0,5fr);border-top:1px dashed var(--line-d)}
251.rows{display:flex;flex-direction:column}
252.srow{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:24px;padding:34px clamp(16px,3vw,32px);border-bottom:1px dashed var(--line-d);opacity:.38;transition:opacity .35s}
253.srow.on{opacity:1}
254.srow:last-child{border-bottom:0}
255.srow .l{display:flex;flex-direction:column;gap:14px}
256.srow .l small{font-family:var(--f-mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--soft-d)}
257.srow h3{font-family:var(--f-head);font-weight:500;font-size:clamp(22px,2.2vw,30px);line-height:1.08;letter-spacing:-.03em;margin:0}
258.srow p{margin:0;color:var(--soft-d);font-size:15px;line-height:1.5;padding-top:24px;max-width:34ch}
259.srow .mob{display:none}
260.viz{border-left:1px dashed var(--line-d);position:relative}
261.viz-in{position:sticky;top:96px;height:min(520px,72vh);display:grid;place-items:center;padding:28px}
262.vp{position:absolute;inset:28px;display:grid;place-items:center;opacity:0;transform:translateY(10px);transition:opacity .45s,transform .45s}
263.vp.on{opacity:1;transform:none}
264.slice{width:100%;max-width:380px;background:rgba(238,241,248,.04);border:1px solid var(--line-d);padding:16px;display:flex;flex-direction:column;gap:8px;font-size:14px}
265.slice .sh{display:flex;justify-content:space-between;font-family:var(--f-mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--soft-d);padding-bottom:8px;border-bottom:1px dashed var(--line-d);margin-bottom:4px}
266.slice .it{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:10px 12px;background:rgba(238,241,248,.05)}
267.slice .it b{font-weight:500}
268.slice .it small{font-family:var(--f-mono);font-size:10px;letter-spacing:.06em;text-transform:uppercase;color:var(--soft-d)}
269.st{font-family:var(--f-mono);font-size:10px;letter-spacing:.06em;text-transform:uppercase;padding:3px 6px;white-space:nowrap}
270.st.act{background:rgba(240,138,93,.18);color:#FFB394}.st.wait{background:rgba(238,241,248,.1);color:var(--fg-d)}.st.ai{background:rgba(10,122,255,.22);color:#9CCBFF}.st.done{background:rgba(34,211,154,.18);color:#7FF0C8}
271.bub{max-width:86%;padding:9px 12px;line-height:1.4}
272.bub small{display:block;font-family:var(--f-mono);font-size:10px;letter-spacing:.06em;text-transform:uppercase;opacity:.7;margin-bottom:2px}
273.bub.g{align-self:flex-end;background:rgba(238,241,248,.08)}
274.bub.a{align-self:flex-start;background:rgba(10,122,255,.22)}
275.pf{display:grid;grid-template-columns:minmax(0,9ch) minmax(0,1fr);gap:8px 14px;font-size:14px}
276.pf span:nth-child(odd){font-family:var(--f-mono);font-size:10px;letter-spacing:.06em;text-transform:uppercase;color:var(--soft-d);padding-top:3px}
277.sugg{background:rgba(34,211,154,.12);padding:10px 12px;display:flex;gap:10px;align-items:flex-start}
278.sugg::before{content:"";width:6px;height:6px;background:var(--green);margin-top:7px;flex:none}
279.drone-big{width:min(300px,80%);height:auto}
280@media (max-width:900px){
281  .stack{grid-template-columns:minmax(0,1fr)}
282  .viz{display:none}
283  .srow{grid-template-columns:minmax(0,1fr);opacity:1;gap:12px}
284  .srow p{padding-top:0}
285  .srow .mob{display:block;margin-top:8px}
286  .srow .mob .slice{max-width:none}
287}
288
289.wl{display:grid;grid-template-columns:minmax(0,7fr) minmax(0,5fr);border-top:1px dashed var(--line-d)}
290.wl-l{padding:clamp(32px,4vw,56px) clamp(16px,3vw,32px);display:flex;flex-direction:column;gap:24px;justify-content:space-between;position:relative;overflow:hidden;min-height:320px}
291.wl-l svg{width:min(360px,90%);height:auto;opacity:.95}
292.wl-r{border-left:1px dashed var(--line-d);padding:clamp(32px,4vw,56px) clamp(16px,3vw,32px);display:flex;flex-direction:column;justify-content:center;background:rgba(238,241,248,.03)}
293@media (max-width:820px){.wl{grid-template-columns:minmax(0,1fr)}.wl-r{border-left:0;border-top:1px dashed var(--line-d)}.wl-l{min-height:0}}
294
295footer{border-top:1px dashed var(--line-d)}
296footer .frame{display:flex;flex-wrap:wrap;gap:24px;justify-content:space-between;align-items:flex-end;padding:32px clamp(16px,3vw,32px) 44px;color:var(--soft-d);font-size:13px}
297footer .lockup{color:var(--fg-d)}
298footer .fam{display:flex;flex-direction:column;gap:16px}
299footer .links{display:flex;gap:2px;flex-wrap:wrap}
300footer .links a{font-family:var(--f-mono);font-size:11px;letter-spacing:.06em;text-transform:uppercase;padding:8px 12px;background:rgba(238,241,248,.06);text-decoration:none;color:var(--fg-d)}
301footer .links a:hover{background:rgba(238,241,248,.12)}
302
303/* ---------- Below the game: simple, calm sections ---------- */
304.wrap{max-width:var(--wrap);margin:0 auto;padding-inline:clamp(16px,3vw,32px)}
305.sb{padding-block:clamp(72px,9vw,120px);border-top:1px solid var(--line-d)}
306.sb:first-child{border-top:0}
307.cards{display:grid;gap:16px;margin-top:40px}
308.cards.c3{grid-template-columns:repeat(auto-fit,minmax(min(100%,280px),1fr))}
309.cards.c2{grid-template-columns:repeat(auto-fit,minmax(min(100%,440px),1fr))}
310.card{background:rgba(238,241,248,.035);border:1px solid var(--line-d);display:flex;flex-direction:column;min-width:0}
311.card .art{display:grid;place-items:center;padding:28px 20px;min-height:170px;border-bottom:1px solid var(--line-d)}
312.card .art svg{width:100%;max-width:150px;height:auto}
313.card.big .art{min-height:250px;padding:28px}
314.card.big .art > *{width:100%;max-width:380px}
315.card.big .art svg.drone-big{max-width:300px}
316.card h3{font-family:var(--f-head);font-weight:500;font-size:19px;letter-spacing:-.02em;line-height:1.25;margin:0;padding:18px 22px 22px}
317.split{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,420px),1fr));gap:clamp(32px,5vw,72px);align-items:center}
318.slice.idc{max-width:none}
319.split-copy{display:flex;flex-direction:column;gap:20px}
320#how .slice.idc{padding:22px;gap:14px;background:#FFFFFF;font-size:15px;box-shadow:0 1px 0 rgba(20,23,43,.04)}
321.who-row{display:flex;gap:14px;align-items:center}
322.who-row .av{width:44px;height:44px;flex:none;display:grid;place-items:center;background:var(--blue);color:#fff;font-family:var(--f-mono);font-size:13px;font-weight:600;letter-spacing:.04em}
323.who-row b{display:block;font-family:var(--f-head);font-weight:500;font-size:20px;letter-spacing:-.02em;line-height:1.2}
324.who-row small{display:block;color:var(--soft);font-size:13.5px}
325#how .idc .pf{font-size:15px;gap:12px 16px;grid-template-columns:minmax(0,13ch) minmax(0,1fr)}
326#how .idc .sugg{font-size:15px;padding:12px 14px}
327#how .idc .sugg b{font-weight:600}
328@media (max-width:560px){#how .idc .pf{grid-template-columns:minmax(0,9ch) minmax(0,1fr);gap:12px}#how .slice.idc{padding:18px}}
329.idc .src{margin:0;font-family:var(--f-mono);font-size:10.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--soft)}
330.wl-l{display:flex;flex-direction:column;gap:28px;padding:0;min-height:0}
331.wl-l svg{width:min(340px,90%);height:auto}
332.formcard{padding:clamp(20px,3vw,32px)}
333.fwrap{display:flex;flex-wrap:wrap;gap:24px;justify-content:space-between;align-items:flex-end;padding-block:32px 44px;color:var(--soft-d);font-size:13px}
334footer{border-top:1px solid var(--line-d)}
335
336/* ---------- Simplified light page (no guide lines) ---------- */
337body{background:#F7F9FC}
338.frame{border:0!important}
339.mk::before{display:none}
340.nav{border-bottom:1px solid rgba(20,23,43,.08);background:rgba(247,249,252,.85)}
341.hero{background:#F7F9FC}
342.band{background:#F7F9FC}
343.band .sec-head{color:var(--ink)}
344.band .sec-head .label{color:var(--ink)}
345.stage{outline:0}
346#how,footer{background:#F7F9FC;color:var(--ink)}
347#how .lede{color:var(--soft)}
348.sb{border-top:1px solid rgba(20,23,43,.08)}
349#how .card{background:#FFFFFF;border:1px solid rgba(20,23,43,.1)}
350#how .card .art{border-bottom:1px solid rgba(20,23,43,.08)}
351#how .card .art svg g[stroke]{stroke:#0A7AFF}
352#how svg g[fill="#EEF1F8"]{fill:#9DA6C0}
353#how .slice{background:#F7F9FC;border:1px solid rgba(20,23,43,.1)}
354#how .slice .sh{color:var(--soft);border-bottom-color:rgba(20,23,43,.12)}
355#how .slice .it{background:#FFFFFF}
356#how .slice .it small{color:var(--soft)}
357#how .bub.g{background:#FFFFFF;border:1px solid rgba(20,23,43,.08)}
358#how .bub.a{background:#DCEBFF;color:var(--ink)}
359#how .pf span:nth-child(odd){color:var(--soft)}
360#how .sugg{background:#DDF7EC}
361#how .st.act{background:#FDE3D7;color:#A4451C}
362#how .st.wait{background:rgba(20,23,43,.07);color:var(--ink)}
363#how .st.ai{background:#DCEBFF;color:#0A5FCC}
364#how .st.done{background:#D3F6E8;color:#0B7A55}
365#how .form input[type=email]{background:#FFFFFF;color:var(--ink);border:1px solid rgba(20,23,43,.16)}
366#how .form input[type=email]::placeholder{color:#8A90A6}
367#how .form input[type=email]:focus{border-color:var(--blue);background:#FFFFFF}
368#how .form .consent{color:var(--soft)}
369#how .form .ok{color:#0B7A55}
370#how .formcard{background:#FFFFFF}
371footer{border-top:1px solid rgba(20,23,43,.08)}
372footer .fwrap{color:var(--soft)}
373footer .lockup{color:var(--ink)}
374footer .links a{background:rgba(20,23,43,.06);color:var(--ink)}
375footer .links a:hover{background:rgba(20,23,43,.12)}
376footer.deep.dark{background:#F7F9FC;color:var(--ink)}
377
378/* pitch-style concierge bubbles */
379.pchat{display:flex;flex-direction:column;gap:14px;width:100%;max-width:400px}
380.pb{position:relative;margin:0;padding:12px 15px;border-radius:12px;max-width:80%;font-weight:600;font-size:14px;line-height:1.38;color:#FFFFFF}
381.pb.me{align-self:flex-end;background:#0B8A5C;border-bottom-right-radius:0}
382.pb.me::after{content:"";position:absolute;right:-9px;bottom:0;border-left:10px solid #0B8A5C;border-top:10px solid transparent}
383.pb.ai{align-self:flex-start;background:#2A6FE0;border-bottom-left-radius:0}
384.pb.ai::after{content:"";position:absolute;
384left:-9px;bottom:0;border-right:10px solid #2A6FE0;border-top:10px solid transparent}
385.card .art.photo{padding:0;min-height:0;display:block;overflow:hidden}
386.card .art.photo img{display:block;width:100%;height:auto;max-width:100%;aspect-ratio:16/10;object-fit:cover}
387
388/* ---------- EXPOFUT callout (navy card so the event's white logo reads on the light section) ---------- */
389#how .xp{background:var(--navy);color:var(--fg-d);display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.5fr);gap:clamp(28px,5vw,72px);align-items:center;padding:clamp(28px,4.5vw,64px)}
390#how .xp-logo{display:block;justify-self:center;width:min(300px,100%)}
391#how .xp-logo img{display:block;width:100%;height:auto}
392#how .xp-copy{display:flex;flex-direction:column;gap:18px;min-width:0}
393#how .xp .label{color:var(--blue-l)}
394#how .xp blockquote{margin:0;font-family:var(--f-head);font-weight:500;font-size:clamp(24px,2.7vw,38px);line-height:1.08;letter-spacing:-.035em;text-wrap:balance}
395#how .xp .meta{margin:0;font-family:var(--f-mono);font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--soft-d)}
396@media (max-width:760px){#how .xp{grid-template-columns:1fr}#how .xp-logo{justify-self:start;width:min(220px,70%)}}
397
398/* ---------- Coherent spacing ---------- */
399:root{--sec:clamp(112px,13vw,192px);--gap:56px}
400.hero .frame{min-height:calc(100vh - 67px);min-height:calc(100svh - 67px)}
401.hero-copy{padding:clamp(48px,6vw,96px) clamp(16px,3vw,32px) clamp(64px,8vw,120px);gap:28px;justify-content:center}
402.band{padding-top:var(--sec)}
403.band .sec-head{padding:0 clamp(16px,3vw,32px) var(--gap)}
404.stage-wrap{padding-bottom:0}
405.sb{padding-block:var(--sec) 0;border-top:0}
406#how > .sb:last-child{padding-bottom:var(--sec)}
407.cards{margin-top:var(--gap)}
408footer{margin-top:0}
409
410/* ---------- Hero lettering animation ---------- */
411.hero h1.h1{font-size:clamp(48px,6.6vw,104px);line-height:.96}
412.hero h1 .w{display:inline-block;overflow:hidden;vertical-align:top;padding-bottom:.1em;margin-bottom:-.1em}
413.hero h1 .wi{display:inline-block;transition:transform .95s cubic-bezier(.2,.75,0,1),opacity .95s ease}
414.hero h1.pre .wi{transform:translateY(110%);opacity:0;transition:none}
415.hero h1 em{transition:color .6s ease}
416.hero-copy .lede,.hero-copy .row{transition:transform .8s cubic-bezier(.2,.75,0,1) .55s,opacity .8s ease .55s}
417.hero-copy.pre .lede,.hero-copy.pre .row{transform:translateY(14px);opacity:0;transition:none}
418
419/* ---------- Value cards ---------- */
420#platform .card .txt,#where .card .txt{padding:22px 24px 26px;display:flex;flex-direction:column;gap:8px}
421#platform .card .txt h3,#where .card .txt h3{padding:0;font-size:21px}
422#platform .card .txt p,#where .card .txt p{margin:0;color:var(--soft);font-size:15.5px;line-height:1.5;max-width:46ch}
423#platform .card.big .art{min-height:290px}
424.prog{display:flex;flex-direction:column;gap:6px;margin-top:6px;font-family:var(--f-mono);font-size:10.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--soft)}
425.prog i{display:block;height:4px;background:rgba(20,23,43,.08)}
426.prog i b{display:block;height:100%;background:var(--green)}
427.comms{display:flex;flex-direction:column;gap:14px;width:100%;max-width:380px}
428.comms .pchat{max-width:none}
429.comms .feed{padding:10px}
430.comms .feed .it{flex-direction:row}
431.it.doc b{display:flex;align-items:center;gap:10px}
432.it.doc b::before{content:"";width:12px;height:15px;border:1.5px solid var(--blue);flex:none}
433.pills{display:flex;flex-wrap:wrap;gap:6px}
434.pills span{font-size:13px;padding:4px 9px;background:#FFFFFF;border:1px solid rgba(20,23,43,.1)}
435.chan{display:flex;gap:2px}
436.chan span{font-family:var(--f-mono);font-size:10.5px;letter-spacing:.06em;text-transform:uppercase;padding:6px 10px;background:rgba(20,23,43,.06);color:var(--soft)}
437.chan span.on{background:#0B8A5C;color:#FFFFFF}
438.offer{background:#FFFFFF;border-left:3px solid #0B8A5C;padding:10px 12px;font-size:14px;line-height:1.4;display:flex;flex-direction:column;gap:3px}
439.offer small{font-family:var(--f-mono);font-size:10px;letter-spacing:.06em;text-transform:uppercase;color:var(--soft)}
440#platform .card .art{flex:1}
441
442/* ---------- Bento value cards ---------- */
443#platform .cards.bento{grid-template-columns:repeat(12,minmax(0,1fr))}
444#platform .s7{grid-column:span 7}
445#platform .s5{grid-column:span 5}
446#platform .major .txt{padding:28px 30px 32px;gap:12px}
447#platform .major .txt h3{font-size:clamp(24px,2.2vw,30px);letter-spacing:-.03em}
448#platform .major .txt p{font-size:16.5px;max-width:52ch}
449#platform .major .art{min-height:330px}
450.kick{font-family:var(--f-mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--blue);display:inline-flex;align-items:center;gap:8px}
451.kick::before{content:"";width:6px;height:6px;background:var(--blue)}
452.who{display:flex;flex-wrap:wrap;gap:2px;margin-top:6px}
453.who span{font-family:var(--f-mono);font-size:10.5px;letter-spacing:.06em;text-transform:uppercase;padding:6px 10px;background:rgba(20,23,43,.06);color:var(--ink)}
454#platform .major .comms{display:grid;grid-template-columns:minmax(0,1.1fr) minmax(0,1fr);gap:18px;max-width:none;align-items:center}
455@media (max-width:980px){
456  #platform .s7,#platform .s5{grid-column:1 / -1}
457  #platform .major .comms{grid-template-columns:minmax(0,1fr)}
458}
459.who span.on{background:#0B8A5C;color:#FFFFFF}
460
461/* ---------- Dot illustrations ---------- */
462#platform .card .art.dotart,#where .card .art.dotart{position:relative;padding:0;min-height:280px;display:block;background:#FFFFFF}
463#platform .major .art.dotart{min-height:330px}
464#platform .art > .dots,#where .art > .dots{position:absolute;left:32px;right:32px;top:32px;bottom:32px;width:calc(100% - 64px);height:calc(100% - 64px);max-width:none}
465@media (max-width:560px){#platform .card .art.dotart,#platform .major .art.dotart{min-height:240px}}
466
467#where .card .txt p{color:#3B4157;font-size:16px;line-height:1.5}
468#where .card .txt h3{font-size:22px}
469#where .card .art.pix{padding:0;min-height:0;display:block;overflow:hidden;border-bottom:1px solid rgba(20,23,43,.1)}
470#where canvas.pix{display:block;width:100%;height:auto;aspect-ratio:96/60;image-rendering:pixelated;image-rendering:crisp-edges}
471
472
473/* ---------- Feature cards (compact, expandable) ---------- */
474.fcards{display:flex;flex-direction:column;gap:16px;margin-top:clamp(28px,4vw,48px)}
475.fc{display:grid;grid-template-columns:minmax(0,5fr) minmax(0,6fr);align-items:stretch;background:#FFFFFF;border:1px solid rgba(20,23,43,.1);min-height:220px;overflow:hidden}
476.fc-txt{display:flex;flex-direction:column;justify-content:center;gap:12px;padding:clamp(24px,3vw,44px)}
477.fc-txt h3{margin:0;font-family:var(--f-head);font-weight:500;font-size:clamp(22px,2vw,28px);line-height:1.15;letter-spacing:-.03em;color:var(--ink)}
478.fc-txt p{margin:0;color:var(--soft);font-size:16.5px;line-height:1.5;max-width:42ch}
479.fc-art{position:relative;background:#F4F7FB;border-left:1px solid rgba(20,23,43,.06)}
480.fc-art .dots{position:absolute;left:24px;right:24px;top:22px;bottom:22px;width:calc(100% - 48px);height:calc(100% - 44px);max-width:none}
481.fc-exp{position:absolute;top:14px;right:14px;width:42px;height:42px;border-radius:50%;border:0;background:var(--navy);color:#fff;display:grid;place-items:center;cursor:pointer;box-shadow:0 4px 14px rgba(20,23,43,.18);transition:transform .2s ease}
482.fc-exp svg{width:18px;height:18px}
483.fc-exp:hover{transform:scale(1.07)}
484.fc-exp:focus-visible{outline:2px solid var(--blue);outline-offset:3px}
485@media (max-width:760px){.fc{grid-template-columns:minmax(0,1fr)}.fc-art{order:-1;height:210px;border-left:0;border-bottom:1px solid rgba(20,23,43,.06)}.fc-txt{padding:22px}}
486#platform .card .art.dotart{min-height:290px}
487#platform .major .art.dotart{min-height:290px}
488#platform .card.big .art > .fc-exp{z-index:2;width:42px;height:42px;max-width:none;position:absolute;top:14px;right:14px}
489#platform .card .txt{padding:20px 24px 24px;gap:8px}
490#platform .card .txt h3,#platform .major .txt h3{font-size:clamp(19px,1.6vw,22px);letter-spacing:-.02em;line-height:1.25}
491#platform .card .txt p,#platform .major .txt p{font-size:15.5px;max-width:48ch}
492@media (max-width:560px){#platform .card .art.dotart,#platform .major .art.dotart{min-height:210px}}
493.hero{position:relative}
494.hero-copy .lede{max-width:50ch;font-size:clamp(15.5px,1.15vw,17px);line-height:1.55}
495.hero-form{max-width:440px;margin-top:2px;gap:8px}
496.hero-form .fields{gap:0;flex-wrap:nowrap}
497.hero-form input[type=email]{flex:1 1 auto;min-height:42px;font-size:15px;background:#FFFFFF;color:var(--ink);border:1px solid rgba(20,23,43,.16);border-right:0;padding:10px 14px}
498.hero-form input[type=email]:focus{outline:none;border-color:var(--blue);background:#FFFFFF}
499.hero-form .btn{min-height:42px;flex:none;font-size:12px}
500.hero-form .btn > span{padding:0 14px}
501.hero-form .btn > i{width:38px}
502.hero-form .consent{color:var(--soft);font-size:11.5px}
503.hero-form .consent input{width:14px;
503height:14px}
504.hero-form .ok{color:#0B8A5C}
505.hero-form .err{color:#C2393E}
506@media (max-width:480px){.hero-form .fields{flex-wrap:wrap;gap:8px}.hero-form input[type=email]{border-right:1px solid rgba(20,23,43,.16);flex-basis:100%}.hero-form .btn{width:100%}.hero-form .btn > span{flex:1}}
507.hero-copy .lede,.hero-copy .hero-form{transition:transform .8s cubic-bezier(.2,.75,0,1) .55s,opacity .8s ease .55s}
508.hero-copy.pre .hero-form{transform:translateY(14px);opacity:0;transition:none}
509.scroll-cue{position:absolute;left:50%;bottom:clamp(20px,4vh,40px);transform:translateX(-50%);display:flex;flex-direction:column;align-items:center;gap:10px;text-decoration:none;color:var(--soft);font-family:var(--f-mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;opacity:0;animation:cueIn .8s ease 2.6s forwards}
510@keyframes cueIn{to{opacity:1}}
511.scroll-cue i{position:relative;width:22px;height:34px;border:1.5px solid currentColor;border-radius:12px}
512.scroll-cue i::after{content:"";position:absolute;left:50%;top:7px;width:3px;height:7px;margin-left:-1.5px;border-radius:2px;background:var(--blue);animation:cueDot 1.8s cubic-bezier(.5,0,.5,1) infinite}
513@keyframes cueDot{0%{transform:translateY(0);opacity:0}25%{opacity:1}70%{transform:translateY(11px);opacity:0}100%{opacity:0}}
514@media (max-width:640px){
515  .hero .frame{min-height:0}
516  .hero-copy{padding:84px 20px 40px;gap:30px;justify-content:flex-start}
517  .hero h1.h1{font-size:clamp(42px,12.4vw,56px);line-height:1}
518  .hero-copy .lede{font-size:15.5px;line-height:1.6}
519  .hero-form{margin-top:6px;gap:12px}
520  .hero-form .fields{gap:10px}
521  .scroll-cue{position:static;transform:none;margin:16px auto 0;padding-bottom:44px;width:max-content}
522  .band{padding-top:72px}
523  #playTitle{font-size:clamp(28px,8vw,34px)}
524  .band .sec-head{padding-top:0;padding-bottom:20px}
525}
526.scroll-cue:hover{color:var(--ink)}
527@media (prefers-reduced-motion:reduce){.scroll-cue i::after{animation:none}}
528#platform .card.big .art > .fc-exp{width:30px;height:30px;top:10px;right:10px;background:rgba(255,255,255,.55);color:var(--ink);border:1px solid rgba(20,23,43,.1);box-shadow:0 2px 10px rgba(20,23,43,.08);backdrop-filter:blur(8px) saturate(1.4);-webkit-backdrop-filter:blur(8px) saturate(1.4);opacity:.75;transition:opacity .2s ease,background .2s ease,transform .2s ease}
529#platform .card.big .art > .fc-exp svg{width:13px;height:13px}
530#platform .card:hover .art > .fc-exp,#platform .card.big .art > .fc-exp:focus-visible{opacity:1;background:rgba(255,255,255,.8)}
531.fmodal{position:fixed;inset:0;z-index:50;background:rgba(20,23,43,.55);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);display:grid;place-items:center;padding:16px}
532.fm-box{position:relative;background:#FFFFFF;width:min(980px,100%);max-height:calc(100vh - 32px);overflow:auto;display:grid;grid-template-columns:minmax(0,7fr) minmax(0,5fr)}
533.fm-art{position:relative;background:#F4F7FB;min-height:clamp(300px,42vw,460px)}
534.fm-art .dots{position:absolute;left:28px;right:28px;top:28px;bottom:28px;width:calc(100% - 56px);height:calc(100% - 56px);max-width:none}
535.fm-txt{padding:clamp(28px,3.5vw,48px);display:flex;flex-direction:column;justify-content:center;gap:16px}
536.fm-txt h3{margin:0;font-family:var(--f-head);font-weight:500;font-size:clamp(24px,2.4vw,32px);letter-spacing:-.03em;line-height:1.1;color:var(--ink)}
537.fm-txt p{margin:0;color:var(--soft);font-size:16.5px;line-height:1.55}
538.fm-x{position:absolute;top:12px;right:12px;z-index:2;width:42px;height:42px;border-radius:50%;border:0;background:var(--navy);color:#fff;font-size:24px;line-height:1;cursor:pointer}
539.fm-x:focus-visible{outline:2px solid var(--blue);outline-offset:3px}
540@media (max-width:760px){.fm-box{grid-template-columns:minmax(0,1fr)}.fm-art{min-height:240px}}
541body.fm-open{overflow:hidden}
542
543/* highlighted hero words in dot style */
544.hero h1 em.dotted{position:relative;color:transparent}
545.hero h1 em .emdots{position:absolute;pointer-events:none}
546</style>
547</head>
548<body>
549<svg width="0" height="0" style="position:absolute" aria-hidden="true"><symbol id="og-logo" viewBox="152.7 274.7 774.7 132.8"><path d="M878,332.45H925.9c-.11.65-.25,1.63-.43,2.95s-.32,2.31-.43,3q-5.7,32.28-20.74,50.35t-35.95,18.08q-20.37,0-29.16-17.68t-3.41-48.2q5.43-30.77,20.89-48.46t36.81-17.68c9.64,0,17.58,2.58,23.07,8.47,6,6.47,9,13.67,10.43,24.6a63.93,63.93,0,0,1-.1,14H901.41c1.12-6.39-.34-14.54-2.57-17.71s-5.64-4.76-10.23-4.76q-10.32,0-16.89,10t-10.36,31.52q-3.63,20.55-.42,31.14t13,10.58q8.55,0,14.63-7.19t8.38-20.24H874Z"/><path d="M422.58,277.16,408.32,358q-2.61,14.82-4.59,21A40.06,40.06,0,0,1,399,389.34a38.72,38.72,0,0,1-15.95,13.71q-9.66,4.45-23.59,4.45t-22.09-4.45a22.86,22.86,0,0,1-11.12-13.71,29.74,29.74,0,0,1-1.17-10.41q.23-6.27,2.81-20.9l.66-3.79,13.59-77.08h25.67l-14.62,82.9q-2.77,15.78-.36,20.32t10.62,4.55q8.37,0,12.32-4.63t6.7-20.24l14.62-82.9Z"/><path d="M471.76,311.57l-16.63,94.35H429.28l16.64-94.35ZM428.13,301l4.19-23.81h65.17L493.3,301Z"/><path d="M535.34,332.43h47.89c-.12.64-.26,1.63-.43,2.95s-.32,2.31-.44,2.95q-5.68,32.28-20.74,50.36t-36,18.08q-20.37,0-29.16-17.68t-3.4-48.2q5.42-30.78,20.89-48.46t36.8-17.68c9.65,0,17.59,2.58,23.07,8.47,6,6.47,9,13.67,10.44,24.6a63.86,63.86,0,0,1-.1,14H558.73c1.13-6.38-.33-14.53-2.57-17.7s-5.64-4.77-10.22-4.77q-10.32,0-16.89,10t-10.37,31.53q-3.61,20.55-.42,31.13t13,10.59q8.55,0,14.62-7.19t8.38-20.24H531.33Z"/><path d="M681.56,342.66q-5.19,29.44-20.6,46.78t-36,17.33q-21.33,0-30.17-17.42t-3.36-48.46q5.46-30.94,20.6-48.55t36.21-17.59q21.16,0,30.08,18T681.56,342.66Zm-52.31,39.59q9.87,0,16.59-10.32t10.37-31q3.67-20.89.61-31.22t-13-10.32q-9.87,0-16.53,10.28T617,340.89q-3.69,20.91-.64,31.13T629.25,382.25Z"/><path d="M734.19,276.42l-22.7,128.76H685.3L708,276.42Z"/><path d="M730.84,405.18l22.71-128.76h26s16.18,68.28,16.85,74.51l13.14-74.51h23.81l-22.7,128.76H786.84s-18.06-78.12-18.24-80.12l-14.12,80.12Z"/><path d="M314.28,293.47q-8.91-18-30.07-18l-71.86.07c-.56,0-2.28,0-2.59,0h-.19a19.06,19.06,0,0,0-3.62.25c-12.09,1.16-21.63,6-30.6,16.45q-15.14,17.6-20.6,48.55t3.37,48.46q8,15.83,26.37,17.26l.11,0a20,20,0,0,0,2.73.11c.32,0,.63,0,1,0l.57,0h1.31l70.74.13c13.76,0,25.79-5,36.06-16.6S314.1,363,317.56,343.39Q323.2,311.47,314.28,293.47Zm-32.44,79.2Q275.14,383,265.25,383t-12.92-10.23q-3-10.23.64-31.13t10.36-31.27q6.66-10.28,16.54-10.27t12.95,10.32q3.07,10.3-.61,31.22Q288.56,362.35,281.84,372.67Z"/></symbol><symbol id="og-o" viewBox="161 322 161 147"><path fill-rule="evenodd" d="M309.94,335.19Q298,319,277.2,322.69l-69.94,11.49c-.55.08-2.24.39-2.55.45l-.19,0a18.91,18.91,0,0,0-3.51.87q-17.55,4.86-28.1,22.57-11.85,20-11.86,51.39t11.73,47.14q10.65,14.17,29,12.41h.11a21.39,21.39,0,0,0,2.71-.37c.32-.05.63-.08,1-.14s.37-.09.56-.13l1.29-.23,69.83-11.44q20.32-3.58,32.48-23.33t12.16-49.65Q321.84,351.36,309.94,335.19Zm-18.19,83.62q-4.83,11.32-14.55,13T262.7,424q-4.79-9.54-4.78-30.77t4.78-32.59q4.77-11.27,14.5-13t14.55,7.91q4.81,9.63,4.82,30.85Q296.57,407.49,291.75,418.81Z"/></symbol></svg>
550<div id="loader" aria-hidden="true">
551  <div class="ld-mark">
552    <canvas class="ld-dots"></canvas>
553  </div>
554</div>
555
556<header class="nav">
557  <a class="lockup" href="#top" aria-label="Outgoing Hospitality"><svg class="logo" viewBox="0 0 774.7 132.8" aria-hidden="true"><use href="#og-logo"/></svg><span class="sub" aria-hidden="true">Hospitality</span></a>
558  <nav class="seg" aria-label="Language">
559    <div class="lang" role="group" aria-label="Language"><button type="button" data-lang="en" aria-pressed="true">EN</button><button type="button" data-lang="pt" aria-pressed="false">PT</button></div>
560  </nav>
561</header>
562
563<section class="hero" id="top">
564  <div class="frame">
565    <div class="hero-copy mk">
566      <h1 class="h1" data-i18n="heroH2"><em>AI agents</em> for hospitality teams.</h1>
567      <p class="lede" data-i18n="heroLede">One AI concierge for every sponsor, member and guest. It handles requests on WhatsApp, email and phone, learns each person's taste, and passes your team only what needs a human touch.</p>
568      <form class="form hero-form" id="formHero" novalidate>
569        <div class="sr" aria-hidden="true"><input type="text" name="company" tabindex="-1" autocomplete="off"></div>
570        <label for="emailHero" class="sr" data-i18n="workEmail">Work email</label>
571        <div class="fields">
572          <input type="email" id="emailHero" name="email" placeholder="[email protected]" data-i18n-ph="ph" autocomplete="email" required>
573          <button class="btn go" type="submit"><span data-i18n="join">Request early access</span><i aria-hidden="true">→</i></button>
574        </div>
575        <label class="consent" for="consentHero"><input type="checkbox" id="consentHero" required> <span data-i18n="consent">Email me about Outgoing Hospitality. Unsubscribe any time.</span></label>
576        <p class="err" hidden></p>
577        <p class="ok" hidden data-i18n="okLong">We'll be in touch with more info.</p>
578      </form>
579    </div>
580  </div>
581  <a class="scroll-cue" href="#playTitle" aria-label="Scroll down"><i aria-hidden="true"></i></a>
582</section>
583
584<section class="band" aria-labelledby="playTitle">
585  <div class="frame">
586    <div class="sec-head mk">
587      <h2 class="h2" id="playTitle" data-i18n="bridge2" style="scroll-margin-top:96px">On matchday, everything happens at once. Can you keep up?</h2>
588    </div>
589    <div class="stage-wrap">
590      <div class="stage" id="stage"><span id="play" class="anchor"></span>
591        <canvas id="cv" aria-label="Hospitality balancing game"></canvas>
592
593        <div class="hud" id="hud" hidden>
594          <div class="chips">
595            <span class="chip score" id="hScore"><b id="hYou">0</b> <span data-i18n="byYou">points</span></span>
596          </div>
597        </div>
598
599        <button class="help" id="helpBtn" type="button" hidden>Call</button>
600        <div class="swarm-badge glass" id="swarmBadge" hidden><i></i><span data-i18n="swarmOn">Swarm active</span> · <span id="swarmT">18</span>s</div>
601
602        <div class="toast glass" id="toast" hidden><span class="t" id="toastT"></span><span class="b" id="toastB"></span></div>
603
604        <div class="overlay" id="ovStart">
605          <div class="panel glass dark">
606            <h2 class="stage-title" data-i18n="take">Now it's your turn to run hospitality.</h2>
607            <p class="lede" data-i18n="takeLede">Catch the requests before they hit the floor.</p>
608            <p class="hint" data-i18n="controls">Mouse, <kbd>←</kbd><kbd>→</kbd> or drag</p>
609            <p class="legend"><span class="cube" aria-hidden="true"></span><span data-i18n="legendG"><b>Green</b> requests are worth more.</span></p>
610            <div class="row">
611              <button class="btn go" id="playBtn" type="button"><span data-i18n="start">Start shift</span><i aria-hidden="true">→</i></button>
612              <a class="btn ghost" href="#platform"><span data-i18n="skip">Skip</span></a>
613            </div>
614          </div>
615        </div>
616
617        <div class="overlay center" id="ovHelp" hidden>
618          <div class="panel glass dark help-panel" role="dialog" aria-modal="true" aria-labelledby="helpTitle">
619            <svg class="logo help-logo" viewBox="0 0 774.7 132.8" role="img" aria-label="Outgoing"><use href="#og-logo"/></svg>
620            <h2 class="stage-title help-title" id="helpTitle" data-i18n="helpL">Let's try it with Outgoing's AI agents.</h2>
621            <button class="btn go call" id="helpCall" type="button"><span class="k-desk" data-i18n="pressSpace">Press <kbd>Space</kbd></span><span class="k-touch" data-i18n="tapGo">
621Tap here</span></button>
622          </div>
623        </div>
624
625        <div class="overlay center" id="ovEnd" hidden>
626          <div class="panel glass dark">
627            <p class="label" id="endEyebrow" hidden>Shift over</p>
628            <h2 class="stage-title" id="endTitle">Your team should only carry what matters.</h2>
629            <p class="lede" id="endText"></p>
630            <div class="stats">
631              <div class="stat"><b id="sYou">0</b><span data-i18n="sYou">your points</span></div>
632              <div class="stat og"><b id="sOg">0</b><span data-i18n="sOg">handled by Outgoing</span></div>
633              <div class="stat green"><b id="sMom">0</b><span data-i18n="sMom">moments that mattered</span></div>
634              <div class="stat warn"><b id="sDrop">0</b><span data-i18n="sDrop">guests who asked twice</span></div>
635            </div>
636            <form class="form" id="formEnd" novalidate>
637        <div class="sr" aria-hidden="true"><input type="text" name="company" tabindex="-1" autocomplete="off"></div>
638              <label for="emailEnd" class="sr">Email</label>
639              <div class="fields">
640                <input type="email" id="emailEnd" name="email" placeholder="[email protected]" data-i18n-ph="ph" autocomplete="email" required>
641                <button class="btn go" type="submit"><span data-i18n="joinShort">Request access</span><i aria-hidden="true">→</i></button>
642              </div>
643              <label class="consent" for="consentEnd"><input type="checkbox" id="consentEnd" required> <span data-i18n="consent">Email me about Outgoing Hospitality. Unsubscribe any time.</span></label>
644              <p class="err" hidden></p>
645              <p class="ok" hidden data-i18n="okShort">Request received.</p>
646            </form>
647            <div class="row">
648              <button class="btn ghost" id="againBtn" type="button"><span>Play again</span></button>
649              <a class="btn ghost" href="#platform"><span data-i18n="seeProduct">See the product</span></a>
650            </div>
651          </div>
652        </div>
653      </div>
654    </div>
655  </div>
656</section>
657
658<main id="how" class="deep dark">
659
660  <section class="sb" id="platform">
661    <div class="wrap">
662      <h2 class="h2" data-i18n="h2plat">While it all happens, Outgoing handles the rest.</h2>
663      <div class="cards bento">
664        <article class="card big s7 major">
665          <div class="art dotart"><canvas class="dots" data-scene="comms" aria-hidden="true"></canvas><button class="fc-exp" type="button" data-scene="comms" aria-label="Expand"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 4h6v6M20 4l-7 7M10 20H4v-6M4 20l7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square"/></svg></button></div>
666          <div class="txt"><h3 data-i18n="f1h">One agent for every conversation.</h3><p data-i18n="f1p">Guests, sponsor managers, catering and suppliers write to one place. Outgoing answers what it can and passes the rest to your team.</p></div>
667          <div class="fc-full" hidden><h3 data-i18n="k2h">Communication manager</h3><p data-i18n="k2pL">One AI agent handles every conversation between guests, sponsor managers and suppliers like catering. It answers, books and routes requests, flags what needs a person, and notes each guest's tastes along the way. Your team follows one live feed instead of ten inboxes.</p><div class="who"><span data-i18n="w1">Guests</span><span data-i18n="w2">Sponsor managers</span><span data-i18n="w3">Catering</span><span data-i18n="w4">Suppliers</span></div></div>
668        </article>
669        <article class="card big s5">
670          <div class="art dotart"><canvas class="dots" data-scene="hub" aria-hidden="true"></canvas><button class="fc-exp" type="button" data-scene="hub" aria-label="Expand"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 4h6v6M20 4l-7 7M10 20H4v-6M4 20l7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square"/></svg></button></div>
671          <div class="txt"><h3 data-i18n="f2h">Sponsors hand out their own tickets.</h3><p data-i18n="f2p">Each sponsor gets a hub to invite guests and colleagues. You stop chasing guest lists.</p></div>
672          <div class="fc-full" hidden><h3 data-i18n="k1h">Sponsor hub</h3><p data-i18n="k1p">Sponsors get their own space to hand out tickets to their guests and colleagues. You stop chasing guest lists.</p></div>
673        </article>
674        <article class="card big s5">
675          <div class="art dotart"><canvas class="dots" data-scene="kb" aria-hidden="true"></canvas><button class="fc-exp" type="button" data-scene="kb" aria-label="Expand"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 4h6v6M20 4l-7 7M10 20H4v-6M4 20l7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square"/></svg></button></div>
676          <div class="txt"><h3 data-i18n="f3h">Know-how that stays when people leave.</h3><p data-i18n="f3p">Documents, contacts and lessons from every event live in one knowledge base.</p></div>
677          <div class="fc-full" hidden><h3 data-i18n="k3h">Knowledge base</h3><p data-i18n="k3p">
677All your hospitality documents and know-how in one place. When someone new joins the team, nothing is lost.</p></div>
678        </article>
679        <article class="card big s7 major">
680          <div class="art dotart"><canvas class="dots" data-scene="fu" aria-hidden="true"></canvas><button class="fc-exp" type="button" data-scene="fu" aria-label="Expand"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 4h6v6M20 4l-7 7M10 20H4v-6M4 20l7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square"/></svg></button></div>
681          <div class="txt"><h3 data-i18n="f4h">Follow-ups that feel personal.</h3><p data-i18n="f4p">Every event teaches us each guest's taste. Offers go out by email, WhatsApp or phone, whichever they prefer.</p></div>
682          <div class="fc-full" hidden><h3 data-i18n="k4h">Personal follow-ups</h3><p data-i18n="k4pL">Every event teaches us more about each guest: what they eat, where they like to sit, which matches they never miss. Sponsors and your most passionate fans then get exclusive offers based on their personal taste, on the channel they prefer.</p><div class="who"><span>Email</span><span class="on">WhatsApp</span><span data-i18n="phone">Phone</span></div></div>
683        </article>
684      </div>
685    </div>
686  </section>
687
688  <section class="sb">
689    <div class="wrap split">
690      <div class="split-copy">
691        <h2 class="h2" data-i18n="h2c">Know every guest before they arrive.</h2>
692        <p class="lede" data-i18n="h2cL">Every preference, visit and request is saved to the guest's profile, so your team walks in prepared.</p>
693      </div>
694      <div class="slice idc">
695        <div class="sh"><span data-i18n="profT">Guest profile</span><span data-i18n="profN">6 events</span></div>
696        <div class="who-row"><span class="av" aria-hidden="true">ID</span><div><b>Inês Duarte</b><small data-i18n="idsub">Sponsor guest · Banco Atlântico</small></div></div>
697        <div class="pf">
698          <span data-i18n="pref">Prefers</span><span data-i18n="pfv">Gluten-free · Quiet table · Arrives by car</span>
699          <span data-i18n="last">Last visit</span><span data-i18n="lastv">Needed a stroller at gate B, sorted in 4 min</span>
700          <span data-i18n="next">This matchday</span><span data-i18n="bday">It's her birthday</span>
701        </div>
702        <div class="sugg"><span data-i18n="sugg"><b>For your team:</b> a signed shirt at her table at half-time.</span></div>
703        <p class="src" data-i18n="src">Learned by the concierge from 14 conversations</p>
704      </div>
705    </div>
706  </section>
707
708  <section class="sb" id="where">
709    <div class="wrap">
710      <h2 class="h2" data-i18n="h2d">For all teams.</h2>
711      <div class="cards c3">
712        <article class="card"><div class="art pix"><canvas class="pix" data-pix="venue" width="96" height="60" role="img" aria-label="A covered terrace box over a grass tennis court, with champagne, strawberries and a host with a silver tray"></canvas></div><div class="txt"><h3 data-i18n="c8">Venues</h3><p data-i18n="c8p">Stadiums, arenas and courts that host guests at every event.</p></div></article>
713        <article class="card"><div class="art pix"><canvas class="pix" data-pix="team" width="96" height="60" role="img" aria-label="A football club lounge on matchday, with sponsor guests at a long table and the manager greeting a guest at the door"></canvas></div><div class="txt"><h3 data-i18n="c9">Teams</h3><p data-i18n="c9p">Clubs that look after sponsors and members every matchday.</p></div></article>
714        <article class="card"><div class="art pix"><canvas class="pix" data-pix="series" width="96" height="60" role="img" aria-label="A paddock hospitality suite above the pit lane, with cocktails at the bar and a race car nose below"></canvas></div><div class="txt"><h3 data-i18n="c10">Series</h3><p data-i18n="c10p">Championships that travel, with a new guest list at every stop.</p></div></article>
715      </div>
716    </div>
717  </section>
718
719  <section class="sb" id="expofut">
720    <div class="wrap">
721      <div class="xp mk">
722        <a class="xp-logo" href="https://expofut.pt/noticias/a-expofut-d-nos-aquilo-que-nenhum-outro-palco-em-portugal-oferece" target="_blank" rel="noopener"><img src="/assets/expofut-logo-white.png" alt="EXPOFUT" width="600" height="178" loading="lazy" decoding="async"></a>
723        <div class="xp-copy">
724          <p class="label" data-i18n="xpLabel">See us at EXPOFUT 2026</p>
725          <blockquote data-i18n="xpQuote">“EXPOFUT gives us what no other stage in Portugal offers.”</blockquote>
726          <p class="meta" data-i18n="xpMeta">Arena Liga Portugal · 14–15 October</p>
727          <div class="row"><a class="btn go" href="https://expofut.pt/noticias/a-expofut-d-nos-aquilo-que-nenhum-outro-palco-em-portugal-oferece" target="_blank" rel="noopener"><span data-i18n="xpRead">Read the article</span><i aria-hidden="true">↗</i></a></div>
728        </div>
729      </div>
730    </div>
731  </section>
732
733  <section class="sb" id="join">
734    <div class="wrap split">
735      <div class="wl-l">
736        <h2 class="h2" data-i18n="h2e">Be one of the first clubs to try it.</h2>
737        <svg viewBox="0 0 360 150" aria-hidden="true">
738          <g stroke="#7DC3F0" stroke-width="1.6" fill="none" stroke-linecap="round"><path d="M70 46 L44 28 M124 42 L150 24 M226 70 L206 54 M276 66 L298 50"/></g>
739          <g fill="#EEF1F8" opacity=".5"><ellipse cx="40" cy="26" rx="26" ry="3"/><ellipse cx="154" cy="22" rx="26" ry="3"/>
739<ellipse cx="202" cy="52" rx="20" ry="2.5"/><ellipse cx="302" cy="48" rx="20" ry="2.5"/></g>
740          <ellipse cx="118.6" cy="73" rx="11" ry="23" fill="#0C0F1F"/><ellipse cx="120.5" cy="71" rx="5" ry="10" fill="#22D39A"/>
741          <ellipse cx="271" cy="90" rx="9" ry="18" fill="#0C0F1F"/><ellipse cx="272.5" cy="88.5" rx="4" ry="8" fill="#22D39A"/>
742          <use href="#og-o" x="56" y="34" width="92" height="84" fill="#0A7AFF"/>
743          <use href="#og-o" x="220" y="60" width="70" height="64" fill="#0A7AFF"/>
744        </svg>
745      </div>
746      <div class="card formcard">
747        <form class="form" id="formJoin" novalidate>
748        <div class="sr" aria-hidden="true"><input type="text" name="company" tabindex="-1" autocomplete="off"></div>
749          <label for="emailJoin" class="sr" data-i18n="workEmail">Work email</label>
750          <div class="fields">
751            <input type="email" id="emailJoin" name="email" placeholder="[email protected]" data-i18n-ph="ph" autocomplete="email" required>
752            <button class="btn go" type="submit"><span data-i18n="join">Request early access</span><i aria-hidden="true">→</i></button>
753          </div>
754          <label class="consent" for="consentJoin"><input type="checkbox" id="consentJoin" required> <span data-i18n="consent">Email me about Outgoing Hospitality. Unsubscribe any time.</span></label>
755          <p class="err" hidden></p>
756          <p class="ok" hidden data-i18n="okLong">We'll be in touch with more info.</p>
757        </form>
758      </div>
759    </div>
760  </section>
761</main>
762
763<footer class="deep dark">
764  <div class="wrap fwrap">
765    <div class="fam">
766      <span class="lockup" role="img" aria-label="Outgoing Hospitality"><svg class="logo" viewBox="0 0 774.7 132.8" aria-hidden="true"><use href="#og-logo"/></svg><span class="sub" aria-hidden="true">Hospitality</span></span>
767      <div class="links"><a href="/" data-i18n="fHome">Home</a><a href="/promoters" data-i18n="fPromoters">For organizers</a><a href="/careers" data-i18n="fCareers">Careers</a></div>
768    </div>
769    <p style="margin:0;max-width:64ch" data-i18n="legal">outGoing is a brand owned and operated by Kangaroo Theory, Lda. · VAT PT516657798 · Rua do Nogueiral, N.º 1 A, 2350-413 Torres Novas, Portugal</p>
770  </div>
771</footer>
772
773<div class="fmodal" id="fmodal" hidden>
774  <div class="fm-box" role="dialog" aria-modal="true" aria-labelledby="fmTitle">
775    <button class="fm-x" id="fmX" type="button" aria-label="Close">×</button>
776    <div class="fm-art"><canvas class="dots fm-cv" data-scene="comms" aria-hidden="true"></canvas></div>
777    <div class="fm-txt" id="fmTxt"></div>
778  </div>
779</div>
780<script>
781(() => {
782/* ---------- Always open on the hero ----------
783   Reload would otherwise restore the last scroll position (usually the game), and in-page links
784   would leave a hash like #playTitle that reload jumps back to. Deep links shared on purpose
785   (/hospitality#join) still land on their section. */
786if ('scrollRestoration' in history) history.scrollRestoration = 'manual';
787document.addEventListener('click', e => {
788  const a = e.target.closest('a[href^="#"]'); if (!a) return;
789  const t = document.getElementById(a.getAttribute('href').slice(1)); if (!t) return;
790  e.preventDefault(); t.scrollIntoView({ block: 'start' });
791});
792
793const $ = id => document.getElementById(id);
794const stage = $('stage'), cv = $('cv'); let ctx = cv.getContext('2d');
795const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
796const rand = (a, b) => a + Math.random() * (b - a);
797const pick = a => a[Math.floor(Math.random() * a.length)];
798
799let W = 0, H = 0, DPR = 1, P = 5, groundY = 0, halfW = 80, S_ = 30, bg = null;
800const sn = v => Math.round(v / P) * P;
801function R(x, y, w, h, c) { ctx.fillStyle = c; ctx.fillRect(sn(x), sn(y), Math.max(P, sn(w)), Math.max(P, sn(h))); }
802
803/* ---------- Content ---------- */
804const CATS = {
805  sponsor:  { base: '#6E7BFF', glyph: ['####', '#..#', '####'] },
806  tickets:  { base: '#2F7BFF', glyph: ['####', '##.#', '####'] },
807  guest:    { base: '#38AEEA', glyph: ['###', '###', '.#.'] },
808  travel:   { base: '#8D97C8', glyph: ['.##.', '####', '#..#'] },
809  after:    { base: '#4E8BF0', glyph: ['.#.', '###', '.#.'] },
810  human:    { base: '#22D39A', glyph: ['#.#', '###', '###'] }
811};
812const ROUTINE = [
813  // Sponsors
814  ['sponsor', 'Sponsor sent names on WhatsApp', 'Nomes enviados por WhatsApp'],
815  ['sponsor', '3 extra guests, 2 hours to go', 'Mais 3 convidados, faltam 2 horas'],
816  ['sponsor', 'Sponsor wants proof of value', 'Patrocinador pede relatório'],
817  ['sponsor', 'Which guest list is the latest?', 'Qual é a lista atual?'],
818  ['sponsor', 'Unused seats in Box 12', 'Lugares vazios no camarote 12'],
819  ['sponsor', 'Logo missing on the menu', 'Logótipo em falta no menu'],
820  ['sponsor', 'Sponsor wants a pitch photo', 'Patrocinador quer foto no relvado'],
821  ['sponsor', 'Swap 2 names on the list', 'Trocar 2 nomes na lista'],
822  ['sponsor', 'Sponsor asks for 10 more seats', 'Patrocinador pede mais 10 lugares'],
823  ['sponsor', 'Branding for the box', 'Branding para o camarote'],
824
825  // Guests
826  ['guest', 'Nut allergy, table 7', 'Alergia a frutos secos, mesa 7'],
827  ['guest', 'Vegan menu for 4', 'Menu vegano para 4'],
828  ['guest', 'Where do I park?', 'Onde estaciono?'],
829  ['guest', 'Can I bring my son?', 'Posso levar o meu filho?'],
830  ['guest', 'Is jeans OK?', 'Posso ir de calças de ganga?'],
831  ['guest', 'Wheelchair access', 'Acesso em cadeira de rodas'],
832  ['guest', 'Guest supports the away team', 'Convidado é adepto visitante'],
833  ['guest', 'Which gate do I use?', 'Qual é a minha porta?'],
834  ['guest', 'Can I meet the players?', 'Posso conhecer os jogadores?'],
835  ['guest', 'Lost my coat in the lounge', 'Perdi o casaco no lounge'],
836  ['guest', 'Halal option?', 'Há opção halal?'],
837  ['guest', 'Pregnant guest needs a seat', 'Convidada grávida precisa de lugar'],
838
839  // Tickets
840  ['tickets', 'Ticket not arrived', 'Bilhete não chegou'],
841  ['tickets', 'Transfer ticket to a colleague', 'Passar bilhete a um colega'],
842  ['tickets', 'QR code not scanning', 'Código QR não lê'],
843  ['tickets', 'Name missing at the door', 'Nome em falta na entrada'],
844  ['tickets', 'Wrong name on the ticket', 'Nome errado no bilhete'],
845
846  // Day of the event
847  ['tickets', 'Catering needs final numbers', 'Catering pede números finais'],
848  ['tickets', 'Parking passes not sent', 'Passes de estacionamento por enviar'],
849  ['tickets', 'Table plan changed again', 'Plano de mesas mudou outra vez'],
850  ['tickets', 'Wine for the box ran out', 'Acabou o vinho no camarote'],
851  ['travel', 'Running late, kick-off soon', 'Atrasado, o jogo vai começar'],
852  ['travel', 'Taxi after the match', 'Táxi no fim do jogo'],
853  ['travel', 'Airport pickup at 15:00', 'Recolha no aeroporto às 15:00'],
854  ['travel', 'Hotel for the away directors', 'Hotel para a direção visitante'],
855
856  // After the event
857  ['after', 'Post-match report for sponsor', 'Relatório pós-jogo'],
858  ['after', 'Thank-you emails', 'Emails de agradecimento'],
859  ['after', 'Renewal pack for next season', 'Proposta de renovação'],
860  ['after', 'Send match photos to guests', 'Enviar fotos aos convidados'],
861  ['after', 'Invite for the next home game', 'Convite para o próximo jogo']
862];
863const HUMAN = [
864  { label: "Title sponsor's CEO arrives", labelPt: 'Chega o CEO do patrocinador principal',
865    story: 'You greeted the CEO at the door. Outgoing had already flagged the arrival and their usual table.',
866    storyPt: 'Recebeu o CEO à porta. A Outgoing já tinha assinalado a chegada e a mesa habitual.' },
867  { label: "Guest's birthday on matchday", labelPt: 'Convidado faz anos no dia de jogo',
868    story: 'You brought a signed shirt to Inês at half-time. Outgoing spotted the date in her profile.',
869    storyPt: 'Levou uma camisola autografada à Inês ao intervalo. A Outgoing viu a data no perfil.' },
870  { label: 'Upset guest, wrong seat', labelPt: 'Convidado aborrecido, lugar errado',
871    story: 'You fixed the seat in person and apologised. The whole conversation was already in front of you.',
872    storyPt: 'Resolveu o lugar em pessoa e pediu desculpa. Tinha a conversa toda à sua frente.' },
873  { label: 'Sponsor about to renew', labelPt: 'Patrocinador prestes a renovar',
874    story: 'You sat down with the sponsor. Every match, guest and request this season was in one report.',
875    storyPt: 'Sentou-se com o patrocinador. Cada jogo, convidado e pedido da época estava num só relatório.' },
876  { label: 'Thank a 10-season member', labelPt: 'Agradecer a um sócio de 10 épocas',
877    story: 'You thanked a member on their tenth season. Their profile flagged the milestone.',
878    storyPt: 'Agradeceu a um sócio na sua décima época. O perfil assinalou o marco.' },
879  { label: 'Prospect sponsor visiting', labelPt: 'Visita de um potencial patrocinador',
880    story: 'You gave a prospect sponsor the full matchday welcome. Outgoing had briefed you on who they were.',
881    storyPt: 'Recebeu um potencial patrocinador em pleno dia de jogo. A Outgoing já lhe tinha dito quem era.' },
882  { label: 'Former player in the lounge', labelPt: 'Antigo jogador no lounge',
883    story: 'You introduced a club legend to your top guests. The moment people will talk about all week.',
884    storyPt: 'Apresentou uma lenda do clube aos seus melhores convidados. Um momento de que vão falar toda a semana.' },
885  { label: 'First visit for a new partner', labelPt: 'Primeira visita de um novo parceiro',
886    story: "You walked a new partner through the stadium. Their team's preferences were already set up.",
887    storyPt: 'Mostrou o estádio a um novo parceiro. As preferências da equipa dele já estavam tratadas.' },
888  { label: 'Guest proposing at half-time', labelPt: 'Pedido de casamento ao intervalo',
889    story: 'You helped a guest propose on the big screen. Outgoing handled the logistics; you handled the nerves.',
890    storyPt: 'Ajudou um convidado a pedir em casamento no ecrã gigante. A Outgoing tratou da logística; o senhor tratou dos nervos.' },
891  { label: 'Complaint from last match', labelPt: 'Reclamação do último jogo',
892    story: 'You apologised in person for last week. Outgoing had kept the history and a gesture ready.',
893    storyPt: 'Pediu desculpa em pessoa pelo último jogo. A Outgoing tinha o histórico e um gesto preparado.' }
894];
895function shade(hex, f) {
896  const n = parseInt(hex.slice(1), 16); let r = n >> 16, g = (n >> 8) & 255, b = n & 255;
897  if (f > 0) { r += (255 - r) * f; g += (255 - g) * f; b += (255 - b) * f; } else { r *= 1 + f; g *= 1 + f; b *= 1 + f; }
898  return 'rgb(' + (r | 0) + ',' + (g | 0) + ',' + (b | 0) + ')';
899}
900for (const k in CATS) { const c = CATS[k]; c.hi = shade(c.base, .35); c.lo = shade(c.base, -.38); }
901
902/* ---------- Sprites ---------- */
903const MAN = [
904  '......hhhhh.....', '.....hhhhhhh....', '....hhhhhhhhh...', '....hhssssssh...', '....hssssssss...',
905  '....ssesssess...', '....sssssssssr..', '....ssssmmsss.r.', '.....ssssssss.r.', '......ssss....r.',
906  '....kkwwttwwkkr.', '...kkkklwtwlkkk.', '..kkkkklwtlkkkkk', '..kkqkkkltlkkgkk', '..kkkkkkktkkkkkk',
907  '..kkkkkkkkkkkkkk', '..kkkkkkKkkkkkkk', '..kkkkkkkkkkkkkk', '.nnkkkkkKkkkkkk.', '.nnnkkkkkkkkkkk.',
908  '.nnnskkkkkkkkkk.', '.nnn.kkkkkkkkk..', '.nn..pppppppp...', '.....pppppppp...', '.....ppp..ppp...',
909  '.....ppp..ppp...', '.....ppp..ppp...', '.....ppp..ppp...', '.....ppp..ppp...', '.....ppp..ppp...',
910  '....PPPP..PPPP..', '....PPPP..PPPP..'
911];
912const MAN_STEP = ['.....ppp..ppp...', '.....ppp...ppp..', '....ppp....ppp..', '....ppp....ppp..', '...ppp.....ppp..', '...ppp.....ppp..', '..PPPP....PPPP..', '..PPPP....PPPP..'];
913const PAL = { h: '#2B1D15', s: '#E6B18C', e: '#14161F', m: '#B07052', k: '#2B3042', K: '#C9A55A', l: '#1E2231', w: '#F5F3EE', t: '#22D39A', q: '#22D39A', g: '#E2C277', n: '#FBFAF6', r: '#D9DDE6', p: '#2B3042', P: '#111218' };
914function drawSprite(rows, x, y) {
915  for (let r = 0; r < rows.length; r++) { const row = rows[r];
916    for (let c = 0; c < row.length; c++) { const ch = row[c]; if (ch !== '.') { ctx.fillStyle = PAL[ch]; ctx.fillRect(x + c * P, y + r * P, P, P); } } }
917}
918
919/* ---------- State ---------- */
920// Shift length: help is offered at HELP_BY seconds (earlier only if stress hits HELP_STRESS, well before
921// the 100 burnout), then the swarm runs SWARM_SECS (~37s total).
922const SWARM_SECS = 18, HELP_BY = 19, HELP_STRESS = 70;
923const S = { mode: 'idle', phase: 'solo', t: 0, phaseT: 0, items: [], drones: [], parts: [], texts: [], stress: 0,
924  you: 0, og: 0, dropped: 0, moments: 0, missed: 0, spawnT: 0, greenT: 0, procT: 0, helpReady: false, easy: false,
925  man: { x: 0, vx: 0 }, tx: null, tilt: 0, keys: { l: false, r: false }, visible: true, hub: { x: 0, y: 0 }, step: 0 };
926let uid = 0;
927
928const plateX = () => S.man.x + 7 * P;
929const bob = () => (Math.abs(S.man.vx) > 40 && Math.floor(S.t * 8) % 2) ? P : 0;
930const plateY = () => groundY - 41 * P + bob();
931const surfaceY = o => plateY() + o * Math.sin(S.tilt);
932
933function newItem(kind, vy, x) {
934  let def, cat;
935  if (kind === 'human') { def = pick(HUMAN); cat = 'human'; } else { const r = pick(ROUTINE); cat = r[0]; def = { label: r[1], labelPt: r[2] }; }
936  return { id: ++uid, kind, cat, label: def.label, labelPt: def.labelPt, story: def.story, storyPt: def.storyPt, x, y: -S_ - 10, vx: 0, vy, state: 'fall', o: 0, v: 0, stack: 0, claimed: null, landedAt: 0 };
937}
938function plateItems() { return S.items.filter(i => i.state === 'plate').sort((a, b) => a.landedAt - b.landedAt); }
939function computeStacks(list) {
940  for (let i = 0; i < list.length; i++) { let base = 0;
941    for (let j = 0; j < i; j++) if (Math.abs(list[i].o - list[j].o) < S_ * .85) base = Math.max(base, list[j].stack + S_);
942    list[i].stack = base; }
943}
944function stackAt(o, list) { let m = 0; for (const j of list) if (Math.abs(o - j.o) < S_ * .85) m = Math.max(m, j.stack + S_); return m; }
945function itemPos(it) {
946  if (it.state === 'plate') return { x: plateX() + it.o - S_ / 2, y: surfaceY(it.o) - it.stack - S_ };
947  return { x: it.x, y: it.y };
948}
949
950/* ---------- Effects ---------- */
951function burst(x, y, color, n) { for (let i = 0; i < n; i++) S.parts.push({ x, y, vx: rand(-160, 160), vy: rand(-260, -60), life: rand(.4, .8), c: color }); }
952function floatText(x, y, txt, c) { S.texts.push({ x, y, txt, c, life: 1.1 }); }
953let toastTimer = 0, toastKind = '', toastNext = null;
954function toast(title, body, kind) {
955  // A caught green moment keeps the box until it fades; other messages wait their turn.
956  if (kind !== 'green' && toastKind === 'green' && toastTimer > .4) { toastNext = [title, body, kind]; return; }
957  toastKind = kind || ''; toastNext = null;
958  const el = $('toast'); $('toastT').textContent = title; $('toastB').textContent = body;
959  el.className = 'toast' + (kind === 'og' ? ' og' : kind === 'green' ? ' green' : ''); el.hidden = false; el.style.opacity = '1'; void el.offsetWidth; el.classList.add('pop'); toastTimer = kind === 'green' ? 3.2 : 3.6;
960}
961
962/* ---------- Background ---------- */
963function rng(seed) { return () => { seed |= 0; seed = seed + 0x6D2B79F5 | 0; let t = Math.imul(seed ^ seed >>> 15, 1 | seed); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; }; }
964function buildBg() {
965  bg = document.createElement('canvas'); bg.width = Math.round(W * DPR); bg.height = Math.round(H * DPR);
966  const b = bg.getContext('2d'); b.setTransform(DPR, 0, 0, DPR, 0, 0);
967  const f = (x, y, w, h, c) => { b.fillStyle = c; b.fillRect(sn(x), sn(y), Math.max(P, sn(w)), Math.max(P, sn(h))); };
968  const r = rng(11);
969  const ceilB = sn(H * .09), wainTop = sn(groundY - H * .2);
970  const winL = sn(W * .05), winR = sn(W * .95), winT = sn(H * .17), winB = sn(wainTop - 3 * P), winH = winB - winT;
971
972  // wall: deep green panelling
973  f(0, 0, W, groundY, '#1E3A31');
974  for (let x = sn(W * .02); x < W; x += 24 * P) { f(x, ceilB, P, wainTop - ceilB, '#18302A'); f(x + P, ceilB, P, wainTop - ceilB, '#26473C'); }
975
976  // window view: sky, opposite stand, pitch
977  const skyB = winT + winH * .34, standB = winT + winH * .68;
978  const sky = ['#86BEE6', '#93C6EA', '#A2CFEE', '#B3D8F1', '#C6E2F4'];
979  sky.forEach((c, i) => f(winL, winT + i * (skyB - winT) / sky.length, winR - winL, (skyB - winT) / sky.length + P, c));
980  for (let i = 0; i < 4; i++) { const cx = winL + r() * (winR - winL - 30 * P), cy = winT + P * 2 + r() * (skyB - winT - 8 * P);
981    f(cx, cy + 2 * P, 16 * P, 2 * P, '#EEF6FB'); f(cx + 3 * P, cy, 9 * P, 2 * P, '#EEF6FB'); f(cx + 2 * P, cy + 4 * P, 12 * P, P, '#DCEBF5'); }
982  // stand roof and tiers
983  f(winL, skyB, winR - winL, 2 * P, '#3A4152'); f(winL, skyB + 2 * P, winR - winL, P, '#E9EDF3');
984  const crowd = ['#E8E3D9', '#2F7BFF', '#22D39A', '#F2C14E', '#D9644F', '#F5F3EE', '#3A4152'];
985  for (let y = skyB + 3 * P, row = 0; y < standB; y += 2 * P, row++) {
986    f(winL, y, winR - winL, 2 * P, row % 2 ? '#6B7488' : '#5D6579');
987    for (let x = winL; x < winR; x += P) if (r() < .42) f(x, y, P, P, crowd[Math.floor(r() * crowd.length)]);
988  }
989  f(winL, standB, winR - winL, P, '#2F3546');
990  // pitch
991  for (let y = standB + P, i = 0; y < winB; y += 3 * P, i++) f(winL, y, winR - winL, 3 * P, i % 2 ? '#3E9B52' : '#48A75C');
992  f(winL, standB + 4 * P, winR - winL, P, '#EAF3EA');
993  // glass glare
994  b.globalAlpha = .1;
995  for (let i = 0; i < 3; i++) { const gx = winL + (winR - winL) * (.15 + i * .3); for (let k = 0; k < winH; k += P) f(gx + k * .45, winT + k, 3 * P, P, '#FFFFFF'); }
996  b.globalAlpha = 1;
997  // frame and mullions
998  f(winL - 2 * P, winT - 2 * P, winR - winL + 4 * P, 2 * P, '#3A2719'); f(winL - 2 * P, winB, winR - winL + 4 * P, 2 * P, '#3A2719');
999  f(winL - 2 * P, winT, 2 * P, winH, '#3A2719'); f(winR, winT, 2 * P, winH, '#3A2719');
1000  f(winL, winT, winR - winL, P, '#C9A55A');
1001  const panes = W < 640 ? 2 : 4;
1002  for (let i = 1; i < panes; i++) { const mx = winL + (winR - winL) * i / panes; f(mx - P, winT, 2 * P, winH, '#3A2719'); f(mx, winT, P, winH, '#4A3423'); }
1003
1004  // ceiling and crown moulding
1005  f(0, 0, W, ceilB, '#2A1D15'); f(0, ceilB, W, P, '#C9A55A'); f(0, ceilB + P, W, P, '#3A2719');
1006  // pendant lamps
1007  const lamps = W < 640 ? [.3, .7] : [.22, .5, .78];
1008  for (const lx of lamps) {
1009    const x = W * lx, y = winT + 4 * P;
1010    f(x, ceilB + 2 * P, P, y - ceilB - 2 * P, '#1A120D');
1011    for (let k = 0; k < 4; k++) { b.globalAlpha = .07; f(x - (5 + k * 3) * P, y + 3 * P + k * 3 * P, (11 + k * 6) * P, 3 * P, '#FFE3A3'); }
1012    b.globalAlpha = 1;
1013    f(x - 2 * P, y, 5 * P, P, '#B8913F'); f(x - 3 * P, y + P, 7 * P, P, '#C9A55A'); f(x - 4 * P, y + 2 * P, 9 * P, P, '#E2C277');
1014    f(x - P, y + 3 * P, 3 * P, P, '#FFF1C9');
1015  }
1016
1017  // wainscot with brass rail
1018  f(0, wainTop, W, groundY - wainTop, '#4A3020');
1019  for (let x = sn(W * .02); x < W; x += 24 * P) { f(x + 2 * P, wainTop + 4 * P, 20 * P, groundY - wainTop - 8 * P, '#553722'); f(x + 2 * P, wainTop + 4 * P, 20 * P, P, '#6A462D'); }
1020  f(0, wainTop, W, P, '#E2C277'); f(0, wainTop + P, W, P, '#B8913F');
1021
1022  // marble floor
1023  f(0, groundY, W, H - groundY, '#E7E1D6');
1024  for (let y = groundY + 2 * P, row = 0; y < H; y += 5 * P, row++)
1025    for (let x = (row % 2) * 10 * P; x < W; x += 20 * P) f(x, y, 10 * P, 5 * P, '#D4CDC0');
1026  f(0, groundY, W, 2 * P, '#2A1D15');
1027
1028  // props: dressed table with champagne bucket, potted palm
1029  const tX = sn(W * (W < 700 ? .08 : .14)), tW = 22 * P, tY = groundY - 12 * P;
1030  f(tX + 9 * P, tY + 2 * P, 4 * P, 10 * P, '#3A2719');
1031  f(tX, tY, tW, 2 * P, '#FBFAF6'); f(tX + P, tY + 2 * P, tW - 2 * P, 6 * P, '#F1EEE6'); f(tX + P, tY + 7 * P, tW - 2 * P, P, '#DDD8CC');
1032  f(tX + 4 * P, tY - 5 * P, 5 * P, 5 * P, '#C3CAD6'); f(tX + 4 * P, tY - 5 * P, 5 * P, P, '#E9EDF3');
1033  f(tX + 6 * P, tY - 9 * P, 2 * P, 4 * P, '#1F4A35'); f(tX + 6 * P, tY - 10 * P, 2 * P, P, '#E2C277');
1034  f(tX + 13 * P, tY - 4 * P, P, 4 * P, '#F2E7C4'); f(tX + 16 * P, tY - 4 * P, P, 4 * P, '#F2E7C4');
1035  const pX = sn(W * .9);
1036  f(pX - 4 * P, groundY - 8 * P, 8 * P, 8 * P, '#2A1D15'); f(pX - 4 * P, groundY - 8 * P, 8 * P, P, '#C9A55A');
1037  const leaves = [[-1, -14, 2, 6], [-6, -16, 5, 2], [1, -17, 6, 2], [-8, -13, 4, 2], [4, -14, 5, 2], [-3, -20, 4, 2], [0, -21, 2, 3], [-10, -11, 3, 2], [7, -11, 3, 2]];
1038  for (const [lx, ly, lw, lh] of leaves) f(pX + lx * P, groundY + ly * P, lw * P, lh * P, '#2E7D4F');
1039  f(pX - P, groundY - 14 * P, 2 * P, 6 * P, '#3E5E2E');
1040}
1041function resize() {
1042  const r = stage.getBoundingClientRect(); W = r.width; H = r.height;
1043  DPR = Math.min(2, window.devicePixelRatio || 1);
1044  cv.width = Math.round(W * DPR); cv.height = Math.round(H * DPR); ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
1045  ctx.imageSmoothingEnabled = false;
1046  P = W < 640 ? 4 : 5; halfW = 12 * P; S_ = 6 * P;
1047  groundY = H - Math.max(64, Math.round(H * .13));
1048  buildBg();
1049  if (S.mode !== 'play') S.man.x = idleX();
1050  S.man.x = clamp(S.man.x, halfW - 7 * P, W - halfW - 7 * P);
1051}
1052function idleX() { return W < 700 ? W * .5 - 7 * P : W * .68; }
1053
1054/* ---------- Game flow ---------- */
1055function reset() {
1056  toastNext = null; toastKind = '';
1057  S.items = []; S.drones = []; S.parts = []; S.texts = [];
1058  S.mood = 0; S.t = 0; S.phaseT = 0; S.phase = 'solo'; S.stress = 0; S.you = 0; S.og = 0; S.dropped = 0; S.moments = 0; S.missed = 0;
1059  S.spawnT = .6; S.greenT = 3.5; S.procT = 0; S.helpReady = false; S.tilt = 0; S.man.vx = 0; S.tx = null;
1060  S.man.x = clamp(W / 2 - 7 * P, halfW - 7 * P, W - halfW - 7 * P);
1061}
1062function idleScene() {
1063  S.items = []; S.man.x = idleX();
1064  const demo = [['tickets', -30], ['guest', 18], ['sponsor', -8]];
1065  demo.forEach(([cat, o], i) => { const it = newItem('routine', 0, 0); it.cat = cat; it.state = 'plate'; it.o = o; it.landedAt = i; S.items.push(it); });
1066}
1067function start(easy) {
1068  reset(); S.easy = !!easy; S.mode = 'play'; S.paused = false; $('ovHelp').hidden = true;
1069  $('ovStart').hidden = true; $('ovEnd').hidden = true; $('hud').hidden = false; $('helpBtn').hidden = true; $('swarmBadge').hidden = true;
1070  stage.classList.add('playing'); if (stage.classList.contains('ended')) { stage.classList.remove('ended'); resize(); }
1071}
1072function offerHelp() {
1073  S.helpReady = true; S.paused = true; S.keys.l = S.keys.r = false;
1074  if (toastKind !== 'green') { $('toast').hidden = true; toastTimer = 0; }
1075  stage.classList.remove('playing');
1076  $('ovHelp').hidden = false;
1077  try { $('helpCall').focus({ preventScroll: true }); } catch (_) {}
1078}
1079function callSwarm() {
1080  if (S.mode !== 'play' || S.phase !== 'solo' || !S.helpReady) return;
1081  const br = $('helpCall').getBoundingClientRect(), sr = stage.getBoundingClientRect();
1082  S.hub = { x: br.left - sr.left + br.width / 2, y: br.top - sr.top + br.height / 2 };
1083  $('ovHelp').hidden = true; S.paused = false; stage.classList.add('playing');
1084  $('helpBtn').hidden = true; $('swarmBadge').hidden = false;
1085  S.phase = 'swarm'; S.phaseT = 0; S.spawnT = .2; S.greenT = 1.2;
1086  const n = W < 640 ? 2 : 3;
1087  for (let i = 0; i < n; i++) S.drones.push({ x: S.hub.x + rand(-20, 20), y: S.hub.y + rand(-20, 20), state: 'seek', target: null, i, spin: Math.random() * 6 });
1088  burst(S.hub.x, S.hub.y, '#2F7BFF', 24);
1089}
1090function end(kind) {
1091  S.mode = 'end'; stage.classList.remove('playing');
1092  $('hud').hidden = true; $('helpBtn').hidden = true; $('swarmBadge').hidden = true; $('toast').hidden = true; toastTimer = 0; toastNext = null; toastKind = ''; $('ovHelp').hidden = true; S.paused = false;
1093  $('sYou').textContent = S.you; $('sOg').textContent = S.og; $('sMom').textContent = S.moments; $('sDrop').textContent = S.dropped;
1094  S.lastEnd = kind; fillEnd(kind);
1095  $('ovEnd').hidden = false; stage.classList.add('ended'); resize();
1096  S.items = S.items.filter(i => i.state === 'plate');
1097}
1098
1099function fillEnd(kind) {
1100  const again = $('againBtn');
1101  if (kind === 'burnout') {
1102    $('endTitle').textContent = tr('One tray was never going to be enough.', 'Uma bandeja nunca ia chegar.');
1103    $('endText').textContent = tr('You handled ' + S.you + ' requests on your own and ' + S.dropped + ' guests had to ask twice. Try again with the swarm.',
1104      'Tratou de ' + S.you + ' pedidos sozinho e ' + S.dropped + ' convidados tiveram de pedir duas vezes. Tente outra vez com a Outgoing.');
1105    (again.querySelector('span') || again).textContent = tr('Play again with Outgoing', 'Jogar com a Outgoing'); again.dataset.easy = '1';
1106  } else {
1107    $('endTitle').textContent = tr('Your team should only carry what matters.', 'A sua equipa só devia carregar o que importa.');
1108    $('endText').textContent = tr('Outgoing handled ' + S.og + ' routine requests. You were there for ' + S.moments + (S.moments === 1 ? ' moment' : ' moments') + ' that needed a person.',
1109      'A Outgoing tratou de ' + S.og + ' pedidos de rotina. Esteve presente em ' + S.moments + (S.moments === 1 ? ' momento' : ' momentos') + ' que precisavam de uma pessoa.');
1110    (again.querySelector('span') || again).textContent = tr('Play again', 'Jogar outra vez'); again.dataset.easy = '';
1111  }
1112}
1113
1114/* ---------- Update ---------- */
1115function approach(v, t, d) { return v < t ? Math.min(t, v + d) : Math.max(t, v - d); }
1116function update(dt) {
1117  S.t += dt; S.phaseT += dt; S.mood = (S.mood || 0) * Math.max(0, 1 - .22 * dt);
1118  const maxV = Math.max(300, W * .6);
1119  let desired = 0;
1120  if (S.keys.l && !S.keys.r) desired = -maxV; else if (S.keys.r && !S.keys.l) desired = maxV;
1121  else if (S.tx != null) desired = clamp((S.tx - plateX()) * 7, -maxV, maxV);
1122  const nv = approach(S.man.vx, desired, 2400 * dt), acc = (nv - S.man.vx) / dt;
1123  S.man.vx = nv; S.man.x += nv * dt;
1124  const minX = halfW - 7 * P, maxX = W - halfW - 7 * P;
1125  if (S.man.x < minX) { S.man.x = minX; S.man.vx = 0; } if (S.man.x > maxX) { S.man.x = maxX; S.man.vx = 0; }
1126
1127  let plate = plateItems(); computeStacks(plate);
1128  let torque = 0; for (const it of plate) torque += it.o / halfW;
1129  const target = clamp(torque * .035 - acc * .00009, -.55, .55);
1130  S.tilt += (target - S.tilt) * Math.min(1, dt * 7);
1131  const sinT = Math.sin(S.tilt);
1132  for (const it of plate) {
1133    if (Math.abs(S.tilt) > .05) it.v += 1500 * sinT * dt; else it.v *= Math.max(0, 1 - 12 * dt);
1134    it.v *= (1 - 1.2 * dt); it.o += it.v * dt;
1135    if (Math.abs(it.o) > halfW + S_ * .2) {
1136      const p = itemPos(it); it.state = 'off'; it.x = p.x; it.y = p.y; it.vx = it.v * .8 + S.man.vx * .5; it.vy = -60;
1137      if (it.claimed) { it.claimed.target = null; it.claimed = null; }
1138    }
1139  }
1140  plate = plateItems(); computeStacks(plate);
1141
1142  // spawning
1143  if (S.phase === 'solo') {
1144    S.spawnT -= dt;
1145    if (S.spawnT <= 0) { S.items.push(newItem('routine', H * Math.min(.3, .14 + S.phaseT * .0045), rand(S_, W - 2 * S_))); S.spawnT = Math.max(.55, 1.45 - S.phaseT * .028); }
1146    S.greenT -= dt;
1147    if (S.greenT <= 0) { S.items.push(newItem('human', H * .11, rand(halfW, W - halfW - S_))); S.greenT = 5.5; }
1148  } else {
1149    S.spawnT -= dt; S.greenT -= dt;
1150    if (S.spawnT <= 0) { S.items.push(newItem('routine', H * .14, rand(S_, W - 2 * S_))); S.spawnT = 1.05; }
1151    if (S.greenT <= 0) { S.items.push(newItem('human', H * .12, rand(halfW, W - halfW - S_))); S.greenT = 2.4; }
1152  }
1153
1154  // falling
1155  for (const it of S.items) {
1156    if (it.state !== 'fall' && it.state !== 'off') continue;
1157    const prevBottom = it.y + S_;
1158    if (it.state === 'off') { it.vy += 1600 * dt; it.x += it.vx * dt; }
1159    it.y += it.vy * dt;
1160    if (it.state === 'fall') {
1161      const o = it.x + S_ / 2 - plateX();
1162      if (Math.abs(o) <= halfW + S_ * .35) {
1163        const top = surfaceY(o) - stackAt(o, plate);
1164        if (prevBottom <= top + 6 && it.y + S_ >= top) { land(it, o); plate = plateItems(); computeStacks(plate); continue; }
1165      }
1166    }
1167    if (it.y + S_ >= groundY) hitGround(it);
1168  }
1169
1170  // you handle the oldest request
1171  const mine = plate.filter(i => !i.claimed);
1172  if (mine.length) {
1173    S.procT += dt; const dur = S.phase === 'solo' ? 1.6 : 1.1;
1174    if (S.procT >= dur) { S.procT = 0; const it = mine[0]; const p = itemPos(it); it.state = 'gone'; S.you++; S.mood = Math.min(1, (S.mood || 0) + .2); burst(p.x + S_ / 2, p.y + S_ / 2, '#EEF1F8', 10); floatText(p.x + S_ / 2, p.y - 6, tr('+1 handled', '+1 tratado'), '#EEF1F8'); }
1175  } else S.procT = 0;
1176
1177  // drones
1178  for (const d of S.drones) updateDrone(d, dt);
1179
1180  // stress
1181  if (S.phase === 'solo') {
1182    const n = mine.length; if (n > 4) S.stress += (n - 4) * 3.2 * dt; else S.stress -= 1.5 * dt;
1183  } else S.stress -= 9 * dt;
1184  S.stress = clamp(S.stress, 0, 100);
1185  if (S.phase === 'solo' && !S.helpReady && (S.stress >= HELP_STRESS || S.phaseT >= HELP_BY || (S.easy && S.phaseT >= 5))) offerHelp();
1186  if (S.phase === 'solo' && S.stress >= 100) { end('burnout'); return; }
1187  if (S.phase === 'swarm' && S.phaseT >= SWARM_SECS) { end('win'); return; }
1188
1189  S.items = S.items.filter(i => i.state !== 'gone');
1190}
1191function land(it, o) {
1192  if (it.kind === 'human') {
1193    it.state = 'gone'; S.moments++; S.you += 3; S.mood = Math.min(1, (S.mood || 0) + .5); const px = plateX() + o, py = surfaceY(o) - S_;
1194    burst(px, py, '#22D39A', 26); floatText(px, py - 10, tr('+3 · Moment that mattered', '+3 · Momento que contou'), '#22D39A');
1195    return;
1196  }
1197  it.state = 'plate'; it.o = clamp(o, -halfW, halfW); it.v = 0; it.landedAt = S.t;
1198  if (it.claimed) { it.claimed.target = null; it.claimed = null; }
1199}
1200function hitGround(it) {
1201  it.state = 'gone';
1202  const cx = it.x + S_ / 2;
1203  if (it.kind === 'human') { S.missed++; S.stress += 3; S.mood = Math.max(-1, (S.mood || 0) - .45); burst(cx, groundY, '#22D39A', 8); floatText(cx, groundY - 24, tr('Moment missed', 'Momento perdido'), '#A6AEC7'); }
1204  else { S.dropped++; S.mood = Math.max(-1, (S.mood || 0) - .35); S.stress += S.phase === 'solo' ? 10 : 4; burst(cx, groundY, '#FF9461', 12); floatText(cx, groundY - 24, tr('Guest asked twice', 'Pediu duas vezes'), '#FF9461'); }
1205  if (it.claimed) { it.claimed.target = null; it.claimed = null; }
1206}
1207function updateDrone(d, dt) {
1208  d.spin += dt * 30;
1209  const speed = Math.max(300, W * .3);
1210  let tx, ty;
1211  if (d.state === 'seek') {
1212    if (!d.target || (d.target.state !== 'fall' && d.target.state !== 'plate') || d.target.claimed !== d) {
1213      d.target = null; let best = null, bs = Infinity;
1214      for (const it of S.items) {
1215        if (it.kind !== 'routine' || it.claimed || (it.state !== 'fall' && it.state !== 'plate')) continue;
1216        const p = itemPos(it); if (p.y < 0) continue;
1217        const sc = Math.hypot(p.x - d.x, p.y - d.y) - p.y * .4;
1218        if (sc < bs) { bs = sc; best = it; }
1219      }
1220      if (best) { best.claimed = d; d.target = best; }
1221    }
1222    if (d.target) { const p = itemPos(d.target); tx = p.x + S_ / 2; ty = p.y - O_H * droneScale() * .5 - 16; }
1223    else { tx = W * (.25 + d.i * .25); ty = H * .3 + Math.sin(S.t * 2 + d.i) * 12; }
1224  } else { tx = S.hub.x; ty = S.hub.y; }
1225  const dx = tx - d.x, dy = ty - d.y, dist = Math.hypot(dx, dy);
1226  const step = Math.min(dist, speed * dt);
1227  const mvx = dist > 0 ? dx / dist * step / dt : 0;
1228  d.vx = (d.vx || 0) + (mvx - (d.vx || 0)) * Math.min(1, dt * 8);
1229  if (d.vx > 40) d.face = 1; else if (d.vx < -40) d.face = -1;
1230  const lk = Math.max(1, dist); d.lookX = d.lookX == null ? 0 : d.lookX + ((dx / lk) - d.lookX) * Math.min(1, dt * 6);
1231  d.lookY = d.lookY == null ? 0 : d.lookY + ((dy / lk) - d.lookY) * Math.min(1, dt * 6);
1232  if (dist > 0) { d.x += dx / dist * step; d.y += dy / dist * step; }
1233  if (d.state === 'seek' && d.target && dist < 22) { const it = d.target; it.state = 'carried'; d.carry = it; d.state = 'carry'; d.target = null; }
1234  else if (d.state === 'carry' && dist < 18) {
1235    S.og++; d.carry.state = 'gone'; d.carry = null; d.state = 'seek'; burst(d.x, d.y, '#2F7BFF', 6);
1236  }
1237}
1238
1239/* ---------- Draw ---------- */
1240function drawCube(x, y, cat, scale) {
1241  const c = CATS[cat], z = S_ * (scale || 1), sx = sn(x), sy = sn(y);
1242  R(sx, sy, z, z, c.base); R(sx, sy, z, P, c.hi); R(sx + z - P, sy + P, P, z - P, c.lo); R(sx, sy + z - P, z, P, c.lo);
1243  const g = c.glyph, u = Math.max(2, Math.round(z / 10)), gw = g[0].length * u, gh = g.length * u;
1244  const gx = Math.round(sx + (z - P - gw) / 2), gy = Math.round(sy + P + (z - 2 * P - gh) / 2);
1245  ctx.fillStyle = 'rgba(255,255,255,.85)';
1246  for (let r = 0; r < g.length; r++) for (let k = 0; k < g[r].length; k++) if (g[r][k] === '#') ctx.fillRect(gx + k * u, gy + r * u, u, u);
1247}
1248function drawLabel(txt, cx, by, green) {
1249  ctx.font = '600 ' + (P < 5 ? 14 : 16) + 'px "Pixelify Sans", ui-monospace, monospace';
1250  const w = Math.ceil(ctx.measureText(txt).width) + 20, h = P < 5 ? 26 : 30;
1251  const x = Math.round(clamp(cx - w / 2, 6, W - w - 6)), y = Math.round(by - h);
1252  ctx.fillStyle = 'rgba(0,0,0,.28)'; ctx.fillRect(x + 3, y + 3, w, h);
1253  ctx.fillStyle = green ? '#22D39A' : '#FBFAF6'; ctx.fillRect(x, y, w, h);
1254  ctx.fillStyle = green ? '#0B8A5C' : '#14172B'; ctx.fillRect(x, y + h - 3, w, 3);
1255  ctx.fillStyle = '#14172B'; ctx.textBaseline = 'middle'; ctx.fillText(txt, x + 10, y + h / 2 - 1);
1256}
1257/* Drone: built from the logo's O capsule. The O window is the drone's eye. */
1258const O_PATH = new Path2D('M309.94,335.19Q298,319,277.2,322.69l-69.94,11.49c-.55.08-2.24.39-2.55.45l-.19,0a18.91,18.91,0,0,0-3.51.87q-17.55,4.86-28.1,22.57-11.85,20-11.86,51.39t11.73,47.14q10.65,14.17,29,12.41h.11a21.39,21.39,0,0,0,2.71-.37c.32-.05.63-.08,1-.14s.37-.09.56-.13l1.29-.23,69.83-11.44q20.32-3.58,32.48-23.33t12.16-49.65Q321.84,351.36,309.94,335.19Zm-18.19,83.62q-4.83,11.32-14.55,13T262.7,424q-4.79-9.54-4.78-30.77t4.78-32.59q4.77-11.27,14.5-13t14.55,7.91q4.81,9.63,4.82,30.85Q296.57,407.49,291.75,418.81Z');
1259const O_CX = 241.5, O_CY = 395.6, O_W = 161, O_H = 147, EYE_X = 277.2, EYE_Y = 389.6;
1260function droneScale() { return (11 * P) / O_W; }
1261function drawDrone(d) {
1262  const k = droneScale(), bw = O_W * k, bh = O_H * k;
1263  const x = d.x, y = d.y + Math.sin(S.t * 5 + d.i) * 2;
1264  const bank = clamp((d.vx || 0) / 1400, -.35, .35);
1265  const face = d.face || 1;
1266
1267  // tractor beam
1268  let beamTo = null;
1269  if (d.carry) beamTo = { x: x, y: y + bh / 2 + 10 + S_ * .7 };
1270  else if (d.target) { const p = itemPos(d.target); const tx = p.x + S_ / 2, ty = p.y; if (Math.hypot(tx - x, ty - y) < 170) beamTo = { x: tx, y: ty + S_ / 2 }; }
1271  if (beamTo) {
1272    const g = ctx.createLinearGradient(x, y, beamTo.x, beamTo.y);
1273    g.addColorStop(0, 'rgba(126,170,255,.45)'); g.addColorStop(1, 'rgba(34,211,154,.06)');
1274    ctx.fillStyle = g; ctx.beginPath();
1275    ctx.moveTo(x - 6, y + bh * .3); ctx.lineTo(x + 6, y + bh * .3);
1276    ctx.lineTo(beamTo.x + S_ * .55, beamTo.y); ctx.lineTo(beamTo.x - S_ * .55, beamTo.y); ctx.closePath(); ctx.fill();
1277  }
1278  if (d.carry) drawCube(x - S_ * .35, y + bh / 2 + 10, d.carry.cat, .7);
1279
1280  // thrust glow
1281  const glow = ctx.createRadialGradient(x, y + bh * .55, 0, x, y + bh * .55, bw * .8);
1282  glow.addColorStop(0, 'rgba(47,123,255,.35)'); glow.addColorStop(1, 'rgba(47,123,255,0)');
1283  ctx.fillStyle = glow; ctx.fillRect(x - bw, y, bw * 2, bw);
1284
1285  ctx.save();
1286  ctx.translate(x, y); ctx.rotate(bank);
1287
1288  // arms and rotors
1289  const armY = -bh * .5 - 7, armX = bw * .62;
1290  ctx.strokeStyle = '#2C3260'; ctx.lineWidth = 3; ctx.lineCap = 'round';
1291  ctx.beginPath(); ctx.moveTo(-bw * .28, -bh * .32); ctx.lineTo(-armX, armY); ctx.moveTo(bw * .28, -bh * .4); ctx.lineTo(armX, armY); ctx.stroke();
1292  for (const sx of [-armX, armX]) {
1293    const sw = bw * .42, ph = Math.abs(Math.sin(d.spin * .9 + sx));
1294    ctx.fillStyle = 'rgba(238,242,248,.14)'; ctx.beginPath(); ctx.ellipse(sx, armY, sw, 3.5, 0, 0, Math.PI * 2); ctx.fill();
1295    ctx.fillStyle = 'rgba(238,242,248,.75)'; ctx.beginPath(); ctx.ellipse(sx, armY, sw * (.2 + .8 * ph), 2, 0, 0, Math.PI * 2); ctx.fill();
1296    ctx.fillStyle = '#2C3260'; ctx.beginPath(); ctx.arc(sx, armY + 1, 3, 0, Math.PI * 2); ctx.fill();
1297  }
1298
1299  // body in logo space
1300  ctx.scale(face * k, k);
1300 ctx.translate(-O_CX, -O_CY);
1301  // eye behind the O window
1302  ctx.fillStyle = '#070912'; ctx.beginPath(); ctx.ellipse(EYE_X, EYE_Y, 26, 48, -.1, 0, Math.PI * 2); ctx.fill();
1303  const lx = clamp(d.lookX || 0, -1, 1), ly = clamp(d.lookY || 0, -1, 1);
1304  ctx.save(); ctx.shadowColor = '#22D39A'; ctx.shadowBlur = 18 / k * .5;
1305  ctx.fillStyle = '#22D39A'; ctx.beginPath(); ctx.ellipse(EYE_X + lx * face * 7, EYE_Y + ly * 16, 10, 18, -.1, 0, Math.PI * 2); ctx.fill();
1306  ctx.restore();
1307  ctx.fillStyle = 'rgba(255,255,255,.9)'; ctx.beginPath(); ctx.ellipse(EYE_X + lx * face * 7 + 3, EYE_Y + ly * 16 - 7, 3, 5, 0, 0, Math.PI * 2); ctx.fill();
1308  // capsule
1309  const g2 = ctx.createLinearGradient(0, 322, 0, 469);
1310  g2.addColorStop(0, '#6FA4FF'); g2.addColorStop(.45, '#2F7BFF'); g2.addColorStop(1, '#1A4FC0');
1311  ctx.fillStyle = g2; ctx.fill(O_PATH, 'evenodd');
1312  // rim light and a panel seam
1313  ctx.save(); ctx.clip(O_PATH, 'evenodd');
1314  ctx.fillStyle = 'rgba(255,255,255,.35)'; ctx.beginPath(); ctx.ellipse(225, 338, 70, 9, -.16, 0, Math.PI * 2); ctx.fill();
1315  ctx.strokeStyle = 'rgba(10,12,24,.28)'; ctx.lineWidth = 4; ctx.beginPath(); ctx.moveTo(236, 330); ctx.quadraticCurveTo(222, 400, 238, 462); ctx.stroke();
1316  ctx.fillStyle = '#22D39A'; ctx.beginPath(); ctx.arc(196, 420, 6, 0, Math.PI * 2); ctx.fill();
1317  ctx.restore();
1318  ctx.restore();
1319}
1320function draw() {
1321  ctx.clearRect(0, 0, W, H);
1322  if (bg) ctx.drawImage(bg, 0, 0, W, H);
1323
1324  // processing bar on the floor
1325  if (S.mode === 'play' && S.procT > 0) {
1326    const dur = S.phase === 'solo' ? 1.6 : 1.1, bw = 16 * P, bx = S.man.x - 8 * P, by = groundY + 4 * P;
1327    R(bx, by, bw, 2 * P, '#2B3042'); R(bx, by, bw * (S.procT / dur), 2 * P, '#22D39A');
1328  }
1329
1330  // manager
1331  const left = sn(S.man.x - 8 * P), top = sn(groundY - 32 * P + bob());
1332  const rows = (Math.abs(S.man.vx) > 40 && Math.floor(S.t * 8) % 2) ? MAN.slice(0, 24).concat(MAN_STEP) : MAN;
1333  ctx.globalAlpha = .18; R(left + P, groundY - P, 14 * P, 2 * P, '#000000'); ctx.globalAlpha = 1;
1334  drawSprite(rows, left, top);
1335  R(left + 14 * P, top - 6 * P, 2 * P, 18 * P, PAL.k);
1336  R(left + 14 * P, top - 6 * P, 2 * P, P, PAL.w);
1337  R(left + 14 * P, top - 8 * P, 2 * P, 2 * P, PAL.s);
1338
1339  // silver salver
1340  const px = plateX(), py = plateY(), st = Math.sin(S.tilt);
1341  for (let k = -halfW; k <= halfW; k += P) {
1342    const y = py + k * st, ak = Math.abs(k);
1343    if (ak > halfW - P) { R(px + k, y - P, P, 2 * P, '#9AA3B6'); R(px + k, y + P, P, P, '#6E778C'); continue; }
1344    R(px + k, y, P, P, ak < halfW * .55 ? '#F7F9FC' : '#DCE2EC');
1345    R(px + k, y + P, P, P, '#A9B2C4');
1346    if (ak < halfW - 3 * P) R(px + k, y + 2 * P, P, P, '#7E879B');
1347  }
1348  R(px - 2 * P, py + 2 * P, 4 * P, P, '#7E879B');
1349
1350  // items
1351  for (const it of S.items) {
1352    if (it.state === 'plate') { const p = itemPos(it); drawCube(p.x, p.y, it.cat); }
1353  }
1354  for (const it of S.items) {
1355    if (it.state !== 'fall' && it.state !== 'off') continue;
1356    if (it.kind === 'human') {
1357      const a = .25 + .2 * Math.sin(S.t * 8);
1358      ctx.globalAlpha = a; R(it.x - 2 * P, it.y - 2 * P, S_ + 4 * P, S_ + 4 * P, '#22D39A'); ctx.globalAlpha = 1;
1359    }
1360    drawCube(it.x, it.y, it.cat);
1361    if (it.state === 'fall' && it.y > -S_) drawLabel(tr(it.label, it.labelPt || it.label), it.x + S_ / 2, it.y - 6, it.kind === 'human');
1362  }
1363  for (const d of S.drones) drawDrone(d);
1364
1365  // particles and text
1366  for (const p of S.parts) { ctx.globalAlpha = clamp(p.life * 2, 0, 1); R(p.x, p.y, P, P, p.c); }
1367  ctx.globalAlpha = 1;
1368  ctx.font = '600 ' + (P < 5 ? 13 : 15) + 'px "Pixelify Sans", ui-monospace, monospace'; ctx.textBaseline = 'middle';
1369  for (const t of S.texts) {
1370    ctx.globalAlpha = clamp(t.life * 1.5, 0, 1);
1371    const w = ctx.measureText(t.txt).width, tx = clamp(t.x - w / 2, 6, W - w - 6);
1372    ctx.lineWidth = 4; ctx.strokeStyle = '#0A0C18'; ctx.lineJoin = 'round'; ctx.strokeText(t.txt, tx, t.y); ctx.fillStyle = t.c; ctx.fillText(t.txt, tx, t.y);
1373  }
1374  ctx.globalAlpha = 1;
1375}
1376function tickFx(dt) {
1377  for (const p of S.parts) { p.vy += 900 * dt; p.x += p.vx * dt; p.y += p.vy * dt; p.life -= dt; }
1378  S.parts = S.parts.filter(p => p.life > 0);
1379  for (const t of S.texts) { t.y -= 40 * dt; t.life -= dt; }
1380  S.texts = S.texts.filter(t => t.life > 0);
1381  if (toastTimer > 0) { toastTimer -= dt; const el = $('toast'); if (toastTimer < .4) el.style.opacity = String(Math.max(0, toastTimer / .4)); if (toastTimer <= 0) { el.hidden = true; toastKind = ''; if (toastNext) { const q = toastNext; toastNext = null; toast(...q); } } }
1382}
1383function idleUpdate(dt) {
1384  S.t += dt; S.tilt = Math.sin(S.t * 1.3) * .04; S.man.vx = 0;
1385  const plate = plateItems(); computeStacks(plate);
1386}
1387
1388/* ---------- HUD ---------- */
1389const hud = { you: -1, mood: -9, sw: -1 };
1390function renderHud() {
1391  if (hud.you !== S.you) $('hYou').textContent = hud.you = S.you;
1392  const m = Math.round((S.mood || 0) * 20) / 20;
1393  if (hud.mood !== m) {
1394    hud.mood = m; const el = $('hScore'), a = Math.min(1, Math.abs(m)), base = [14, 17, 32], to = m >= 0 ? [34, 211, 154] : [229, 72, 77];
1395    const c = base.map((v, i) => Math.round(v + (to[i] - v) * a * .9));
1396    el.style.background = 'rgb(' + c.join(',') + ')'; el.style.color = (m > .45) ? '#14172B' : '#FFFFFF';
1397  }
1398  if (S.phase === 'swarm') { const r = Math.max(0, Math.ceil(SWARM_SECS - S.phaseT)); if (hud.sw !== r) $('swarmT').textContent = hud.sw = r; }
1399}
1400
1401/* ---------- Loop ---------- */
1402let last = performance.now();
1403function frame(now) {
1404  const dt = Math.min(.033, (now - last) / 1000); last = now;
1405  if (S.visible && !document.hidden) {
1406    if (S.mode === 'play') { if (!S.paused) update(dt); renderHud(); } else idleUpdate(dt);
1407    if (!(S.mode === 'play' && S.paused)) tickFx(dt); draw();
1408  }
1409  requestAnimationFrame(frame);
1410}
1411
1412/* ---------- Input ---------- */
1413window.addEventListener('keydown', e => {
1414  if (S.mode !== 'play') return;
1415  const k = e.key;
1416  if (k === 'ArrowLeft' || k === 'a' || k === 'A') { S.keys.l = true; S.tx = null; e.preventDefault(); }
1417  else if (k === 'ArrowRight' || k === 'd' || k === 'D') { S.keys.r = true; S.tx = null; e.preventDefault(); }
1418  else if (k === ' ' || k === 'Spacebar') { e.preventDefault(); callSwarm(); }
1419});
1420window.addEventListener('keyup', e => {
1421  const k = e.key;
1422  if (k === 'ArrowLeft' || k === 'a' || k === 'A') S.keys.l = false;
1423  if (k === 'ArrowRight' || k === 'd' || k === 'D') S.keys.r = false;
1424});
1425function pointerX(e) { return e.clientX - stage.getBoundingClientRect().left; }
1426cv.addEventListener('pointermove', e => { if (S.mode === 'play' && (e.pointerType === 'mouse' || e.buttons)) S.tx = pointerX(e); });
1427cv.addEventListener('pointerdown', e => { if (S.mode !== 'play') return; S.tx = pointerX(e); try { cv.setPointerCapture(e.pointerId); } catch (_) {} });
1428$('helpBtn').addEventListener('click', callSwarm);
1429$('helpCall').addEventListener('click', callSwarm);
1430$('playBtn').addEventListener('click', () => { stage.scrollIntoView({ behavior: 'smooth', block: 'start' }); start(false); });
1431$('againBtn').addEventListener('click', e => start(e.currentTarget.dataset.easy === '1'));
1432
1433/* ---------- Forms → POST /api/waitlist (the landing server emails the team) ---------- */
1434document.querySelectorAll('form.form').forEach(f => {
1435  f.addEventListener('submit', async e => {
1436    e.preventDefault();
1437    const email = f.querySelector('input[type=email]'), consent = f.querySelector('input[type=checkbox]');
1438    const err = f.querySelector('.err'), ok = f.querySelector('.ok'), btn = f.querySelector('button[type=submit]');
1439    if (btn.disabled) return;
1440    if (!email.value || !email.checkValidity()) { err.textContent = tr('Enter a valid email address, like [email protected].', 'Indique um email válido, como [email protected].'); err.hidden = false; email.focus(); return; }
1441    if (!consent.checked) { err.textContent = tr('Tick the box so we can email you.', 'Assinale a caixa para lhe podermos enviar emails.'); err.hidden = false; consent.focus(); return; }
1442    err.hidden = true; btn.disabled = true;
1443    try {
1444      const res = await fetch('/api/waitlist', {
1445        method: 'POST',
1446        headers: { 'Content-Type': 'application/json' },
1447        body: JSON.stringify({ email: email.value.trim(), consent: true, lang: LANG, source: f.id, company: f.elements.company.value }),
1448      });
1449      if (!res.ok) throw new Error('waitlist ' + res.status);
1450    } catch (_) {
1451      err.textContent = tr("We couldn't send your request. Try again in a moment.", 'Não conseguimos enviar o seu pedido. Tente novamente daqui a pouco.');
1452      err.hidden = false; btn.disabled = false; return;
1453    }
1454    ok.hidden = false; f.querySelector('.fields').hidden = true; consent.closest('label').hidden = true;
1455    f.querySelector('label[for^="email"]').hidden = true;
1456  });
1457});
1458
1459
1460/* ---------- Hero preview: the same suite, drawn once into a small canvas ---------- */
1461function renderPreview() {
1462  const pc = $('pv'); if (!pc) return; const r = pc.getBoundingClientRect(); if (!r.width) return;
1463  const keep = { W, H, P, groundY, halfW, S_, bg, ctx, items: S.items, drones: S.drones, parts: S.parts, texts: S.texts, x: S.man.x, vx: S.man.vx, tilt: S.tilt, mode: S.mode };
1464  W = r.width; H = r.height; pc.width = Math.round(W * DPR); pc.height = Math.round(H * DPR);
1465  ctx = pc.getContext('2d'); ctx.setTransform(DPR, 0, 0, DPR, 0, 0); ctx.imageSmoothingEnabled = false;
1466  P = W < 420 ? 2 : W < 560 ? 3 : 4; halfW = 12 * P; S_ = 6 * P; groundY = H - Math.max(36, Math.round(H * .13));
1467  buildBg();
1468  S.mode = 'preview'; S.drones = []; S.parts = []; S.texts = []; S.man.vx = 0; S.tilt = -.07;
1469  S.man.x = W * .52 - 7 * P;
1470  const stackDemo = [['tickets', -28], ['guest', -4], ['sponsor', 20], ['travel', -16], ['after', 8]];
1471  S.items = stackDemo.map(([cat, o], i) => { const it = newItem('routine', 0, 0); it.cat = cat; it.state = 'plate'; it.o = o * P / 5; it.landedAt = i; return it; });
1472  computeStacks(plateItems());
1473  draw();
1474  const fall = Math.round(plateY() - S_ * 3);
1475  document.querySelectorAll('.drops span').forEach(el => el.style.setProperty('--fall', fall + 'px'));
1476  ({ W, H, P, groundY, halfW, S_, bg, ctx } = keep);
1477  S.items = keep.items; S.drones = keep.drones; S.parts = keep.parts; S.texts = keep.texts; S.man.x = keep.x; S.man.vx = keep.vx; S.tilt = keep.tilt; S.mode = keep.mode;
1478}
1479let pvN = 23;
1480setInterval(() => { if (document.hidden) return; pvN += 1 + Math.floor(Math.random() * 3); const el = $('pvCount'); if (el) el.textContent = pvN; }, 1400);
1481
1482
1483/* ---------- Language ---------- */
1484const PT = {"navPlay": "Jogar", "navProduct": "Produto", "navJoin": "<span class=\"hide-sm\">Lista de&nbsp;</span>espera", "heroH": "Para os <mark>patrocinadores</mark> que vos apoiam e os <mark class=\"m2\">adeptos</mark> que nunca faltam a um jogo.", "heroLede": "Um concierge com IA para cada patrocinador, sócio e convidado. Trata dos pedidos por WhatsApp, email e telefone, aprende as preferências de cada pessoa e encaminha para a sua equipa apenas o que precisa de um toque humano.", "bridge": "No dia de jogo, tudo acontece ao mesmo tempo. Vai conseguir aguentar?", "try": "Experimente um turno <span aria-hidden=\"true\">↓</span>", "join": "Pedir acesso antecipado", "byYou": "pontos", "byOg": "pela Outgoing", "twice": "pediram 2x", "swarmOn": "Enxame ativo", "call": "Chamar o enxame", "space": "Espaço", "take": "Agora é a sua vez de gerir a hospital
1484idade.", "takeLede": "Apanhe os pedidos antes de caírem ao chão.", "start": "Começar turno", "skip": "Saltar", "controls": "Rato, <kbd>←</kbd><kbd>→</kbd> ou arraste", "helpT": "Demasiado em mãos?", "helpL": "Experimente com os agentes de IA da Outgoing.", "sYou": "os seus pontos", "sOg": "tratados pela Outgoing", "sMom": "momentos que contaram", "sDrop": "convidados que pediram duas vezes", "joinShort": "Pedir acesso", "consent": "Aceito receber emails sobre a Outgoing Hospitality. Posso cancelar a qualquer momento.", "okShort": "Pedido recebido.", "seeProduct": "Ver o produto", "ph": "[email protected]", "h2a": "A hospitalidade ainda depende das ferramentas erradas.", "h2b": "Uma plataforma. Quatro partes.", "h2c": "Conheça cada convidado antes de ele chegar.", "h2d": "Para todas as equipas.", "h2e": "Seja um dos primeiros clubes a experimentar.", "xpLabel": "Encontre-nos na EXPOFUT 2026", "xpQuote": "“A EXPOFUT dá-nos aquilo que nenhum outro palco em Portugal oferece.”", "xpMeta": "Arena Liga Portugal · 14 e 15 de outubro", "xpRead": "Ler a notícia", "c1": "Bilhética feita só para a entrada", "c2": "Melhores lugares não são uma experiência", "c3": "CRMs feitos para vender", "c4": "Bilhetes com a sua marca", "c5": "Concierge com IA, passagem para a equipa", "c6": "Todos os pedidos, uma só fila", "c7": "Agentes que fazem o trabalho pesado", "c8": "Recintos", "c9": "Equipas", "c10": "Competições", "c8p": "Estádios, pavilhões e courts que recebem convidados em cada evento.", "c9p": "Clubes que cuidam de patrocinadores e sócios em cada dia de jogo.", "h2cL": "Cada preferência, visita e pedido fica guardado no perfil do convidado, para que a sua equipa chegue preparada.", "profT": "Perfil do convidado", "profN": "6 eventos", "src": "Aprendido pelo concierge em 14 conversas", "legendG": "Os pedidos <b>verdes</b> valem mais.", "pressSpace": "Prima <kbd>Espaço</kbd>", "tapGo": "Toque aqui", "f1h": "Um agente para todas as conversas.", "f1p": "Convidados, gestores de patrocínio, catering e fornecedores escrevem para um só sítio. A Outgoing responde ao que pode e encaminha o resto para a sua equipa.", "f2h": "Os patrocinadores distribuem os seus bilhetes.", "f2p": "Cada patrocinador tem um espaço próprio para convidar clientes e colegas. Deixa de ter de andar atrás de listas.", "f3h": "Know-how que fica quando alguém sai.", "f3p": "Documentos, contactos e lições de cada evento ficam numa só base de conhecimento.", "f4h": "Follow-ups verdadeiramente pessoais.", "f4p": "Cada evento revela-nos as preferências de cada convidado. As ofertas seguem por email, WhatsApp ou telefone, conforme preferirem.", "scrollCue": "Deslize", "c10p": "Campeonatos que viajam, com uma nova lista de convidados em cada etapa.", "entry": "ENTRADA", "yourclub": "O seu clube · Hospitality", "b1": "Vou atrasado. Qual é o caminho mais rápido?", "guest": "Convidado", "b2": "Há um transfer oficial às 18:40 a partir do seu hotel. Deseja que o reserve?", "b3": "Ou posso pô-lo já em contacto com a Rita.", "q1": "Carrinho de bebé · Porta B", "q2": "Chegada de helicóptero · 17:20", "q3": "Zona pet-friendly", "q4": "Menu sem glúten", "st1": "Ação", "st2": "A aguardar", "st3": "IA", "st4": "Resolvido", "idsub": "Convidada de patrocinador · Banco Atlântico", "pref": "Prefere", "gf": "Sem glúten", "qt": "Mesa calma", "car": "De carro", "last": "Última visita", "lastv": "Precisou de um carrinho na porta B, resolvido em 4 min", "next": "Neste jogo", "bday": "Faz anos", "sugg": "<b>Para a sua equipa:</b> camisola autografada na mesa dela ao intervalo.", "okLong": "Entraremos em contacto com mais informações.", "legal": "outGoing é uma marca detida e operada pela Kangaroo Theory, Lda. · NIF 516657798 · Rua do Nogueiral, N.º 1 A, 2350-413 Torres Novas, Portugal", "fHome": "Início", "fPromoters": "Para organizadores", "fCareers": "Carreiras", "workEmail": "Email profissional", "photoTag": "Dia de jogo · Mesa 1", "photoAlt": "Uma longa mesa de jantar com toalhas brancas, posta para convidados no relvado de um estádio", "navJoin2": "Lista", "heroLabel": "Dia de jogo · Hospital
1484ity", "heroH2": "<em>Agentes de IA</em> para equipas de hospitalidade.", "tryT": "Gerir Hospitalidade", "phLive": "Dia de jogo", "phTable": "Mesa 01", "phGuests": "12 convidados", "phTime": "19:30 · Junto ao relvado", "hb1": "Lembra-se de cada convidado", "hb2": "Responde em qualquer app de chat", "hb3": "Liberta a equipa para os momentos", "lblPlay": "02 · Experimente um dia de jogo", "bridge2": "No dia de jogo, tudo acontece ao mesmo tempo. Vai conseguir aguentar?", "startLbl": "O seu turno · Começa em 20 min", "lblWhy": "03 · Porquê agora", "lblStack": "04 · A plataforma", "h2stack": "Uma plataforma. Todos os convidados.", "r1l": "Bilhética", "r1h": "Venda a experiência, não o lugar.", "r1p": "Pacotes com a sua marca, extras e bilhetes que os patrocinadores passam aos convidados.", "r2l": "Concierge", "r2h": "Um concierge com IA em qualquer app de chat.", "r2p": "Responde de imediato e passa à sua equipa quando importa.", "r3l": "Uma só fila", "r3h": "Todos os pedidos num só lugar.", "r3p": "Toda a equipa vê o que é preciso fazer, em tempo real.", "r4l": "Perfis de convidados", "r4p": "Preferências e histórico acompanham cada convidado, com o seu consentimento.", "r5l": "Inteligência", "r5p": "Encaminhamento, reservas e o próximo gesto atencioso, sugerido.", "lblFor": "05 · Feito para", "lblJoin": "06 · Parceiros de lançamento", "pk1": "Jantar no relvado", "pk1s": "12 convidados", "pk2": "Camarote executivo", "pk2s": "Época", "pk3": "Passe lounge", "pk3s": "+ Motorista", "qHead": "Pedidos", "pfv": "Sem glúten · Mesa calma · Vem de carro", "pc1": "Vou chegar atrasado. Como chego mais depressa ao recinto?", "pc2": "A opção mais rápida é um transfer oficial do evento, que posso reservar para garantir uma chegada tranquila e a horas.", "pc3": "Se preferir, posso pô-lo já em contacto com um assistente.", "h2plat": "Enquanto tudo acontece, a Outgoing trata do resto.", "sh1": "Hub de patrocinadores", "sh1b": "Época · 40 bilhetes", "stSent": "Enviado", "stPend": "Pendente", "prog": "32 de 40 atribuídos", "k1h": "Hub de patrocinadores", "k1p": "Os patrocinadores têm o seu próprio espaço para distribuir bilhetes pelos convidados e colegas. Deixa de ter de andar atrás de listas.", "pc4": "O seu transfer está reservado para as 18:40. A equipa já foi avisada.", "f1": "Lista de convidados atualizada", "f1s": "Gestor de patrocínio", "f2": "3 menus sem glúten confirmados", "f2s": "Catering", "k2h": "Gestor de comunicação", "k2p": "Um agente de IA trata de todas as conversas entre convidados, gestores de patrocínio e fornecedores como o catering, e regista as preferências de cada convidado. Tem tudo num só lugar.", "sh3": "Base de conhecimento", "sh3b": "24 documentos", "d1": "Guia do dia de jogo", "d2": "Protocolo VIP", "d3": "Catering e fornecedores", "d4": "Contratos de patrocínio 2026", "k3h": "Base de conhecimento", "k3p": "Todos os documentos e o know-how de hospitality num só lugar. Quando alguém novo entra na equipa, nada se perde.", "sh4b": "Gosto pessoal", "tn": "Ténis", "phone": "Telefone", "offerL": "Oferta exclusiva", "offer": "Acesso antecipado exclusivo: jantar junto ao relvado na final, com menu sem glúten.", "k4h": "Follow-ups personalizados", "k4p": "Patrocinadores e os adeptos mais apaixonados recebem ofertas exclusivas com base no seu gosto pessoal, no canal que preferirem: email, WhatsApp ou telefone.", "kick2": "O coração da Outgoing", "k2pL": "Um agente de IA trata de todas as conversas entre convidados, gestores de patrocínio e fornecedores como o catering. Responde, reserva e encaminha pedidos, assinala o que precisa de uma pessoa e regista as preferências de cada convidado. A sua equipa acompanha um só feed em vez de dez caixas de entrada.", "w1": "Convidados", "w2": "Gestores de patrocínio", "w3": "Catering", "w4": "Fornecedores", "kick4": "Trazê-los de volta", "k4pL": "Cada evento revela-nos mais sobre cada convidado: o que come, onde gosta de se sentar, os jogos que nunca perde. Patrocinadores e adeptos mais fiéis recebem depois ofertas exclusivas de acordo com as suas preferências, no canal que preferirem."};
1485let LANG = 'en';
1486const tr = (en, pt) => (LANG === 'pt' ? pt : en);
1487function applyLang(l) {
1488  LANG = l === 'pt' ? 'pt' : 'en';
1489  document.documentElement.lang = LANG === 'pt' ? 'pt-PT' : 'en';
1490  document.querySelectorAll('[data-i18n]').forEach(el => {
1491    if (el.dataset.en == null) el.dataset.en = el.innerHTML;
1492    const v = LANG === 'pt' ? PT[el.dataset.i18n] : el.dataset.en;
1493    if (v != null) el.innerHTML = v;
1494  });
1495  document.querySelectorAll('[data-i18n-ph]').forEach(el => {
1496    if (el.dataset.enPh == null) el.dataset.enPh = el.placeholder;
1497    el.placeholder = LANG === 'pt' ? PT[el.dataset.i18nPh] : el.dataset.enPh;
1498  });
1499  document.querySelectorAll('[data-i18n-alt]').forEach(el => {
1500    if (el.dataset.enAlt == null) el.dataset.enAlt = el.alt;
1501    el.alt = LANG === 'pt' ? PT[el.dataset.i18nAlt] : el.dataset.enAlt;
1502  });
1503  document.querySelectorAll('.lang button').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.lang === LANG)));
1504  hud.mood = -9;
1505  if (S.mode === 'end') fillEnd(S.lastEnd);
1506  if (typeof splitHero === 'function') splitHero(heroShown);
1507  try { localStorage.setItem('outgoing-lang', LANG); } catch (_) {}
1508}
1509document.querySelectorAll('.lang button').forEach(b => b.addEventListener('click', () => applyLang(b.dataset.lang)));
1510
1511/* ---------- Hero lettering ---------- */
1512let heroShown = false;
1513function splitHero(animate) {
1514  const h = document.querySelector('.hero h1'); if (!h) return;
1515  const frag = document.createDocumentFragment(); let i = 0;
1516  const word = (txt, em) => {
1517    const o = document.createElement('span'); o.className = 'w';
1518    const n = document.createElement('span'); n.className = 'wi'; n.style.transitionDelay = (i++ * 60) + 'ms';
1519    if (em) { const e = document.createElement('em'); e.textContent = txt; n.appendChild(e); } else n.textContent = txt;
1520    o.appendChild(n); return o;
1521  };
1522  [...h.childNodes].forEach(node => {
1523    const isEm = node.nodeType === 1 && node.tagName === 'EM';
1524    (node.textContent || '').split(/(\s+)/).forEach(part => {
1525      if (!part) return;
1526      if (/^\s+$/.test(part)) frag.appendChild(document.createTextNode(' ')); else frag.appendChild(word(part, isEm));
1527    });
1528  });
1529  h.innerHTML = ''; h.appendChild(frag);
1530  setupDotWords(); if (heroShown) playDotWords();
1531  if (animate) { h.classList.add('pre'); void h.offsetWidth; requestAnimationFrame(() => requestAnimationFrame(() => h.classList.remove('pre'))); }
1532}
1533function revealHero() {
1534  if (heroShown) return; heroShown = true;
1535  const h = document.querySelector('.hero h1'), c = document.querySelector('.hero-copy');
1536  requestAnimationFrame(() => { if (h) h.classList.remove('pre'); if (c) c.classList.remove('pre'); });
1537  playDotWords();
1538}
1539
1540/* Highlighted words drawn in the blue-dot style */
1541const dotWords = [];
1542const dwReduce = window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches;
1543const dwHash = (x, y, s) => { let h = (x * 374761393 + y * 668265263 + s * 2147483647) | 0; h = (h ^ (h >>> 13)) * 1274126177; return ((h ^ (h >>> 16)) >>> 0) / 4294967296; };
1544function setupDotWords() {
1545  dotWords.length = 0;
1546  document.querySelectorAll('.hero h1 em').forEach(em => {
1547    const cv = document.createElement('canvas'); cv.className = 'emdots'; cv.setAttribute('aria-hidden', 'true');
1548    em.classList.add('dotted'); em.appendChild(cv);
1549    const d = { em, cv, ctx: cv.getContext('2d'), m: document.createElement('canvas'), shown: false, tick: 0, playing: false };
1550    d.mc = d.m.getContext('2d', { willReadFrequently: true });
1551    d.em.addEventListener('mouseenter', () => { if (!dwReduce && d.shown && !d.playing) playWord(d); });
1552    dotWords.push(d);
1553  });
1554  layoutDotWords();
1555}
1556function layoutDotWords() {
1557  dotWords.forEach(d => {
1558    const cs = getComputedStyle(d.em), fs = parseFloat(cs.fontSize), pad = 0;
1559    d.fs = fs; d.pad = pad; d.w = d.em.offsetWidth + pad * 2; d.h = d.em.offsetHeight + pad * 2;
1560    d.dpr = Math.min(2, window.devicePixelRatio || 1);
1561    Object.assign(d.cv.style, { left: -pad + 'px', top: -pad + 'px', width: d.w + 'px', height: d.h + 'px' });
1562    d.cv.width = Math.round(d.w * d.dpr); d.cv.height = Math.round(d.h * d.dpr);
1563    d.font = cs.fontStyle + ' 400 ' + fs + 'px ' + cs.fontFamily; d.ls = cs.letterSpacing; d.text = d.em.textContent;
1564    d.fine = Math.max(2.2, fs / 21);
1565    wordFrame(d, d.shown ? d.fine : fs / 2.6);
1566  });
1567}
1568function wordFrame(d, cell) {
1569  const gw = Math.max(1, Math.ceil(d.w / cell)), gh = Math.max(1, Math.ceil(d.h / cell));
1570  d.m.width = gw; d.m.height = gh; const c = d.mc;
1571  c.setTransform(1 / cell, 0, 0, 1 / cell, 0, 0); c.clearRect(0, 0, d.w, d.h);
1572  c.font = d.font; try { if (d.ls && d.ls !== 'normal') c.letterSpacing = d.ls; } catch (_) {}
1573  c.textBaseline = 'alphabetic'; c.fillStyle = '#0A7AFF';
1574  const mtx = c.measureText(d.text), asc = mtx.fontBoundingBoxAscent || d.fs * .82, desc = mtx.fontBoundingBoxDescent || d.fs * .22;
1575  const top = d.pad + Math.max(0, (d.h - d.pad * 2 - asc - desc) / 2);
1576  const inset = d.fs * .03, avail = d.w - inset * 2, sx = Math.min(1, avail / Math.max(1, mtx.width)), x0 = inset + (avail - mtx.width * sx) / 2;
1577  c.setTransform(sx / cell, 0, 0, 1 / cell, x0 / cell, 0); c.fillText(d.text, 0, top + asc);
1578  d.data = c.getImageData(0, 0, gw, gh).data; d.gw = gw; d.gh = gh; d.cell = cell; paintWord(d);
1579}
1580function paintWord(d) {
1581  const x = d.ctx, cs = d.cell, gap = Math.max(.6, cs * .2), D = d.data;
1582  x.setTransform(d.dpr, 0, 0, d.dpr, 0, 0); x.clearRect(0, 0, d.w, d.h); x.fillStyle = '#0A7AFF';
1583  for (let gy = 0; gy < d.gh; gy++) for (let gx = 0; gx < d.gw; gx++) {
1584    const a = D[(gy * d.gw + gx) * 4 + 3] / 255; if (a < .12) continue;
1585    const px = gx * cs, py = gy * cs, flick = ((gx * 7 + gy * 13) % 29) === (d.tick % 29) ? d.tick : 0, h = dwHash(gx, gy, flick);
1586    if (a > .72) {
1587      if (h < .05 && cs > 4) { x.fillRect(px, py, cs - gap, cs - gap); x.fillStyle = '#F7F9FC'; x.beginPath(); x.arc(px + (cs - gap) / 2, py + (cs - gap) / 2, cs * .22, 0, 6.283); x.fill(); x.fillStyle = '#0A7AFF'; }
1588      else if (h < .1) { x.beginPath(); x.arc(px + cs / 2, py + cs / 2, cs * .5, 0, 6.283); x.fill(); }
1589      else x.fillRect(px, py, cs - gap, cs - gap);
1590    } else if (a > .5) x.fillRect(px, py, cs - gap, cs - gap);
1591    else if (a > .35) { x.beginPath(); x.arc(px + cs / 2, py + cs / 2, cs * .4, 0, 6.283); x.fill(); }
1592    else if (a > .22) { const q = cs * .38; x.fillRect(px + (cs - q) / 2, py + (cs - q) / 2, q, q); }
1593  }
1594}
1595function playWord(d, delay) {
1596  if (dwReduce) { d.shown = true; wordFrame(d, d.fine); return; }
1597  d.playing = true; const steps = [d.fs / 2.6, d.fs / 4, d.fs / 6.5, d.fs / 10, d.fine];
1598  steps.forEach((cell, i) => setTimeout(() => { wordFrame(d, Math.max(cell, d.fine)); if (i === steps.length - 1) { d.playing = false; d.shown = true; } }, (delay || 0) + i * 190));
1599}
1600function playDotWords() { dotWords.forEach((d, i) => playWord(d, 250 + i * 260)); }
1601setInterval(() => { if (document.hidden || dwReduce) return; dotWords.forEach(d => { if (d.shown && !d.playing && d.data) { d.tick++; paintWord(d); } }); }, 650);
1602window.addEventListener('resize', () => { clearTimeout(window.__dwT); window.__dwT = setTimeout(layoutDotWords, 150); });
1603if (document.fonts && document.fonts.ready) document.fonts.ready.then(() => layoutDotWords()).catch(() => {});
1604
1605/* ---------- Loader ----------
1606   The logo resolves from coarse blue dots to fine ones (same treatment as the hero's highlighted
1607   words) and stays as dots until the loader lifts. */
1608(function () {
1609  const L = document.getElementById('loader'); if (!L) return;
1610  L.classList.add('js');
1611  const t0 = performance.now(); let done = false;
1612  const reduce = window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches;
1613  const mark = L.querySelector('.ld-mark'), cv = L.querySelector('.ld-dots');
1614
1615  const paths = [...document.querySelectorAll('#og-logo path')].map(el => new Path2D(el.getAttribute('d')));
1616  function dots(cell) {
1617    const w = mark.offsetWidth, h = mark.offsetHeight, dpr = Math.min(2, window.devicePixelRatio || 1);
1618    if (!w || !paths.length) return;
1619    cv.width = Math.round(w * dpr); cv.height = Math.round(h * dpr);
1620    const gw = Math.ceil(w / cell), gh = Math.ceil(h / cell), m = document.createElement('canvas'); m.width = gw; m.height = gh;
1621    const mc = m.getContext('2d', { willReadFrequently: true }), k = w / 774.7 / cell;
1622    mc.setTransform(k, 0, 0, k, -152.7 * k, -274.7 * k); paths.forEach(pa => mc.fill(pa));
1623    const D = mc.getImageData(0, 0, gw, gh).data, x = cv.getContext('2d'), gap = Math.max(.6, cell * .2);
1624    x.setTransform(dpr, 0, 0, dpr, 0, 0); x.clearRect(0, 0, w, h); x.fillStyle = '#0A7AFF';
1625    for (let gy = 0; gy < gh; gy++) for (let gx = 0; gx < gw; gx++) {
1626      const a = D[(gy * gw + gx) * 4 + 3] / 255; if (a < .25) continue;
1627      if (a > .6) x.fillRect(gx * cell, gy * cell, cell - gap, cell - gap);
1628      else { x.beginPath(); x.arc(gx * cell + cell / 2, gy * cell + cell / 2, cell * .32, 0, 6.283); x.fill(); }
1629    }
1630  }
1631  const h = mark.offsetHeight || 41;
1632  if (reduce) dots(h / 14);
1633  else [h / 2.2, h / 3.5, h / 5.5, h / 9, h / 14].forEach((cell, i) => setTimeout(() => dots(cell), 120 + i * 150));
1634
1635  function hide() {
1636    if (done) return; done = true;
1637    const wait = Math.max(0, 1400 - (performance.now() - t0));
1638    setTimeout(() => { L.classList.add('out'); setTimeout(revealHero, 300); setTimeout(() => L.remove(), 950); }, wait);
1639  }
1640  const ready = new Promise(r => (document.readyState === 'complete' ? r() : window.addEventListener('load', r, { once: true })));
1641  const fonts = document.fonts && document.fonts.ready ? document.fonts.ready.catch(() => {}) : Promise.resolve();
1642  Promise.all([ready, fonts]).then(hide);
1643  setTimeout(hide, 3500);
1644})();
1645
1646/* ---------- Platform rows: active row + matching visual ---------- */
1647document.querySelectorAll('.srow').forEach(r => {
1648  const vp = document.querySelector('.vp[data-v="' + r.dataset.v + '"]'), m = r.querySelector('.mob');
1649  if (vp && m) m.innerHTML = vp.innerHTML.replace(/id="dg"/g, 'id="dg' + r.dataset.v + '"').replace(/url\(#dg\)/g, 'url(#dg' + r.dataset.v + ')');
1650});
1651try {
1652  const rowObs = new IntersectionObserver(es => {
1653    es.forEach(e => {
1654      if (!e.isIntersecting) return;
1655      const v = e.target.dataset.v;
1656      document.querySelectorAll('.srow').forEach(r => r.classList.toggle('on', r.dataset.v === v));
1657      document.querySelectorAll('.vp').forEach(p => p.classList.toggle('on', p.dataset.v === v));
1658    });
1659  }, { rootMargin: '-45% 0px -45% 0px' });
1660  document.querySelectorAll('.srow').forEach(r => rowObs.observe(r));
1661} catch (_) {}
1662
1663
1664/* ---------- Dot illustrations (cards) ---------- */
1665(function () {
1666  const BLUE = '#0A7AFF', GREEN = '#12B886';
1667  const ease = t => (t <= 0 ? 0 : t >= 1 ? 1 : 1 - Math.pow(1 - t, 3));
1668  const seg = (t, a, b) => ease((t - a) / (b - a));
1669  const rr = (c, x, y, w, h, r) => { c.beginPath(); if (c.roundRect) c.roundRect(x, y, w, h, r); else c.rect(x, y, w, h); c.fill(); };
1670  const circ = (c, x, y, r) => { c.beginPath(); c.arc(x, y, r, 0, Math.PI * 2); c.fill(); };
1671  const cut = (c, fn) => { c.save(); c.globalCompositeOperation = 'destination-out'; c.fillStyle = '#000'; c.strokeStyle = '#000'; fn(); c.restore(); };
1672  function person(c, x, y, s, col) {
1673    c.fillStyle = col; circ(c, x, y - 30 * s, 14 * s);
1674    c.beginPath(); c.moveTo(x - 24 * s, y + 28 * s); c.quadraticCurveTo(x - 24 * s, y - 8 * s, x, y - 10 * s); c.quadraticCurveTo(x + 24 * s, y - 8 * s, x + 24 * s, y + 28 * s); c.closePath(); c.fill();
1675  }
1676  function ticket(c, x, y, w, h, col) {
1677    c.fillStyle = col; rr(c, x, y, w, h, Math.min(w, h) * .08);
1678    cut(c, () => { circ(c, x, y + h / 2, h * .2); circ(c, x + w, y + h / 2, h * .2); for (let i = 0; i < 3; i++) rr(c, x + w * .68, y + h * .1 + i * h * .3, Math.max(3, w * .055), h * .18, 0); });
1679  }
1680  function bubble(c, x, y, w, h, col, left, done) {
1681    c.fillStyle = col; rr(c, x, y, w, h, 12);
1682    c.beginPath(); if (left) { c.moveTo(x + w - 18, y + h - 2); c.lineTo(x + w + 6, y + h + 12); c.lineTo(x + w - 4, y + h - 10); } else { c.moveTo(x + 18, y + h - 2); c.lineTo(x - 6, y + h + 12); c.lineTo(x + 4, y + h - 10); } c.closePath(); c.fill();
1683    cut(c, () => {
1684      if (done) { c.lineWidth = 9; c.lineCap = 'round'; c.lineJoin = 'round'; c.beginPath(); c.moveTo(x + w * .3, y + h * .52); c.lineTo(x + w * .45, y + h * .7); c.lineTo(x + w * .72, y + h * .3); c.stroke(); }
1685      else for (let i = 0; i < 3; i++) circ(c, x + w * (.28 + i * .22), y + h / 2, 7);
1686    });
1687  }
1688  const dots = (c, x1, y1, x2, y2, p, gap, r) => { const L = Math.hypot(x2 - x1, y2 - y1), n = Math.floor(L / gap); for (let i = 0; i <= n; i++) { const f = i / Math.max(1, n); if (f > p + 1e-6) break; circ(c, x1 + (x2 - x1) * f, y1 + (y2 - y1) * f, r); } };
1689  function drone(c, cx, cy, size, col) {
1690    const k = size / O_W; c.save(); c.translate(cx, cy); c.scale(k, k); c.translate(-O_CX, -O_CY); c.fillStyle = col; c.fill(O_PATH, 'evenodd'); c.restore();
1691    c.fillStyle = GREEN; c.beginPath(); c.ellipse(cx + (EYE_X - O_CX) * k, cy + (EYE_Y - O_CY) * k, size * .045, size * .085, 0, 0, Math.PI * 2); c.fill();
1692  }
1693  function check(c, x, y, r, col) {
1694    c.fillStyle = col; circ(c, x, y, r);
1695    cut(c, () => { c.lineWidth = r * .38; c.lineCap = 'round'; c.lineJoin = 'round'; c.beginPath(); c.moveTo(x - r * .42, y + r * .02); c.lineTo(x - r * .1, y + r * .34); c.lineTo(x + r * .46, y - r * .3); c.stroke(); });
1696  }
1697  function folder(c, x, y, w, h, col) {
1698    cut(c, () => { rr(c, x - 5, y - 5, w + 10, h + 10, 8); });
1699    c.fillStyle = col; rr(c, x, y, w * .42, 16, 4); rr(c, x, y + 9, w, h - 9, 6);
1700  }
1701  function envelope(c, x, y, w, h, col) {
1702    c.fillStyle = col; rr(c, x, y, w, h, 5);
1703    cut(c, () => { c.lineWidth = Math.max(4, w * .08); c.lineJoin = 'round'; c.beginPath(); c.moveTo(x + 5, y + 6); c.lineTo(x + w / 2, y + h * .58); c.lineTo(x + w - 5, y + 6); c.stroke(); });
1704  }
1705  const SCENES = {
1706    comms: { dur: 7.9, draw(c, t) {
1707      const HX = 240, HY = 130, MX = 352, MY = 130;
1708      const B = [226, 203, 180, 157, 134].map(d => { const an = d * Math.PI / 180, cx = HX + Math.cos(an) * 196, cy = HY - 4 + Math.sin(an) * 150; return { x: cx - 26, y: cy - 13, w: 52, h: 26 }; });
1709      // [box, start, routed to a person]
1710      const EV = [[0, .1, 0], [1, .6, 1], [2, 1.1, 0], [3, 1.6, 0], [4, 2.0, 0], [0, 2.4, 0], [2, 2.9, 0], [1, 3.4, 0], [3, 3.9, 1], [4, 4.3, 0], [0, 4.6, 1], [2, 5.2, 0], [4, 5.6, 0]];
1711      const lerp = (a, b, p) => [a[0] + (b[0] - a[0]) * p, a[1] + (b[1] - a[1]) * p];
1712      const HO = [HX + 42, HY], MN = [MX - 26, MY];
1713      const geo = B.map(b => { const cx = b.x + b.w / 2, cy = b.y + b.h / 2, dx = HX - cx, dy = HY - cy, L = Math.hypot(dx, dy); return { m: [cx + dx / L * 32, cy + dy / L * 20], H: [HX - dx / L * 40, HY - dy / L * 34] }; });
1714      const first = B.map((_, i) => Math.min(...EV.filter(e => e[0] === i).map(e => e[1])));
1715      const pending = B.map(() => false), answered = B.map(() => false), dotsNow = [];
1716      let manOn = false;
1717      c.fillStyle = BLUE; dots(c, HO[0], HO[1], MN[0], MN[1], 1, 12, 4.4);
1718      EV.forEach(([i, a, r]) => {
1719        if (t < a) return; const { m, H } = geo[i], end = a + (r ? 3.05 : 1.6);
1720        if (t < end) pending[i] = true; else answered[i] = true;
1721        if (!r) { const p1 = seg(t, a + .25, a + .85), p2 = seg(t, a + 1.0, a + 1.6);
1722          if (p1 > 0 && p1 < 1) dotsNow.push([lerp(m, H, p1), BLUE]); else if (p2 > 0 && p2 < 1) dotsNow.push([lerp(H, m, p2), GREEN]); }
1723        else { const p1 = seg(t, a + .25, a + .85), p2 = seg(t, a + .95, a + 1.4), p3 = seg(t, a + 1.9, a + 2.35), p4 = seg(t, a + 2.45, a + 3.05);
1724          if (t >= a + 1.4 && t < a + 2.35) manOn = true;
1725          if (p1 > 0 && p1 < 1) dotsNow.push([lerp(m, H, p1), BLUE]); else if (p2 > 0 && p2 < 1) dotsNow.push([lerp(HO, MN, p2), BLUE]);
1726          else if (p3 > 0 && p3 < 1) dotsNow.push([lerp(MN, HO, p3), GREEN]); else if (p4 > 0 && p4 < 1) dotsNow.push([lerp(H, m, p4), GREEN]); }
1727      });
1728      B.forEach((b, i) => {
1729        const a = first[i], pop = seg(t, a, a + .25); if (pop <= 0) return;
1730        const { m, H } = geo[i]; c.fillStyle = BLUE; dots(c, m[0], m[1], H[0], H[1], seg(t, a + .1, a + .4), 12, 4.4);
1731        const green = answered[i] && !pending[i];
1732        const k = .4 + .6 * pop, w = b.w * k, h = b.h * k, x = b.x + (b.w - w) / 2, y = b.y + (b.h - h) / 2;
1733        c.fillStyle = green ? GREEN : BLUE; rr(c, x, y, w, h, 8);
1734        c.beginPath(); c.moveTo(x + 10 * k, y + h - 1); c.lineTo(x + 2 * k, y + h + 9 * k); c.lineTo(x + 20 * k, y + h - 1); c.closePath(); c.fill();
1735        if (pop >= 1) cut(c, () => { if (green) { c.lineWidth = 6; c.lineCap = 'round'; c.lineJoin = 'round'; c.beginPath(); c.moveTo(x + w * .34, y + h * .52); c.lineTo(x + w * .46, y + h * .72); c.lineTo(x + w * .68, y + h * .3); c.stroke(); } else for (let j = 0; j < 3; j++) circ(c, x + w * (.3 + j * .2), y + h / 2, 4.5); });
1736      });
1737      dotsNow.forEach(([pt, col]) => { c.fillStyle = col; circ(c, pt[0], pt[1], 8); });
1738      drone(c, HX, HY, 72, BLUE);
1739      person(c, MX + 4, MY + 4, .85, manOn ? GREEN : BLUE);
1740    } },
1741    hub: { dur: 5.0, draw(c, t) {
1742      t = t / 1.8;
1743      const b = seg(t, 0, .3);
1744      if (b > 0) { const w = 64 * b, h = 58 * b, x = 200 - w / 2, y = 8 + (58 - h); c.fillStyle = BLUE; rr(c, x, y, w, h, 4); rr(c, 200 - 40 * b, 62, 80 * b, 8, 2);
1745        if (b >= 1) cut(c, () => { for (let r = 0; r < 2; r++) for (let q = 0; q < 3; q++) rr(c, 178 + q * 16, 20 + r * 18, 9, 10, 1); rr(c, 194, 50, 12, 16, 1); }); }
1746      c.fillStyle = BLUE; dots(c, 200, 76, 200, 92, seg(t, .3, .45), 10, 4.4);
1747      const m = seg(t, .45, .7); if (m > 0) person(c, 200, 124, .82 * m, BLUE);
1748      const bar = seg(t, .8, 1.2); c.fillStyle = BLUE;
1749      dots(c, 200, 154, 200, 172, seg(t, .7, .8), 10, 4.4);
1750      dots(c, 200, 176, 40, 176, bar, 11, 4.4); dots(c, 200, 176, 360, 176, bar, 11, 4.4);
1751      [40, 120, 200, 280, 360].forEach((gx, i) => {
1752        c.fillStyle = BLUE; dots(c, gx, 176, gx, 192, seg(t, 1.1, 1.25), 9, 4.4);
1753        const a = 1.3 + i * .25, lit = t >= a + .25, g = seg(t, 1.1, 1.35);
1754        if (g > 0) person(c, gx, 226, .74 * g, lit ? GREEN : BLUE);
1755        const p = seg(t, a, a + .25); if (p > 0 && p < 1) { c.fillStyle = GREEN; circ(c, 200 + (gx - 200) * p, 176, 7); }
1756      });
1757    } },
1758    kb: { dur: 3.2, draw(c, t) {
1759      const F = [[26, 22], [164, 4], [302, 28], [4, 104], [160, 102], [318, 112], [30, 190], [166, 204], [296, 196]];
1760      const conv = seg(t, 1.3, 2.1), CX = 200, CY = 130;
1761      F.forEach(([fx, fy], i) => {
1762        if (conv >= 1) return;
1763        const bob = Math.sin(t * 3.2 + i * 1.7) * 7 * (1 - conv), sc = 1 - .55 * conv;
1764        const x0 = fx + 33, y0 = fy + 24 + bob, x = x0 + (CX - x0) * conv, y = y0 + (CY - y0) * conv;
1765        c.save(); c.globalAlpha = 1 - conv * .6; folder(c, x - 33 * sc, y - 24 * sc, 66 * sc, 48 * sc, BLUE); c.restore();
1766      });
1767      const g = seg(t, 1.95, 2.35);
1768      if (g > 0) {
1769        const glow = seg(t, 2.2, 3.0);
1770        if (glow > 0) {
1771          c.save(); c.fillStyle = GREEN;
1772          [[86, .55], [112, .32]].forEach(([R, al]) => { c.globalAlpha = al * glow; for (let d = 0; d < 360; d += 10) { const an = d * Math.PI / 180; circ(c, CX + Math.cos(an) * R * (.8 + .2 * glow), CY + Math.sin(an) * R * .82 * (.8 + .2 * glow), 4.6); } });
1773          c.restore();
1774          c.fillStyle = GREEN; [[CX + 76, CY - 62], [CX - 84, CY + 54], [CX + 92, CY + 44]].forEach(([sx, sy], j) => { const k = glow * (j === 0 ? 1 : .7); c.beginPath(); c.moveTo(sx, sy - 14 * k); c.lineTo(sx + 4 * k, sy - 4 * k); c.lineTo(sx + 14 * k, sy); c.lineTo(sx + 4 * k, sy + 4 * k); c.lineTo(sx, sy + 14 * k); c.lineTo(sx - 4 * k, sy + 4 * k); c.lineTo(sx - 14 * k, sy); c.lineTo(sx - 4 * k, sy - 4 * k); c.closePath(); c.fill(); });
1775        }
1776        const w = 112 * (.6 + .4 * g), h = 82 * (.6 + .4 * g);
1777        folder(c, CX - w / 2, CY - h / 2, w, h, GREEN);
1778        cut(c, () => { rr(c, CX - w * .28, CY + h * .02, w * .56, 7, 2); rr(c, CX - w * .28, CY + h * .2, w * .36, 7, 2); });
1779      }
1780    } },
1781    fu: { dur: 2.9, draw(c, t) {
1782      const CX = 200, CY = 132, R = 104, ang = d => d * Math.PI / 180, P = d => [CX + R * Math.cos(ang(d)), CY + R * Math.sin(ang(d))];
1783      c.fillStyle = BLUE; for (let d = 0; d < 360; d += 9) { const [x, y] = P(d); circ(c, x, y, 4.2); }
1784      const icons = [-150, -90, -30];
1785      icons.forEach(d => { const [x, y] = P(d); cut(c, () => circ(c, x, y, 30)); });
1786      const landed = t >= 2.55;
1787      person(c, CX, CY + 18, 1.05, landed ? GREEN : BLUE);
1788      const s = [seg(t, .1, .4), seg(t, .45, .75), seg(t, .8, 1.1)];
1789      c.fillStyle = BLUE;
1790      if (s[0] > 0) { const [x, y] = P(-150), k = s[0]; circ(c, x, y, 24 * k); cut(c, () => circ(c, x, y, 15 * k)); c.fillStyle = BLUE; circ(c, x, y, 8 * k); }
1791      if (s[1] > 0) { const [x, y] = P(-90), k = s[1] * .82; c.fillStyle = BLUE; rr(c, x - 18 * k, y - 26 * k, 36 * k, 24 * k, 5); rr(c, x - 22 * k, y + 2 * k, 44 * k, 10 * k, 3); rr(c, x - 18 * k, y + 12 * k, 8 * k, 16 * k, 2); rr(c, x + 10 * k, y + 12 * k, 8 * k, 16 * k, 2); }
1792      if (s[2] > 0) { const [x, y] = P(-30), k = s[2]; c.fillStyle = BLUE; circ(c, x, y, 22 * k); cut(c, () => { c.lineWidth = 5 * k; c.beginPath(); c.arc(x - 29 * k, y, 25 * k, -.75, .75); c.stroke(); c.beginPath(); c.arc(x + 29 * k, y, 25 * k, Math.PI - .75, Math.PI + .75); c.stroke(); }); }
1793      const e = seg(t, 1.35, 2.55);
1794      if (e > 0) { const [x, y] = P(-30 + 120 * e); cut(c, () => circ(c, x, y, 34)); envelope(c, x - 30, y - 20, 60, 40, landed ? GREEN : BLUE); }
1795    } }
1796  };
1797  const hash = (x, y, s) => { let h = (x * 374761393 + y * 668265263 + s * 2147483647) | 0; h = (h ^ (h >>> 13)) * 1274126177; return ((h ^ (h >>> 16)) >>> 0) / 4294967296; };
1798  const reduce = window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches;
1799  const RES = [30, 22, 16, 12];
1800  class Dots {
1801    constructor(cv) {
1802      this.cv = cv; this.sc = SCENES[cv.dataset.scene]; this.ctx = cv.getContext('2d');
1803      this.m = document.createElement('canvas'); this.mc = this.m.getContext('2d', { willReadFrequently: true });
1804      this.playing = false; this.tick = 0; this.size();
1805    }
1806    size() {
1807      const r = this.cv.getBoundingClientRect(); this.w = r.width; this.h = r.height; this.dpr = Math.min(2, window.devicePixelRatio || 1);
1808      this.cv.width = Math.round(this.w * this.dpr); this.cv.height = Math.round(this.h * this.dpr);
1809      this.fine = this.w < 420 ? 6 : 8;
1810    }
1811    bbox() {
1812      const sc = this.sc; if (sc._bb) return sc._bb;
1813      const m = document.createElement('canvas'); m.width = 400; m.height = 260; const c = m.getContext('2d', { willReadFrequently: true });
1814      let x0 = 400, y0 = 260, x1 = 0, y1 = 0; const ts = [99]; for (let q = 0; q <= sc.dur; q += .2) ts.push(q);
1815      ts.forEach(tt => { c.clearRect(0, 0, 400, 260); sc.draw(c, tt); const d = c.getImageData(0, 0, 400, 260).data;
1816        for (let y = 0; y < 260; y += 2) for (let x = 0; x < 400; x += 2) if (d[(y * 400 + x) * 4 + 3] > 30) { if (x < x0) x0 = x; if (x > x1) x1 = x; if (y < y0) y0 = y; if (y > y1) y1 = y; } });
1817      if (sc.labels) sc.labels().forEach(([lx, ly, txt]) => { const hw = txt.length * 4; x0 = Math.min(x0, lx - hw); x1 = Math.max(x1, lx + hw); y0 = Math.min(y0, ly - 7); y1 = Math.max(y1, ly + 7); });
1818      if (x1 < x0) { x0 = 0; y0 = 0; x1 = 400; y1 = 260; }
1819      return (sc._bb = [x0, y0, x1, y1]);
1820    }
1821    sample(cs, t) {
1822      const gw = Math.ceil(this.w / cs), gh = Math.ceil(this.h / cs); if (!gw || !gh) return;
1823      this.m.width = gw; this.m.height = gh; const c = this.mc;
1824      c.setTransform(1, 0, 0, 1, 0, 0); c.clearRect(0, 0, gw, gh);
1825      const bb = this.bbox(), base = Math.min(this.w / 400, this.h / 260), k = Math.min(base * 1.6, Math.min(this.w / (bb[2] - bb[0] + 16), this.h / (bb[3] - bb[1] + 16))), ox = this.w / 2 - (bb[0] + bb[2]) / 2 * k, oy = this.h / 2 - (bb[1] + bb[3]) / 2 * k;
1826      c.setTransform(k / cs, 0, 0, k / cs, ox / cs, oy / cs); this.sc.draw(c, t); this.k = k; this.ox = ox; this.oy = oy;
1827      this.data = c.getImageData(0, 0, gw, gh).data; this.gw = gw; this.gh = gh; this.cs = cs;
1828    }
1829    paint() {
1830      const x = this.ctx, d = this.data; if (!d) return; const cs = this.cs, gap = Math.max(1, cs * .14);
1831      x.setTransform(this.dpr, 0, 0, this.dpr, 0, 0); x.clearRect(0, 0, this.w, this.h);
1832      for (let gy = 0; gy < this.gh; gy++) for (let gx = 0; gx < this.gw; gx++) {
1833        const i = (gy * this.gw + gx) * 4, a = d[i + 3] / 255; if (a < .12) continue;
1834        const col = d[i + 1] > d[i + 2] ? GREEN : BLUE, px = gx * cs, py = gy * cs;
1835        const flick = ((gx * 7 + gy * 13) % 47) === (this.tick % 47) ? this.tick : 0, h = hash(gx, gy, flick);
1836        x.fillStyle = col;
1837        if (a > .72) {
1838          if (h < .06) { x.fillRect(px, py, cs - gap, cs - gap); x.fillStyle = '#FFFFFF'; x.beginPath(); x.arc(px + (cs - gap) / 2, py + (cs - gap) / 2, cs * .22, 0, 6.283); x.fill(); }
1839          else if (h < .1) { x.beginPath(); x.arc(px + cs / 2, py + cs / 2, cs * .46, 0, 6.283); x.fill(); }
1840          else x.fillRect(px, py, cs - gap, cs - gap);
1841        } else if (a > .4) { x.beginPath(); x.arc(px + cs / 2, py + cs / 2, cs * .34, 0, 6.283); x.fill(); }
1842        else if (a > .22) { const q = cs * .34; x.fillRect(px + (cs - q) / 2, py + (cs - q) / 2, q, q); }
1843      }
1844      if (this.sc.labels && this.cs <= this.fine) {
1845        x.font = '500 ' + Math.max(10, Math.round(11 * this.k)) + 'px "Geist Mono", ui-monospace, monospace'; x.textAlign = 'center'; x.textBaseline = 'middle'; x.fillStyle = '#565C73';
1846        try { x.letterSpacing = '0.08em'; } catch (_) {}
1847        this.sc.labels().forEach(([lx, ly, txt]) => x.fillText(txt.toUpperCase(), this.ox + lx * this.k, this.oy + ly * this.k));
1848      }
1849    }
1850    still() { this.sample(this.fine, 99); this.paint(); this.shown = true; }
1851    play() {
1852      if (this.playing) return; if (reduce) { this.still(); return; }
1853      this.playing = true; this.shown = true; const t0 = performance.now(), resT = .9, D = this.sc.dur, run = this.run = (this.run || 0) + 1;
1854      const step = now => {
1855        if (run !== this.run) return;
1856        const e = (now - t0) / 1000;
1857        if (e < resT) { const cs = RES[Math.min(RES.length - 1, Math.floor(e / resT * RES.length))]; this.sample(Math.max(cs, this.fine), 0); }
1858        else this.sample(this.fine, e - resT);
1859        this.paint();
1860        if (e < resT + D + .1) requestAnimationFrame(step); else { this.playing = false; this.sample(this.fine, 99); this.paint(); }
1861      };
1862      requestAnimationFrame(step);
1863    }
1864    cancel() { this.run = (this.run || 0) + 1; this.playing = false; }
1865    rest() { if (this.playing || !this.shown || reduce) return; this.tick++; this.paint(); }
1866  }
1867  const all = [...document.querySelectorAll('canvas.dots')].map(cv => new Dots(cv));
1868  all.forEach(d => { d.sample(28, 0); d.paint(); });
1869  try {
1870    const io = new IntersectionObserver(es => es.forEach(e => { if (e.isIntersecting) { const d = all.find(z => z.cv === e.target); if (d && !d.shown) d.play(); } }), { threshold: .35 });
1871    all.forEach(d => { if (!d.cv.classList.contains('fm-cv')) io.observe(d.cv); });
1872  } catch (_) { all.forEach(d => d.still()); }
1873  all.forEach(d => { const card = d.cv.closest('.card, .fc'); if (card) card.addEventListener('mouseenter', () => { if (!reduce) { d.shown = false; d.play(); } }); });
1874  // expand a feature card into a modal with the full text
1875  (function () {
1876    const m = document.getElementById('fmodal'), cv = m && m.querySelector('.fm-cv'), fd = all.find(d => d.cv === cv), txt = document.getElementById('fmTxt');
1877    if (!m || !fd) return; let last = null;
1878    const close = () => { m.hidden = true; document.body.classList.remove('fm-open'); fd.cancel(); if (last) last.focus(); };
1879    document.querySelectorAll('.fc-exp').forEach(btn => btn.addEventListener('click', () => {
1880      last = btn; const full = btn.closest('.card').querySelector('.fc-full');
1881      txt.innerHTML = full.innerHTML; const h = txt.querySelector('h3'); if (h) h.id = 'fmTitle';
1882      m.hidden = false; document.body.classList.add('fm-open');
1883      fd.size(); fd.sc = SCENES[btn.dataset.scene]; fd.cancel(); fd.shown = false; reduce ? fd.still() : fd.play();
1884      document.getElementById('fmX').focus();
1885    }));
1886    document.getElementById('fmX').addEventListener('click', close);
1887    m.addEventListener('click', e => { if (e.target === m) close(); });
1888    document.addEventListener('keydown', e => { if (e.key === 'Escape' && !m.hidden) close(); });
1889  })();
1890  setInterval(() => { if (!document.hidden) all.forEach(d => d.rest()); }, 650);
1891  let rt = 0; window.addEventListener('resize', () => { clearTimeout(rt); rt = setTimeout(() => all.forEach(d => { d.size(); if (d.shown && !d.playing) { d.sample(d.fine, 99); d.paint(); } else { d.sample(28, 0); d.paint(); } }), 150); });
1892})();
1893
1894/* ---------- Pixel scenes (venues, teams, series) ---------- */
1895(function () {
1896  const GW = 96, GH = 60;
1897  const SEAT = ['..hhhh..', '.hhhhhh.', '.hsssss.', '.sesses.', '.ssssss.', '..ssss..', '...ss...', '.cwwwwc.', 'cccwwccc', 'cccttccc', 'cccccccc'];
1898  const SHIRT = ['..bbbw.wbbb..', '.bbbbbwbbbbb.', 'bbbbbbbbbbbbb', 'bbbbbbbbbbbbb', 'www.bbbbb.www', '...bbbbbbb...', '...bwbwwwbb..', '...bwbwbwbb..', '...bwbwbwbb..', '...bwbwbwbb..', '...bwbwwwbb..', '...bbbbbbb...', '...bbbbbbb...', '...bbbbbbb...'];
1899  function mk(cv) {
1900    const x = cv.getContext('2d'); x.imageSmoothingEnabled = false;
1901    const f = (a, b, w, h, c) => { x.fillStyle = c; x.fillRect(a, b, w, h); };
1902    const spr = (rows, a, b, pal, drop) =>
1902 rows.forEach((row, r) => [...row].forEach((ch, c) => { if (ch === '.' || (drop && drop.includes(ch))) return; const col = (pal && pal[ch]) || PAL[ch]; if (col) f(a + c, b + r, 1, 1, col); }));
1903    return { x, f, spr };
1904  }
1905  function crowd(f, r, x0, x1, y0, y1, base, alt) {
1906    const cols = ['#E8E3D9', '#2F7BFF', '#22D39A', '#F2C14E', '#D9644F', '#F5F3EE', '#3A4152'];
1907    for (let y = y0, i = 0; y < y1; y++, i++) { f(x0, y, x1 - x0, 1, i % 2 ? alt : base); for (let xx = x0; xx < x1; xx++) if (r() < .4) f(xx, y, 1, 1, cols[Math.floor(r() * cols.length)]); }
1908  }
1909  function flute(f, a, b, liq) { f(a, b, 2, 1, '#E9EDF3'); f(a, b + 1, 2, 3, liq); f(a, b + 4, 1, 1, '#D9DDE6'); f(a - 1, b + 5, 3, 1, '#D9DDE6'); }
1910  function tray(f, a, b, w) { f(a, b, w, 1, '#F5F3EE'); f(a + 1, b + 1, w - 2, 1, '#B8BFCC'); }
1911  function host(spr, f, a, b, items) { spr(MAN, a, b); tray(f, a + 11, b + 5, 11); items && items(a + 11, b + 5); }
1912
1913  const SCN = {
1914    venue(cv) {
1915      const { f, spr } = mk(cv), r = rng(5);
1916      ['#86BEE6', '#93C6EA', '#A2CFEE', '#B3D8F1', '#C6E2F4'].forEach((c, i) => f(0, i * 3, GW, 3, c));
1917      f(0, 12, GW, 1, '#3A4152'); crowd(f, r, 0, GW, 13, 21, '#3F5F4A', '#4A6B55'); f(0, 21, GW, 1, '#2F3546');
1918      for (let y = 22, i = 0; y < 36; y += 2, i++) f(0, y, GW, 2, i % 2 ? '#3E9B52' : '#48A75C');
1919      f(10, 24, 76, 1, '#F5F3EE'); f(10, 24, 1, 12, '#F5F3EE'); f(85, 24, 1, 12, '#F5F3EE'); f(20, 26, 56, 1, '#F5F3EE'); f(48, 26, 1, 3, '#F5F3EE');
1920      f(4, 29, 88, 1, '#FBFAF6'); for (let xx = 4; xx < 92; xx++) f(xx, 30, 1, 2, (xx % 2) ? '#2A2F3A' : '#3A4152'); f(3, 28, 1, 4, '#2A2F3A'); f(92, 28, 1, 4, '#2A2F3A');
1921      f(16, 33, 64, 1, '#F5F3EE'); f(48, 32, 1, 2, '#F5F3EE');
1922      for (let i = 0; i < 16; i++) { const c = i % 2 ? '#F5F0E1' : '#1F5A3E'; f(i * 6, 0, 6, 7, c); f(i * 6 + 1, 7, 4, 1, c); }
1923      f(0, 7, GW, 0, '#000');
1924      f(1, 8, 2, 34, '#F5F3EE'); f(93, 8, 2, 34, '#F5F3EE');
1925      f(0, 36, GW, 1, '#F5F3EE'); f(0, 37, GW, 1, '#D9DDE6');
1926      for (let xx = 1; xx < GW; xx += 4) f(xx, 38, 2, 4, '#F1EEE6');
1927      f(0, 42, GW, 1, '#D9DDE6');
1928      f(0, 43, GW, GH - 43, '#E7E1D6');
1929      for (let y = 44, row = 0; y < GH; y += 4, row++) for (let xx = (row % 2) * 6; xx < GW; xx += 12) f(xx, y, 6, 4, '#D4CDC0');
1930      spr(SEAT, 17, 33, { h: '#E2C277', c: '#F5F3EE', w: '#F5F3EE', t: '#22D39A' });
1931      spr(SEAT, 33, 33, { h: '#2B1D15', s: '#C68B65', c: '#2B3042', w: '#F5F3EE', t: '#0A7AFF' });
1932      f(13, 44, 36, 2, '#FBFAF6'); f(14, 46, 34, 8, '#F1EEE6'); for (let xx = 19; xx < 47; xx += 6) f(xx, 46, 1, 8, '#DDD8CC'); f(14, 53, 34, 1, '#DDD8CC'); f(15, 54, 32, 1, '#C9C1B2');
1933      flute(f, 22, 38, '#E2C277'); flute(f, 41, 38, '#E2C277');
1934      f(28, 42, 8, 1, '#F5F3EE'); f(29, 43, 6, 1, '#D9DDE6'); [[29, 41], [31, 40], [33, 41], [30, 39], [32, 41]].forEach(([a, b]) => f(a, b, 2, 1, '#D9644F')); f(31, 39, 2, 1, '#FBFAF6');
1935      host(spr, f, 62, 27, (a, b) => { flute(f, a + 2, b - 6, '#E2C277'); flute(f, a + 6, b - 6, '#E2C277'); [[a + 9, b - 2]].forEach(([p, q]) => { f(p, q, 2, 2, '#D9644F'); }); });
1936      f(86, 50, 8, 9, '#2A1D15'); f(86, 50, 8, 1, '#C9A55A'); [[85, 44, 3, 2], [88, 42, 2, 8], [91, 44, 3, 2], [86, 47, 2, 2], [91, 47, 3, 1]].forEach(([a, b, w, h]) => f(a, b, w, h, '#2E7D4F'));
1937    },
1938    team(cv) {
1939      const { f, spr } = mk(cv), r = rng(9);
1940      f(0, 0, GW, GH, '#1E3A31'); for (let xx = 2; xx < GW; xx += 12) { f(xx, 6, 1, 32, '#18302A'); f(xx + 1, 6, 1, 32, '#26473C'); }
1941      const wl = 47, wr = 93, wt = 9, wb = 33;
1942      ['#93C6EA', '#A2CFEE', '#B3D8F1'].forEach((c, i) => f(wl, wt + i * 2, wr - wl, 2, c));
1943      [52, 88].forEach(px => { f(px, wt + 1, 1, 8, '#6B7488'); f(px - 2, wt, 5, 2, '#FFF6D6'); });
1944      f(wl, 16, wr - wl, 1, '#3A4152'); crowd(f, r, wl, wr, 17, 25, '#5D6579', '#6B7488'); f(wl, 25, wr - wl, 1, '#2F3546');
1945      for (let y = 26, i = 0; y < wb; y += 2, i++) f(wl, y, wr - wl, 2, i % 2 ? '#3E9B52' : '#48A75C'); f(wl, 28, wr - wl, 1, '#EAF3EA');
1946      f(wl - 1, wt - 1, wr - wl + 2, 1, '#3A2719'); f(wl - 1, wb, wr - wl + 2, 1, '#3A2719'); f(wl - 1, wt, 1, wb - wt, '#3A2719'); f(wr, wt, 1, wb - wt, '#3A2719'); f(wl, wt, wr - wl, 1, '#C9A55A');
1947      f(70, wt, 1, wb - wt, '#3A2719');
1948      f(0, 0, GW, 4, '#2A1D15'); f(0, 4, GW, 1, '#C9A55A'); f(0, 5, GW, 1, '#3A2719');
1949      f(30, 8, 15, 20, '#C9A55A'); f(31, 9, 13, 18, '#E2C277'); f(32, 10, 11, 16, '#14261F');
1950      spr(SHIRT.map(s => s.slice(0, 11 + 2)), 31, 11, { b: '#0A7AFF', w: '#F5F3EE' });
1951      f(0, 38, GW, 14, '#4A3020'); for (let xx = 2; xx < GW; xx += 12) { f(xx + 1, 41, 10, 9, '#553722'); f(xx + 1, 41, 10, 1, '#6A462D'); } f(0, 38, GW, 1, '#E2C277'); f(0, 39, GW, 1, '#B8913F');
1952      f(0, 52, GW, GH - 52, '#E7E1D6'); for (let y = 53, row = 0; y < GH; y += 4, row++) for (let xx = (row % 2) * 6; xx < GW; xx += 12) f(xx, y, 6, 4, '#D4CDC0'); f(0, 52, GW, 1, '#2A1D15');
1953      f(1, 18, 13, 34, '#3A2719'); f(2, 19, 11, 33, '#553722'); f(3, 21, 9, 12, '#6A462D'); f(3, 35, 9, 14, '#6A462D'); f(11, 36, 1, 2, '#E2C277');
1954      spr(MAN, -2, 20, { k: '#9C7A54', K: '#9C7A54', q: '#9C7A54', t: '#7A1F2B', h: '#14161F', s: '#C68B65', p: '#3A3F4E' }, 'rn');
1955      spr(MAN, 15, 20);
1956      [[50, '#E2C277', '#F5F3EE', '#22D39A'], [62, '#2B1D15', '#2B3042', '#0A7AFF'], [74, '#8A5A3B', '#7A1F2B', '#F5F3EE'], [86, '#14161F', '#F5F3EE', '#2B3042']].forEac
1956h(([gx, h, c, t], i) => spr(SEAT, gx, 33, { h, c, w: i === 2 ? '#F5F3EE' : '#F5F3EE', t, s: i % 2 ? '#C68B65' : '#E6B18C' }));
1957      f(44, 44, 52, 2, '#FBFAF6'); f(45, 46, 51, 6, '#F1EEE6'); for (let xx = 50; xx < GW; xx += 7) f(xx, 46, 1, 6, '#DDD8CC'); f(45, 51, 51, 1, '#DDD8CC');
1958      [50, 62, 74, 86].forEach(gx => { f(gx + 1, 43, 6, 1, '#D9DDE6'); f(gx + 2, 43, 4, 1, '#FBFAF6'); f(gx + 8, 40, 2, 2, '#7A1F2B'); f(gx + 8, 39, 2, 1, '#E9EDF3'); f(gx + 8, 42, 1, 2, '#D9DDE6'); });
1959    },
1960    series(cv) {
1961      const { f, spr } = mk(cv), r = rng(13);
1962      f(0, 0, GW, GH, '#E9EDF3');
1963      f(0, 0, GW, 3, '#2B3042');
1964      const wl = 3, wr = 93, wt = 8, wb = 35;
1965      ['#A2CFEE', '#B3D8F1', '#C6E2F4'].forEach((c, i) => f(wl, wt + i * 2, wr - wl, 2, c));
1966      f(wl, 14, wr - wl, 1, '#3A4152'); crowd(f, r, wl, wr, 15, 20, '#5D6579', '#6B7488');
1967      for (let xx = wl; xx < wr; xx += 3) f(xx, 20, 3, 2, (xx / 3) % 2 < 1 ? '#D9644F' : '#F5F3EE');
1968      f(wl, 22, wr - wl, wb - 22, '#4A4F5C'); f(wl, 23, wr - wl, 1, '#565C6B');
1969      f(wl, 26, wr - wl, 1, '#F5F3EE'); for (let xx = wl + 2; xx < wr; xx += 8) f(xx, 33, 4, 1, '#C9CDD6');
1970      // race car in the pit lane
1971      f(43, 23, 7, 1, '#111218'); f(44, 24, 2, 5, '#111218');
1972      f(52, 28, 22, 1, '#0A7AFF'); f(45, 29, 40, 2, '#0A7AFF'); f(45, 31, 37, 1, '#0868D9'); f(50, 30, 24, 1, '#22D39A');
1973      f(60, 26, 3, 2, '#F2C14E'); f(59, 27, 6, 1, '#111218'); f(80, 32, 9, 1, '#111218');
1974      [[47, 29], [73, 29]].forEach(([wx, wy]) => { f(wx, wy, 6, 5, '#111218'); f(wx + 2, wy + 2, 2, 1, '#6B7488'); });
1975      f(wl - 1, wt - 1, wr - wl + 2, 1, '#2B3042'); f(wl - 1, wb, wr - wl + 2, 1, '#2B3042'); f(wl - 1, wt, 1, wb - wt, '#2B3042'); f(wr, wt, 1, wb - wt, '#2B3042');
1976      f(34, wt, 1, wb - wt, '#2B3042');
1977      f(0, 3, GW, 1, '#D9DDE6');
1978      for (let xx = 0; xx < GW; xx++) for (let y = 3; y < 6; y++) f(xx, y, 1, 1, (xx + y) % 2 ? '#111218' : '#F5F3EE');
1979      f(0, 36, GW, 1, '#C9A55A');
1980      f(0, 52, GW, GH - 52, '#4A3020'); for (let y = 54; y < GH; y += 3) f(0, y, GW, 1, '#553722'); f(0, 52, GW, 1, '#2A1D15');
1981      f(50, 40, 46, 12, '#2B3042'); for (let xx = 53; xx < GW; xx += 7) f(xx, 42, 1, 10, '#3A4152'); f(50, 40, 46, 1, '#C9A55A'); f(50, 41, 46, 1, '#B8913F');
1982      const mart = (a, liq) => { f(a, 36, 5, 1, '#E9EDF3'); f(a + 1, 37, 3, 1, liq); f(a + 2, 38, 1, 1, liq); f(a + 2, 39, 1, 1, '#D9DDE6'); };
1983      mart(53, '#22D39A'); mart(70, '#F2C14E'); mart(88, '#D9644F');
1984      [[58, '#14161F', '#F5F3EE', '#0A7AFF', '#E6B18C'], [76, '#8A5A3B', '#2B3042', '#F5F3EE', '#C68B65']].forEach(([gx, h, c, t, s]) => {
1985        f(gx, 47, 8, 1, '#C9A55A'); f(gx + 3, 48, 2, 9, '#6B7488'); f(gx + 1, 53, 6, 1, '#6B7488'); f(gx + 1, 57, 6, 1, '#6B7488');
1986        spr(SEAT, gx, 35, { h, c, w: '#F5F3EE', t, s }); f(gx, 45, 8, 2, '#3A3F4E'); f(gx + 1, 47, 2, 7, '#3A3F4E'); f(gx + 5, 47, 2, 7, '#3A3F4E'); f(gx, 54, 3, 1, '#111218'); f(gx + 5, 54, 3, 1, '#111218');
1987      });
1988      host(spr, f, 16, 27, (a, b) => { const m = (p, liq) => { f(p, b - 3, 3, 1, '#E9EDF3'); f(p + 1, b - 2, 1, 1, liq); f(p + 1, b - 1, 1, 1, '#D9DDE6'); }; m(a + 2, '#22D39A'); m(a + 6, '#F2C14E'); });
1989    }
1990  };
1991  document.querySelectorAll('canvas.pix').forEach(cv => { cv.width = GW; cv.height = GH; try { SCN[cv.dataset.pix](cv); } catch (e) { console.error(e); } });
1992})();
1993
1994/* ---------- Boot ---------- */
1995try { new IntersectionObserver(es => { S.visible = es[0].isIntersecting; last = performance.now(); }).observe(stage); } catch (_) {}
1996let rT = 0;
1997window.addEventListener('resize', () => { resize(); clearTimeout(rT); rT = setTimeout(renderPreview, 150); });
1998resize(); idleScene(); renderPreview(); draw();
1999let startLang = 'en';
2000try { const sv = localStorage.getItem('outgoing-lang'); if (sv) startLang = sv; else if ((navigator.languages || [navigator.language || '']).some(x => /^pt/i.test(x))) startLang = 'pt'; } catch (_) { if (/^pt/i.test(navigator.language || '')) startLang = 'pt'; }
2001applyLang(startLang);
2002{ const h = document.querySelector('.hero h1'), c = document.querySelector('.hero-copy'); if (h) h.classList.add('pre'); if (c) c.classList.add('pre'); }
2003setTimeout(revealHero, 4200);
2004
2005if (document.fonts && document.fonts.ready) document.fonts.ready.then(renderPreview).catch(() => {});
2006if (document.fonts && document.fonts.load) document.fonts.load('600 13px "Pixelify Sans"').catch(() => {});
2007requestAnimationFrame(frame);
2008})();
2009</script>
2009
2010
2011</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.