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 </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.