{"project":{"id":"xGN4MuT","userId":"davidyarham@gmail.com","username":null,"userPicture":null,"name":"Super Voxel Olympics","thumbnail":"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","visible":true,"contributors":"","githubRepo":null,"forkedFrom":null,"isTemplate":false,"tags":"","files":{"folder":"","files":[{"name":"index.html","content":"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Super Voxel Olympics</title>\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=Archivo+Black&family=Space+Grotesk:wght@400;600;700&display=swap\" rel=\"stylesheet\">\n<link rel=\"stylesheet\" href=\"css/styles.css\">\n</head>\n<body>\n<div id=\"app\">\n  <div id=\"vignette\"></div>\n\n  <div id=\"hud\" class=\"hidden\">\n    <div id=\"hud-top\">\n      <div id=\"event-chip\">\n        <span id=\"event-no\">EVENT 1 / 4</span>\n        <span id=\"event-name\">100M SPRINT</span>\n      </div>\n      <div id=\"timer\">0.00</div>\n    </div>\n    <div id=\"attempt-chip\" class=\"hidden\">ATTEMPT 1 / 2</div>\n    <div id=\"replay-badge\" class=\"hidden\">&#9679; REPLAY</div>\n    <div id=\"center-msg\"></div>\n    <div id=\"popup\"></div>\n    <div id=\"meters\">\n      <div id=\"angle-wrap\" class=\"hidden\">\n        <div id=\"angle-fill\"></div>\n        <span class=\"meter-label\" id=\"angle-label\">ANGLE 0&deg;</span>\n      </div>\n      <div id=\"power-wrap\">\n        <div id=\"power-fill\"></div>\n        <span class=\"meter-label\" id=\"power-label\">SPEED &mdash; MASH &#9664; &#9654;</span>\n      </div>\n    </div>\n  </div>\n\n  <div id=\"card\" class=\"overlay hidden\">\n    <div class=\"card-box\">\n      <div class=\"card-kicker\" id=\"card-no\">EVENT 1 OF 4</div>\n      <h2 id=\"card-name\">100M SPRINT</h2>\n      <p id=\"card-desc\"></p>\n      <div id=\"card-qual\" class=\"card-qual hidden\"></div>\n      <div id=\"card-records\"></div>\n      <div class=\"card-hint blink\">PRESS <b>ENTER</b> TO START</div>\n    </div>\n  </div>\n\n  <div id=\"name-entry\" class=\"overlay hidden\">\n    <div class=\"card-box\">\n      <div class=\"card-kicker\">ENTER YOUR INITIALS</div>\n      <div id=\"wheels\">\n        <div class=\"wheel\" id=\"wheel-0\"><span class=\"wl prev\"></span><span class=\"wl cur\"></span><span class=\"wl next\"></span></div>\n        <div class=\"wheel\" id=\"wheel-1\"><span class=\"wl prev\"></span><span class=\"wl cur\"></span><span class=\"wl next\"></span></div>\n        <div class=\"wheel\" id=\"wheel-2\"><span class=\"wl prev\"></span><span class=\"wl cur\"></span><span class=\"wl next\"></span></div>\n      </div>\n      <div class=\"card-hint blink\">&#9664; &#9654; SPIN &nbsp;&middot;&nbsp; <b>SPACE</b> LOCK LETTER</div>\n    </div>\n  </div>\n\n  <div id=\"event-select\" class=\"overlay hidden\">\n    <div class=\"card-box select-box\">\n      <div class=\"card-kicker\">CHOOSE YOUR EVENTS</div>\n      <div id=\"career-line\" class=\"career-line\"></div>\n      <div id=\"ev-grid\"></div>\n      <div id=\"ev-count\" class=\"ev-count\"></div>\n      <div class=\"card-hint\">&#9664; &#9654; MOVE &nbsp;&middot;&nbsp; <b>SPACE</b> TOGGLE &nbsp;&middot;&nbsp; <b>ENTER</b> START</div>\n      <button id=\"ev-start\" class=\"ev-start\">START THE GAMES</button>\n    </div>\n  </div>\n\n  <div id=\"results\" class=\"overlay hidden\">\n    <div class=\"card-box\">\n      <div class=\"card-kicker\" id=\"results-kicker\">RESULTS</div>\n      <h2 id=\"results-title\">100M SPRINT</h2>\n      <div id=\"results-table\"></div>\n      <div class=\"card-hint blink\" id=\"results-hint\">PRESS <b>ENTER</b> TO CONTINUE</div>\n    </div>\n  </div>\n\n  <div id=\"title\" class=\"overlay\">\n    <div class=\"title-box\">\n      <div class=\"title-kicker\">TRACK &middot; FIELD &middot; GLORY</div>\n      <h1>SUPER<br>VOXEL<br>OLYMPICS</h1>\n      <p class=\"title-sub\">A voxel love-letter to Track &amp; Field, Daley Thompson's Decathlon &amp; Sensible Soccer.</p>\n      <div class=\"card-hint blink\">PRESS <b>ENTER</b> TO BEGIN</div>\n      <div class=\"title-controls\">&#9664; &#9654; MASH TO RUN &nbsp;&middot;&nbsp; SPACE JUMP / THROW &nbsp;&middot;&nbsp; ENTER CONTINUE &nbsp;&middot;&nbsp; GAMEPAD OK</div>\n      <div class=\"title-controls touch-only\">TAP TO CONTINUE &nbsp;&middot;&nbsp; PADS TO RUN &nbsp;&middot;&nbsp; BEST IN LANDSCAPE</div>\n      <div id=\"title-standings\" class=\"title-standings hidden\"></div>\n    </div>\n  </div>\n\n  <div id=\"touch-ui\" class=\"hidden\">\n    <div id=\"t-left\">\n      <button id=\"t-run-l\" class=\"tbtn\">&#9664;</button>\n      <button id=\"t-run-r\" class=\"tbtn\">&#9654;</button>\n    </div>\n    <button id=\"t-action\" class=\"tbtn taction\">&#9650;</button>\n  </div>\n</div>\n\n<script type=\"importmap\">\n{\n  \"imports\": {\n    \"three\": \"https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js\",\n    \"three/addons/\": \"https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/\"\n  }\n}\n</script>\n<script src=\"js/main.js\" type=\"module\"></script>\n   \n\n</body>\n</html>\n"},{"name":"README.md","content":"# Super Voxel Olympics\n\nA voxel 3D athletics game in the spirit of Konami's Track & Field, Daley\nThompson's Decathlon and Sensible Soccer. Top-down camera, a life-sized 400m\noval stadium, 8 competitors, world records, and a lot of button mashing.\n\n## Events (14)\n\n100m Sprint · Long Jump · Javelin · 110m Hurdles · High Jump · 200m ·\nTriple Jump · Shot Put · Pole Vault · 400m · Hammer Throw · Discus ·\n4×100m Relay · 800m\n\nAfter entering your initials you pick which events make up your games —\nthe selection is remembered between sessions (`svo-lineup-v1`).\n\n## Features\n\n- **Qualifying marks** — every event has a qualifying mark (shown on the\n  event card); hit it for +100 bonus points\n- **Slow-mo replay** — put a mark on the record board and the last few\n  seconds play back in slow motion before the results\n- **Career mode** — meets, medals and season points persist per initials\n  (`svo-career-v1`); season standings appear on the title screen\n- **Gamepad support** — d-pad or X/B to mash, A = SPACE, Start = ENTER\n- Records are saved in localStorage (`svo-records-v1`)\n\n## Controls\n\n- **◀ ▶ (or A/D)** — alternate rapidly to run / spin up throws\n- **SPACE** — jump hurdles, charge & release jump/throw angle, pass the\n  baton, plant the vault pole (release at the top of the swing!)\n- **ENTER** — advance screens\n- 200m/400m/800m have a **stamina** gauge — pace yourself\n- High Jump / Pole Vault: the bar keeps rising, three misses ends it\n\n## Code layout\n\n- `js/lib.js` — three.js + shared helpers\n- `js/world.js` — the stadium (track, stands, crowd, roof, props)\n- `js/athlete.js` — articulated voxel athlete + all animation modes\n- `js/props.js` — hurdles, javelin, hammer, discus\n- `js/core.js` — renderer, camera, roster, shared state\n- `js/hud.js`, `js/audio.js`, `js/fx.js`, `js/records.js`, `js/npcs.js`\n- `js/events/` — `races` (straights), `laps` (oval + relay), `jumps`, `throws`\n- `js/main.js` — game flow, input, post-FX, render loop\n"}],"folders":[{"folder":"css","files":[{"name":"styles.css","content":":root {\n  --accent: #ff4b3a;\n  --amber: #ffb020;\n  --lime: #7ce34a;\n  --ink: #0b1120;\n  --glass: rgba(10, 16, 30, 0.72);\n  --glass-line: rgba(255, 255, 255, 0.14);\n  font-size: 16px;\n}\n\n* { margin: 0; padding: 0; box-sizing: border-box; }\n\nhtml, body {\n  width: 100%;\n  height: 100%;\n  overflow: hidden;\n  background: #0b1120;\n  font-family: 'Space Grotesk', system-ui, sans-serif;\n  color: #fff;\n  user-select: none;\n  -webkit-user-select: none;\n}\n\n#app { position: fixed; inset: 0; }\n#app canvas { display: block; }\n\n#vignette {\n  position: absolute; inset: 0;\n  pointer-events: none;\n  background: radial-gradient(ellipse at center, transparent 58%, rgba(5, 9, 20, 0.42) 100%);\n  z-index: 5;\n}\n\n.hidden { display: none !important; }\n\n/* ---------- HUD ---------- */\n#hud { position: absolute; inset: 0; z-index: 10; pointer-events: none; }\n\n#hud-top {\n  position: absolute; top: 22px; left: 26px; right: 26px;\n  display: flex; justify-content: space-between; align-items: flex-start;\n}\n\n#event-chip {\n  display: flex; flex-direction: column; gap: 3px;\n  background: var(--glass);\n  border: 1px solid var(--glass-line);\n  border-left: 4px solid var(--accent);\n  border-radius: 12px;\n  padding: 10px 18px 12px;\n  backdrop-filter: blur(8px);\n  box-shadow: 0 10px 30px rgba(0,0,0,.35);\n}\n#event-no {\n  font-size: 0.65rem; letter-spacing: 3px; font-weight: 700;\n  color: var(--amber);\n}\n#event-name {\n  font-family: 'Archivo Black', sans-serif;\n  font-size: 1.25rem; letter-spacing: 1px;\n}\n\n#timer {\n  font-family: 'Archivo Black', sans-serif;\n  font-size: 2.6rem;\n  letter-spacing: 1px;\n  background: var(--glass);\n  border: 1px solid var(--glass-line);\n  border-radius: 14px;\n  padding: 6px 20px;\n  backdrop-filter: blur(8px);\n  font-variant-numeric: tabular-nums;\n  box-shadow: 0 10px 30px rgba(0,0,0,.35);\n}\n\n#attempt-chip {\n  position: absolute; top: 108px; left: 26px;\n  background: var(--glass);\n  border: 1px solid var(--glass-line);\n  border-radius: 999px;\n  padding: 7px 16px;\n  font-size: 0.7rem; font-weight: 700; letter-spacing: 2.5px;\n  color: #cfe0ff;\n  backdrop-filter: blur(8px);\n}\n\n/* meters */\n#meters {\n  position: absolute; bottom: 34px; left: 50%;\n  transform: translateX(-50%);\n  display: flex; flex-direction: column; gap: 14px;\n  width: min(420px, 74vw);\n}\n#power-wrap, #angle-wrap {\n  position: relative;\n  height: 20px;\n  background: var(--glass);\n  border: 1px solid var(--glass-line);\n  border-radius: 999px;\n  overflow: hidden;\n  backdrop-filter: blur(8px);\n  box-shadow: 0 10px 30px rgba(0,0,0,.35);\n}\n#power-fill {\n  height: 100%; width: 0%;\n  background: linear-gradient(90deg, var(--lime), var(--amber) 60%, var(--accent));\n  border-radius: 999px;\n  transition: width 60ms linear;\n}\n#angle-fill {\n  height: 100%; width: 0%;\n  background: linear-gradient(90deg, #46c6ff, #7c5bff);\n  border-radius: 999px;\n  transition: width 40ms linear;\n}\n.meter-label {\n  position: absolute; inset: 0;\n  display: grid; place-items: center;\n  font-size: 0.62rem; font-weight: 700; letter-spacing: 3px;\n  color: rgba(255,255,255,.92);\n  text-shadow: 0 1px 4px rgba(0,0,0,.6);\n}\n\n/* replay badge */\n#replay-badge {\n  position: absolute; top: 108px; right: 26px;\n  background: var(--glass);\n  border: 1px solid rgba(255,75,58,.6);\n  border-radius: 999px;\n  padding: 7px 16px;\n  font-size: 0.7rem; font-weight: 700; letter-spacing: 3px;\n  color: var(--accent);\n  backdrop-filter: blur(8px);\n  animation: blink 1s step-end infinite;\n}\n\n/* center message (countdown, GO, FOUL) */\n#center-msg {\n  position: absolute; top: 34%; left: 50%;\n  transform: translate(-50%, -50%);\n  font-family: 'Archivo Black', sans-serif;\n  font-size: clamp(3rem, 11vw, 8rem);\n  letter-spacing: 4px;\n  text-shadow: 0 6px 0 rgba(0,0,0,.28), 0 18px 60px rgba(0,0,0,.5);\n  opacity: 0;\n}\n#center-msg.pop {\n  animation: msgpop 0.9s cubic-bezier(.2,1.6,.35,1) forwards;\n}\n#center-msg.go { color: var(--lime); }\n#center-msg.bad { color: var(--accent); }\n@keyframes msgpop {\n  0%   { opacity: 0; transform: translate(-50%,-50%) scale(2.2); }\n  18%  { opacity: 1; transform: translate(-50%,-50%) scale(1); }\n  75%  { opacity: 1; }\n  100% { opacity: 0; transform: translate(-50%,-50%) scale(.92); }\n}\n\n/* floating popup (distance / clipped etc.) */\n#popup {\n  position: absolute; top: 46%; left: 50%;\n  transform: translateX(-50%);\n  font-family: 'Archivo Black', sans-serif;\n  font-size: clamp(1.6rem, 4.5vw, 3rem);\n  color: var(--amber);\n  text-shadow: 0 4px 0 rgba(0,0,0,.3), 0 12px 40px rgba(0,0,0,.5);\n  opacity: 0;\n  white-space: nowrap;\n}\n#popup.rise { animation: rise 1.6s ease-out forwards; }\n#popup.bad { color: var(--accent); }\n@keyframes rise {\n  0%   { opacity: 0; transform: translate(-50%, 26px) scale(.7); }\n  15%  { opacity: 1; transform: translate(-50%, 0) scale(1); }\n  70%  { opacity: 1; }\n  100% { opacity: 0; transform: translate(-50%, -46px) scale(.96); }\n}\n\n/* ---------- Overlays ---------- */\n.overlay {\n  position: absolute; inset: 0; z-index: 20;\n  display: grid; place-items: center;\n  background: radial-gradient(ellipse at center, rgba(6,10,22,.25) 0%, rgba(6,10,22,.68) 100%);\n  backdrop-filter: blur(3px);\n}\n\n.card-box, .title-box {\n  background: var(--glass);\n  border: 1px solid var(--glass-line);\n  border-radius: 24px;\n  padding: 42px 58px;\n  text-align: center;\n  max-width: min(620px, 90vw);\n  box-shadow: 0 30px 90px rgba(0,0,0,.55);\n  backdrop-filter: blur(14px);\n  animation: cardin .45s cubic-bezier(.2,1.4,.4,1);\n}\n@keyframes cardin {\n  from { opacity: 0; transform: translateY(26px) scale(.96); }\n  to   { opacity: 1; transform: none; }\n}\n\n.card-kicker, .title-kicker {\n  font-size: 0.7rem; font-weight: 700;\n  letter-spacing: 5px;\n  color: var(--amber);\n  margin-bottom: 12px;\n}\n\n.card-box h2 {\n  font-family: 'Archivo Black', sans-serif;\n  font-size: clamp(2rem, 6vw, 3.4rem);\n  letter-spacing: 2px;\n  line-height: 1.02;\n  background: linear-gradient(120deg, #fff 30%, #ffd9a0);\n  -webkit-background-clip: text;\n  background-clip: text;\n  -webkit-text-fill-color: transparent;\n  margin-bottom: 14px;\n}\n\n#card-desc {\n  font-size: 1rem; line-height: 1.55;\n  color: #c7d4ee;\n  margin-bottom: 26px;\n}\n\n.card-hint {\n  font-size: 0.78rem; font-weight: 700; letter-spacing: 3px;\n  color: #e8eefc;\n}\n.card-hint b { color: var(--lime); }\n.blink { animation: blink 1.4s step-end infinite; }\n@keyframes blink { 50% { opacity: 0.25; } }\n\n/* title screen — floats free over the stadium, no modal */\n#title {\n  background: transparent;\n  backdrop-filter: none;\n}\n#title .title-box {\n  background: transparent;\n  border: none;\n  box-shadow: none;\n  backdrop-filter: none;\n  padding: 0;\n  max-width: none;\n}\n.title-box h1 {\n  font-family: 'Archivo Black', sans-serif;\n  font-size: clamp(3rem, 9.5vw, 5.4rem);\n  line-height: 0.95;\n  letter-spacing: 3px;\n  /* generous padding so background-clip:text never crops edge glyphs */\n  padding: 0.06em 0.2em;\n  background: linear-gradient(160deg, #ffffff 10%, #ffc46b 45%, var(--accent) 90%);\n  -webkit-background-clip: text;\n  background-clip: text;\n  -webkit-text-fill-color: transparent;\n  margin-bottom: 18px;\n  filter: drop-shadow(0 6px 0 rgba(0,0,0,.35)) drop-shadow(0 16px 34px rgba(0,0,0,.5));\n}\n.title-kicker,\n.title-sub,\n#title .card-hint,\n.title-controls {\n  text-shadow: 0 2px 10px rgba(0,0,0,.75), 0 0 30px rgba(0,0,0,.5);\n}\n.title-sub { color: #e6eeff; }\n.title-controls { color: #c6d4ee; }\n.title-sub {\n  color: #c7d4ee;\n  font-size: 0.95rem;\n  line-height: 1.5;\n  margin-bottom: 28px;\n}\n.title-controls {\n  margin-top: 22px;\n  font-size: 0.68rem; letter-spacing: 2.5px; font-weight: 600;\n  color: #8fa3c8;\n}\n\n/* ---------- initials wheels (Track & Field style) ---------- */\n#wheels {\n  display: flex;\n  justify-content: center;\n  gap: 22px;\n  margin: 26px 0 30px;\n}\n.wheel {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  width: 86px;\n  padding: 10px 0;\n  border-radius: 14px;\n  background: rgba(255,255,255,.05);\n  border: 1px solid rgba(255,255,255,.1);\n  overflow: hidden;\n}\n.wheel .wl {\n  font-family: 'Archivo Black', sans-serif;\n  line-height: 1.15;\n}\n.wheel .prev, .wheel .next {\n  font-size: 1.4rem;\n  color: rgba(255,255,255,.22);\n  transform: scale(.8);\n}\n.wheel .cur {\n  font-size: 3.2rem;\n  color: #fff;\n}\n.wheel.active {\n  border-color: var(--accent);\n  background: rgba(255,75,58,.14);\n  box-shadow: 0 0 24px rgba(255,75,58,.35);\n}\n.wheel.active .cur { color: var(--amber); }\n.wheel.locked .cur { color: var(--lime); }\n\n/* qualifying line on the event card */\n.card-qual {\n  display: inline-block;\n  margin: -12px 0 20px;\n  padding: 6px 16px;\n  border-radius: 999px;\n  background: rgba(255,176,32,.12);\n  border: 1px solid rgba(255,176,32,.4);\n  font-size: .68rem; font-weight: 700; letter-spacing: 2px;\n  color: var(--amber);\n}\n\n/* ---------- event record board ---------- */\n#card-records {\n  margin: 4px 0 24px;\n  display: flex;\n  flex-direction: column;\n  gap: 5px;\n}\n.rec-row {\n  display: grid;\n  grid-template-columns: 30px 64px 1fr;\n  gap: 10px;\n  align-items: center;\n  padding: 6px 16px;\n  background: rgba(255,255,255,.05);\n  border: 1px solid rgba(255,255,255,.08);\n  border-radius: 9px;\n  font-size: .82rem;\n  font-weight: 700;\n  letter-spacing: 1px;\n}\n.rec-row .rp { font-family: 'Archivo Black', sans-serif; color: #8fa3c8; }\n.rec-row:nth-child(1) .rp { color: #ffd24a; }\n.rec-row:nth-child(2) .rp { color: #cfd8e6; }\n.rec-row:nth-child(3) .rp { color: #e0925a; }\n.rec-row .rn { color: var(--amber); }\n.rec-row .rv { text-align: right; font-variant-numeric: tabular-nums; color: #e8eefc; }\n\n/* ---------- touch controls ---------- */\n#touch-ui { position: fixed; inset: 0; z-index: 30; pointer-events: none; }\n#t-left {\n  position: absolute; left: 16px; bottom: 18px;\n  display: flex; gap: 14px;\n}\n.tbtn {\n  pointer-events: auto;\n  width: 84px; height: 84px;\n  border-radius: 50%;\n  border: 1px solid rgba(255,255,255,.28);\n  background: rgba(10,16,30,.55);\n  color: #fff;\n  font-size: 1.5rem;\n  backdrop-filter: blur(6px);\n  touch-action: none;\n  -webkit-tap-highlight-color: transparent;\n  user-select: none;\n  -webkit-user-select: none;\n}\n.tbtn:active {\n  background: rgba(255,75,58,.55);\n  transform: scale(.93);\n}\n.taction {\n  position: absolute; right: 18px; bottom: 18px;\n  width: 106px; height: 106px;\n  background: rgba(255,75,58,.35);\n  border-color: rgba(255,75,58,.65);\n}\n.touch-only { display: none; }\nbody.touch .touch-only { display: block; }\nbody.touch #meters { bottom: 128px; width: min(340px, 52vw); }\n\n/* ---------- event select ---------- */\n.select-box {\n  max-width: min(780px, 94vw);\n  max-height: 94vh;\n  overflow-y: auto;\n}\n#ev-grid {\n  display: grid;\n  grid-template-columns: repeat(2, minmax(0, 1fr));\n  gap: 8px;\n  margin: 18px 0 14px;\n  text-align: left;\n}\n.ev-item {\n  display: flex; align-items: center; gap: 12px;\n  padding: 9px 14px;\n  background: rgba(255,255,255,.05);\n  border: 1px solid rgba(255,255,255,.08);\n  border-radius: 12px;\n  font-weight: 700; letter-spacing: 1px; font-size: .88rem;\n  color: #8fa3c8;\n  cursor: pointer;\n}\n.ev-item .tick {\n  width: 20px; height: 20px; flex: none;\n  border-radius: 6px;\n  border: 2px solid rgba(255,255,255,.25);\n  display: grid; place-items: center;\n  font-size: .68rem; font-weight: 900;\n  color: transparent;\n}\n.ev-item.on { color: #fff; }\n.ev-item.on .tick { background: var(--lime); border-color: var(--lime); color: var(--ink); }\n.ev-item.cur {\n  border-color: var(--accent);\n  background: rgba(255,75,58,.14);\n  box-shadow: 0 0 18px rgba(255,75,58,.3);\n}\n.ev-count {\n  font-size: .72rem; font-weight: 700; letter-spacing: 3px;\n  color: var(--amber);\n  margin-bottom: 16px;\n}\n.ev-start {\n  display: block;\n  margin: 18px auto 0;\n  padding: 12px 34px;\n  border-radius: 999px;\n  border: 1px solid rgba(255,75,58,.65);\n  background: rgba(255,75,58,.22);\n  color: #fff;\n  font-family: inherit;\n  font-weight: 700; letter-spacing: 3px; font-size: .82rem;\n  cursor: pointer;\n  -webkit-tap-highlight-color: transparent;\n}\n.ev-start:hover, .ev-start:active { background: rgba(255,75,58,.45); }\n.career-line {\n  font-size: .74rem; font-weight: 700; letter-spacing: 1.5px;\n  color: #cfe0ff;\n  padding: 7px 14px;\n  border-radius: 999px;\n  background: rgba(255,255,255,.06);\n  border: 1px solid rgba(255,255,255,.1);\n  display: inline-block;\n}\n\n/* qualifying / career strips in results */\n.qual-strip, .career-strip {\n  padding: 9px 18px;\n  margin-bottom: 8px;\n  border-radius: 12px;\n  font-size: .74rem; font-weight: 700; letter-spacing: 2px;\n  background: rgba(124,227,74,.12);\n  border: 1px solid rgba(124,227,74,.45);\n  color: var(--lime);\n}\n.qual-strip.missed {\n  background: rgba(255,75,58,.1);\n  border-color: rgba(255,75,58,.4);\n  color: #ff8d80;\n}\n.career-strip {\n  margin-top: 12px;\n  margin-bottom: 0;\n  background: rgba(255,176,32,.1);\n  border-color: rgba(255,176,32,.4);\n  color: var(--amber);\n}\n\n/* season standings on the title */\n.title-standings {\n  margin-top: 20px;\n  display: inline-flex;\n  flex-direction: column;\n  gap: 4px;\n  padding: 12px 26px;\n  border-radius: 14px;\n  background: var(--glass);\n  border: 1px solid var(--glass-line);\n  backdrop-filter: blur(8px);\n  font-size: .72rem; font-weight: 700; letter-spacing: 2px;\n}\n.title-standings .ts-k { color: var(--amber); letter-spacing: 4px; font-size: .62rem; margin-bottom: 3px; }\n.title-standings .ts-row { color: #e8eefc; font-variant-numeric: tabular-nums; }\n\n/* results table */\n#results-table { margin: 6px 0 26px; }\n.res-row {\n  display: grid;\n  grid-template-columns: 44px 1fr auto auto;\n  gap: 14px;\n  align-items: center;\n  padding: 10px 18px;\n  margin-bottom: 8px;\n  background: rgba(255,255,255,.05);\n  border: 1px solid rgba(255,255,255,.08);\n  border-radius: 12px;\n  text-align: left;\n  font-weight: 600;\n}\n.res-row.player {\n  border-color: rgba(255,75,58,.6);\n  background: rgba(255,75,58,.12);\n}\n.res-pos {\n  font-family: 'Archivo Black', sans-serif;\n  font-size: 1.1rem;\n  color: #8fa3c8;\n}\n.res-row:nth-child(1) .res-pos { color: #ffd24a; }\n.res-row:nth-child(2) .res-pos { color: #cfd8e6; }\n.res-row:nth-child(3) .res-pos { color: #e0925a; }\n.res-name { letter-spacing: 1px; }\n.res-mark { font-variant-numeric: tabular-nums; color: #cfe0ff; }\n.res-pts {\n  font-family: 'Archivo Black', sans-serif;\n  color: var(--amber);\n  font-variant-numeric: tabular-nums;\n  min-width: 84px;\n  text-align: right;\n}\n.res-total { font-size: .72rem; color: #8fa3c8; margin-left: 6px; }\n\n/* ---------- small screens (last so it wins the cascade) ---------- */\n@media (max-width: 680px) {\n  #hud-top { top: 10px; left: 10px; right: 10px; }\n  #event-chip { padding: 6px 12px 8px; }\n  #event-name { font-size: 0.95rem; }\n  #event-no { font-size: 0.55rem; }\n  #timer { font-size: 1.7rem; padding: 3px 12px; }\n  #attempt-chip { top: 78px; left: 10px; font-size: 0.6rem; }\n  .card-box, .title-box { padding: 24px 20px; max-width: 94vw; }\n  .card-box h2 { font-size: 1.7rem; }\n  #card-desc { font-size: 0.85rem; margin-bottom: 16px; }\n  #card-records { margin-bottom: 14px; }\n  .rec-row { padding: 4px 12px; font-size: 0.72rem; }\n  .res-row { padding: 5px 10px; margin-bottom: 4px; font-size: 0.78rem; gap: 8px; grid-template-columns: 26px 1fr auto auto; }\n  .res-pts { min-width: 60px; }\n  .res-pos { font-size: 0.9rem; }\n  #replay-badge { top: 78px; right: 10px; font-size: 0.6rem; }\n  .card-qual { font-size: .58rem; margin: -8px 0 12px; padding: 4px 10px; }\n  .career-line { font-size: .6rem; padding: 5px 10px; }\n  .qual-strip, .career-strip { padding: 5px 10px; font-size: .62rem; margin-bottom: 4px; }\n  .title-standings { padding: 8px 16px; font-size: .62rem; margin-top: 12px; }\n  #ev-grid { gap: 5px; margin: 12px 0 10px; }\n  .ev-item { padding: 5px 9px; font-size: .66rem; gap: 8px; border-radius: 8px; }\n  .ev-item .tick { width: 14px; height: 14px; border-radius: 4px; font-size: .5rem; }\n  .ev-start { padding: 9px 24px; margin-top: 10px; font-size: .7rem; }\n  #wheels { gap: 12px; margin: 18px 0 20px; }\n  .wheel { width: 62px; padding: 6px 0; }\n  .wheel .cur { font-size: 2.1rem; }\n  .wheel .prev, .wheel .next { font-size: 1rem; }\n}\n"}],"folders":[]},{"folder":"js","files":[{"name":"athlete.js","content":"// athlete.js — Sensible-Soccer-proportioned voxel athlete: enormous head\n// (mostly hair from the top-down camera), no neck to speak of, stubby limbs,\n// solid kits, black boots. Fully articulated with joint caps and ankles so\n// the figure stays connected and fluid. ~1.78m world height.\nimport { THREE, boxMesh } from './lib.js';\n\nexport class Athlete {\n  constructor({\n    kit = '#ff4b3a', shorts = '#ffffff', skin = '#c98e5a', hair = '#20140e',\n    hairStyle = 'crop', name = 'YOU', num = 0,\n  } = {}) {\n    this.name = name;\n    this.group = new THREE.Group();\n    this.body = new THREE.Group();\n    this.group.add(this.body);\n    this.group.rotation.y = Math.PI / 2; // face +X\n    this.group.scale.setScalar(0.27);    // -> ~1.78m, stocky\n\n    // ----- stubby legs: hip cap -> thigh -> knee cap -> calf -> ankle -> boot\n    const buildLeg = () => {\n      const hip = new THREE.Group();\n      const hipCap = boxMesh(0.82, 0.82, 0.82, shorts);\n      hipCap.position.y = 0;\n      const thigh = boxMesh(0.78, 1.1, 0.78, skin);\n      thigh.position.y = -0.42;\n      const knee = new THREE.Group();\n      knee.position.y = -0.95;\n      const kneeCap = boxMesh(0.72, 0.72, 0.72, skin);\n      kneeCap.position.y = 0;\n      const calf = boxMesh(0.68, 0.95, 0.68, skin);\n      calf.position.y = -0.32;\n      const sock = boxMesh(0.72, 0.42, 0.72, shorts);\n      sock.position.y = -0.52;\n      const ankle = new THREE.Group();\n      ankle.position.y = -0.75;\n      const boot = boxMesh(0.95, 0.4, 1.3, '#22262c', { roughness: 0.55 }); // black boots!\n      boot.position.set(0, 0.02, 0.26);\n      const sole = boxMesh(0.98, 0.12, 1.33, '#dfe3e8', { roughness: 0.6 });\n      sole.position.set(0, -0.15, 0.26);\n      ankle.add(boot, sole);\n      knee.add(kneeCap, calf, sock, ankle);\n      hip.add(hipCap, thigh, knee);\n      return { hip, knee, ankle };\n    };\n    const lL = buildLeg(), lR = buildLeg();\n    this.legL = lL.hip; this.kneeL = lL.knee; this.ankleL = lL.ankle;\n    this.legR = lR.hip; this.kneeR = lR.knee; this.ankleR = lR.ankle;\n    this.legL.position.set(-0.52, 1.9, 0);\n    this.legR.position.set(0.52, 1.9, 0);\n\n    // ----- stocky trunk: solid shorts, solid kit, minimal trim\n    const shortsBox = boxMesh(1.95, 0.85, 1.15, shorts);\n    shortsBox.position.y = 2.1;\n    const torso = boxMesh(1.95, 1.5, 1.1, kit);\n    torso.position.y = 3.0;\n    const collar = boxMesh(1.15, 0.16, 0.92, shorts);\n    collar.position.y = 3.8;\n    const neck = boxMesh(0.7, 0.35, 0.7, skin); // barely there — Sensible style\n    neck.position.y = 3.85;\n\n    // ----- stubby arms: shoulder cap -> upper -> elbow cap -> forearm + fist\n    const buildArm = () => {\n      const shoulder = new THREE.Group();\n      const sleeve = boxMesh(0.72, 0.72, 0.72, kit);\n      sleeve.position.y = -0.1;\n      const upper = boxMesh(0.56, 0.85, 0.56, skin);\n      upper.position.y = -0.45;\n      const elbow = new THREE.Group();\n      elbow.position.y = -0.85;\n      const elbowCap = boxMesh(0.52, 0.52, 0.52, skin);\n      elbowCap.position.y = 0;\n      const forearm = boxMesh(0.5, 0.7, 0.5, skin);\n      forearm.position.y = -0.25;\n      const fist = boxMesh(0.6, 0.45, 0.6, skin);\n      fist.position.y = -0.68;\n      elbow.add(elbowCap, forearm, fist);\n      shoulder.add(sleeve, upper, elbow);\n      return { shoulder, elbow };\n    };\n    const aL = buildArm(), aR = buildArm();\n    this.armL = aL.shoulder; this.elbowL = aL.elbow;\n    this.armR = aR.shoulder; this.elbowR = aR.elbow;\n    this.armL.position.set(-1.3, 3.65, 0);\n    this.armR.position.set(1.3, 3.65, 0);\n\n    // ----- the enormous head, sat straight on the shoulders\n    this.head = new THREE.Group();\n    this.head.position.y = 3.95;\n    const headBox = boxMesh(2.2, 2.2, 2.2, skin);\n    headBox.position.y = 1.15;\n    // minimal sprite face: eyes + expressive brows, nothing else\n    this.eyeL = boxMesh(0.3, 0.34, 0.1, '#181818');\n    this.eyeL.position.set(-0.48, 1.25, 1.12);\n    this.eyeR = this.eyeL.clone();\n    this.eyeR.position.x = 0.48;\n    this.browL = boxMesh(0.42, 0.12, 0.08, hair);\n    this.browL.position.set(-0.48, 1.62, 1.14);\n    this.browR = this.browL.clone();\n    this.browR.position.x = 0.48;\n    this.head.add(headBox, this.eyeL, this.eyeR, this.browL, this.browR);\n\n    // hair dominates the top-down view — thick crown, fringe, and back\n    const addHair = (mesh) => this.head.add(mesh);\n    const fringe = boxMesh(2.28, 0.4, 0.2, hair);\n    fringe.position.set(0, 2.05, 1.08);\n    addHair(fringe);\n    if (hairStyle === 'afro') {\n      const fro = boxMesh(2.65, 1.3, 2.65, hair);\n      fro.position.set(0, 2.55, -0.05);\n      addHair(fro);\n      const froB = boxMesh(2.4, 0.9, 0.55, hair);\n      froB.position.set(0, 1.6, -1.1);\n      addHair(froB);\n    } else {\n      const crown = boxMesh(2.35, 0.65, 2.35, hair);\n      crown.position.set(0, 2.4, -0.05);\n      addHair(crown);\n      const hairBack = boxMesh(2.3, 1.15, 0.45, hair);\n      hairBack.position.set(0, 1.55, -1.06);\n      addHair(hairBack);\n      if (hairStyle === 'spike') {\n        for (const [sx, sz] of [[-0.65, 0.25], [0, -0.3], [0.65, 0.18]]) {\n          const spike = boxMesh(0.48, 0.55, 0.48, hair);\n          spike.position.set(sx, 2.95, sz);\n          addHair(spike);\n        }\n      } else if (hairStyle === 'pony') {\n        const tail = boxMesh(0.55, 1.5, 0.45, hair);\n        tail.position.set(0, 1.05, -1.45);\n        addHair(tail);\n      } else if (hairStyle === 'band') {\n        const band = boxMesh(2.4, 0.3, 2.4, kit);\n        band.position.set(0, 1.95, -0.05);\n        addHair(band);\n      }\n    }\n\n    this.body.add(\n      this.legL, this.legR, shortsBox, torso, collar, neck,\n      this.armL, this.armR, this.head\n    );\n\n    // race-number bib on the back\n    if (num > 0) {\n      const bc = document.createElement('canvas');\n      bc.width = 64; bc.height = 64;\n      const bctx = bc.getContext('2d');\n      bctx.fillStyle = '#f5f1e6';\n      bctx.fillRect(0, 0, 64, 64);\n      bctx.fillStyle = '#1a2233';\n      bctx.font = '900 42px \"Archivo Black\", sans-serif';\n      bctx.textAlign = 'center';\n      bctx.textBaseline = 'middle';\n      bctx.fillText(String(num), 32, 36);\n      const bibTex = new THREE.CanvasTexture(bc);\n      bibTex.colorSpace = THREE.SRGBColorSpace;\n      const bib = new THREE.Mesh(\n        new THREE.PlaneGeometry(0.9, 0.9),\n        new THREE.MeshStandardMaterial({ map: bibTex, roughness: 0.9 })\n      );\n      bib.position.set(0, 3.05, -0.575);\n      bib.rotation.y = Math.PI;\n      this.body.add(bib);\n    }\n\n    this.phase = Math.random() * Math.PI * 2;\n    this._tempo = 0.93 + Math.random() * 0.14;\n    this._impact = 0;\n    this._t = 0;\n    this._mt = 0;\n    this._lastMode = 'idle';\n    this._blink = 0;\n    this._nextBlink = 1.5 + Math.random() * 3;\n    this._s = {\n      hipL: 0, hipR: 0, kneeL: 0.08, kneeR: 0.08,\n      shL: 0, shR: 0, elL: -0.3, elR: -0.3, armZL: 0.12, armZR: -0.12,\n      bobY: 0, bobZ: 0, roll: 0, twist: 0,\n      hx: 0, hy: 0, hz: 0, sy: 1, sxz: 1,\n      browA: 0, browY: 0,\n    };\n  }\n\n  // mode: idle | run | air | crouch | celebrate | dejected | exhausted |\n  //       throwRun | charge | spin\n  update(dt, mode = 'idle', opt = {}) {\n    this._t += dt;\n    if (mode !== this._lastMode) {\n      if (this._lastMode === 'air') this._impact = 0.16;\n      this._lastMode = mode;\n      this._mt = 0;\n    }\n    this._mt += dt;\n    const t = this._t;\n    const mt = this._mt;\n\n    const P = {\n      hipL: 0, hipR: 0, kneeL: 0.08, kneeR: 0.08,\n      shL: 0, shR: 0, elL: -0.3, elR: -0.3, armZL: 0.12, armZR: -0.12,\n      bobY: 0, bobZ: 0, roll: 0, twist: 0,\n      hx: 0, hy: 0, hz: 0, sy: 1, sxz: 1,\n      browA: 0, browY: 0,\n    };\n    let lean = 0;\n\n    if (mode === 'run' || mode === 'throwRun') {\n      const speed = opt.speed || 0;\n      this.phase += dt * (4 + speed * 1.9) * this._tempo;\n      const amp = Math.min(1, speed / 9) * 1.15 + 0.15;\n      const s = Math.sin(this.phase);\n      const c = Math.cos(this.phase);\n      P.hipL = s * amp;\n      P.hipR = -s * amp;\n      P.kneeL = 0.3 * amp + Math.max(0, -c) * 1.5 * amp;\n      P.kneeR = 0.3 * amp + Math.max(0, c) * 1.5 * amp;\n      if (mode === 'throwRun') {\n        P.shL = s * amp * 0.85;\n        P.elL = -(0.8 + Math.abs(s) * 0.3);\n        P.shR = -2.4; P.elR = -0.35; P.armZR = -0.25; P.armZL = 0.2;\n      } else {\n        P.shL = -s * amp;\n        P.shR = s * amp;\n        P.elL = -(0.85 + Math.abs(s) * 0.35 * amp);\n        P.elR = -(0.85 + Math.abs(s) * 0.35 * amp);\n        P.armZL = 0.2; P.armZR = -0.2;\n      }\n      P.bobY = Math.abs(c) * 0.32 * amp;   // extra Sensible bounce\n      lean = Math.min(0.28, speed * 0.026);\n      P.roll = s * 0.05 * amp;\n      P.twist = s * 0.09 * amp;\n      P.hx = -lean * 0.5;\n      P.hz = -s * 0.09 * amp;\n      P.sy = 1 + Math.abs(c) * 0.05 * amp;\n      P.sxz = 1 - Math.abs(c) * 0.02 * amp;\n      P.browA = Math.min(0.3, speed * 0.03);\n      P.browY = -0.04;\n    } else if (mode === 'air') {\n      const pedal = Math.sin(t * 9);\n      P.hipL = 0.95 + pedal * 0.25;  P.kneeL = 0.4;\n      P.hipR = -0.8 - pedal * 0.25;  P.kneeR = 1.5;\n      P.shL = -2.3; P.shR = -1.5; P.elL = -0.6; P.elR = -0.9;\n      P.armZL = -0.35; P.armZR = 0.35;\n      lean = 0.12;\n      P.hx = -0.25;\n      P.sy = 1.07; P.sxz = 0.96;\n      P.browY = 0.1;\n    } else if (mode === 'crouch') {\n      P.bobY = -0.8;\n      P.hipL = 1.25; P.kneeL = 1.7;\n      P.hipR = -0.45; P.kneeR = 0.55;\n      P.shL = -0.9; P.shR = 0.6; P.elL = -0.5; P.elR = -0.4;\n      lean = 0.5;\n      P.hx = -0.55;\n      P.sy = 0.9; P.sxz = 1.06;\n      P.browA = 0.3;\n    } else if (mode === 'celebrate') {\n      const seg = Math.floor(mt / 1.7) % 4;\n      const tp = (mt % 1.7) / 1.7;\n      P.browY = 0.12;\n      if (seg === 0) {\n        const hop = Math.abs(Math.sin(mt * 6.5));\n        const pump = Math.sin(mt * 10);\n        P.bobY = hop * 0.95;\n        P.shL = -2.9 + pump * 0.6; P.shR = -2.9 - pump * 0.6;\n        P.elL = -0.45; P.elR = -0.45;\n        P.armZL = -0.5; P.armZR = 0.5;\n        P.hipL = hop * 0.35; P.hipR = -hop * 0.35;\n        P.kneeL = hop * 0.5; P.kneeR = hop * 0.5;\n        P.hz = Math.sin(mt * 5) * 0.22;\n        P.hx = -0.2;\n        P.sy = 1 + Math.cos(mt * 6.5) * 0.06;\n      } else if (seg === 1) {\n        P.bobY = Math.abs(Math.sin(mt * 3)) * 0.25;\n        P.shL = -2.75; P.shR = -2.75;\n        P.elL = -0.12; P.elR = -0.12;\n        P.armZL = -0.75; P.armZR = 0.75;\n        P.hx = -0.5;\n        lean = -0.12;\n        P.hipL = 0.1; P.hipR = -0.1;\n      } else if (seg === 2) {\n        const sway = Math.sin(mt * 6);\n        P.twist = sway * 0.55;\n        P.bobY = Math.abs(Math.sin(mt * 6)) * 0.35;\n        P.shL = -2.2; P.shR = -2.2;\n        P.elL = -1.15; P.elR = -1.15;\n        P.armZL = -0.35; P.armZR = 0.35;\n        P.hz = sway * 0.15;\n        P.kneeL = 0.4; P.kneeR = 0.4;\n        P.roll = sway * 0.06;\n      } else {\n        const th = -Math.PI * 2 * tp;\n        const H = 2.6; // waist height on the stocky build\n        P.bobY = Math.sin(Math.PI * tp) * 1.6 + H * (1 - Math.cos(th));\n        P.bobZ = -H * Math.sin(th);\n        lean = th;\n        P.hipL = 1.1; P.hipR = 1.1;\n        P.kneeL = 1.9; P.kneeR = 1.9;\n        P.shL = -0.5; P.shR = -0.5;\n        P.elL = -1.8; P.elR = -1.8;\n        P.sy = 0.95; P.sxz = 1.02;\n        P.hx = -0.3;\n      }\n    } else if (mode === 'dejected') {\n      const seg = Math.floor(mt / 2.2) % 2;\n      const w = Math.sin(mt * 1.2);\n      P.browA = -0.35;\n      P.browY = 0.06;\n      if (seg === 0) {\n        P.bobY = -0.3;\n        P.hipL = 0.12; P.hipR = -0.08;\n        P.kneeL = 0.25; P.kneeR = 0.2;\n        P.shL = 0.45; P.shR = 0.45;\n        P.elL = -0.15; P.elR = -0.15;\n        P.armZL = 0.05; P.armZR = -0.05;\n        lean = 0.3;\n        P.hx = 0.6;\n        P.hz = w * 0.06;\n        P.roll = w * 0.03;\n      } else {\n        P.bobY = -0.18;\n        P.shL = -2.6; P.shR = -2.6;\n        P.elL = -2.0; P.elR = -2.0;\n        P.armZL = -0.2; P.armZR = 0.2;\n        P.hx = 0.45;\n        P.hz = w * 0.08;\n        P.roll = w * 0.04;\n        P.kneeL = 0.2; P.kneeR = 0.2;\n      }\n    } else if (mode === 'exhausted') {\n      const breath = Math.sin(mt * 4.2);\n      P.bobY = -0.5;\n      P.hipL = 0.35; P.hipR = 0.35;\n      P.kneeL = 0.55; P.kneeR = 0.55;\n      P.shL = 0.95; P.shR = 0.95;\n      P.elL = -0.25; P.elR = -0.25;\n      P.armZL = 0.15; P.armZR = -0.15;\n      lean = 0.55;\n      P.hx = 0.35 + breath * 0.1;\n      P.sy = 1 + breath * 0.035;\n      P.browA = -0.2;\n      P.browY = 0.1;\n    } else if (mode === 'charge') {\n      P.bobY = -0.3;\n      P.hipL = 0.7; P.kneeL = 0.9;\n      P.hipR = -0.25; P.kneeR = 0.35;\n      P.shL = -0.7; P.elL = -0.5;\n      P.shR = opt.jav ? -2.6 : -0.7; P.elR = opt.jav ? -0.3 : -0.5;\n      lean = -0.14;\n      P.hx = -0.3;\n      P.sy = 0.95; P.sxz = 1.03;\n      P.browA = 0.35;\n    } else if (mode === 'spin') {\n      P.bobY = -0.45;\n      P.hipL = 0.5; P.kneeL = 0.6;\n      P.hipR = -0.3; P.kneeR = 0.5;\n      P.shL = -1.5; P.shR = -1.5; P.elL = -0.25; P.elR = -0.25;\n      P.armZL = -0.15; P.armZR = 0.15;\n      lean = 0.14;\n      P.hz = 0.1;\n      P.browA = 0.3;\n    } else { // idle — shifty and alive, waves at the crowd now and then\n      const w = Math.sin(t * 1.15 + this.phase);\n      P.bobY = Math.sin(t * 2.4 + this.phase) * 0.08;\n      P.shL = Math.sin(t * 2.4 + this.phase) * 0.12;\n      P.shR = -Math.sin(t * 2.4 + this.phase) * 0.12;\n      P.roll = w * 0.045;\n      P.hy = Math.sin(t * 0.65 + this.phase * 2) * 0.55;\n      P.hz = w * 0.05;\n      const waveCycle = (t + this.phase * 3) % 9;\n      if (waveCycle < 1.6) {\n        const k = Math.min(1, waveCycle / 0.3) * Math.min(1, (1.6 - waveCycle) / 0.3);\n        P.shR = -2.6 * k;\n        P.elR = (-0.4 + Math.sin(t * 11) * 0.4) * k;\n        P.armZR = 0.4 * k;\n        P.hy = 0.3 * k;\n        P.hz = 0.12 * k;\n        P.browY = 0.1 * k;\n      }\n    }\n\n    if (this._impact > 0) {\n      this._impact -= dt;\n      P.sy *= 0.85;\n      P.sxz *= 1.1;\n    }\n\n    const fast = mode === 'run' || mode === 'throwRun' || mode === 'spin' || mode === 'air';\n    const k = Math.min(1, dt * (fast ? 30 : 12));\n    const s = this._s;\n    for (const key in s) s[key] += (P[key] - s[key]) * k;\n\n    // blinking (all modes)\n    this._blink += dt;\n    if (this._blink > this._nextBlink + 0.13) {\n      this._blink = 0;\n      this._nextBlink = 1.5 + Math.random() * 3;\n    }\n    const blinking = this._blink > this._nextBlink;\n    this.eyeL.scale.y = this.eyeR.scale.y = blinking ? 0.12 : 1;\n\n    this.browL.rotation.z = -s.browA;\n    this.browR.rotation.z = s.browA;\n    this.browL.position.y = this.browR.position.y = 1.62 + s.browY;\n\n    this.legL.rotation.x = s.hipL;\n    this.legR.rotation.x = s.hipR;\n    this.kneeL.rotation.x = s.kneeL;\n    this.kneeR.rotation.x = s.kneeR;\n    this.ankleL.rotation.x = Math.max(-1.1, Math.min(1.1, -(s.hipL + s.kneeL) * 0.5));\n    this.ankleR.rotation.x = Math.max(-1.1, Math.min(1.1, -(s.hipR + s.kneeR) * 0.5));\n    this.armL.rotation.x = s.shL;\n    this.armR.rotation.x = s.shR;\n    this.elbowL.rotation.x = s.elL;\n    this.elbowR.rotation.x = s.elR;\n    this.armL.rotation.z = s.armZL;\n    this.armR.rotation.z = s.armZR;\n    this.body.position.y = s.bobY;\n    this.body.position.z = s.bobZ;\n    this.body.rotation.x = lean;\n    this.body.rotation.z = s.roll;\n    this.body.rotation.y = s.twist;\n    this.body.scale.set(s.sxz, s.sy, s.sxz);\n    this.head.rotation.set(s.hx, s.hy, s.hz);\n  }\n}\n"},{"name":"audio.js","content":"// audio.js — the stadium soundscape, all synthesized in WebAudio:\n//  - warm crowd murmur that breathes (brown noise + slow LFO, no hiss)\n//  - random whoops / shouts / whistles / clap bursts / air horns, stereo-\n//    panned around the bowl, more frequent as excitement rises\n//  - cheers on a dedicated swell channel\n//  - footsteps synced to the player's stride\nexport const audio = {\n  ctx: null, master: null, murmurGain: null, cheerGain: null,\n  _ambT: 0,\n\n  ensure() {\n    if (this.ctx) { if (this.ctx.state === 'suspended') this.ctx.resume(); return; }\n    const AC = window.AudioContext || window.webkitAudioContext;\n    if (!AC) return;\n    this.ctx = new AC();\n    this.master = this.ctx.createGain();\n    this.master.gain.value = 1;\n    this.master.connect(this.ctx.destination);\n\n    // shared noise buffers\n    const sr = this.ctx.sampleRate;\n    const len = sr * 2;\n    this._white = this.ctx.createBuffer(1, len, sr);\n    const wd = this._white.getChannelData(0);\n    for (let i = 0; i < len; i++) wd[i] = Math.random() * 2 - 1;\n    this._brown = this.ctx.createBuffer(1, len, sr);\n    const bd = this._brown.getChannelData(0);\n    let lastV = 0;\n    for (let i = 0; i < len; i++) {\n      lastV = (lastV + 0.02 * (Math.random() * 2 - 1)) / 1.02;\n      bd[i] = lastV * 3.5;\n    }\n\n    // murmur bed: brown noise, warm lowpass, slow breathing LFO\n    const mSrc = this.ctx.createBufferSource();\n    mSrc.buffer = this._brown; mSrc.loop = true;\n    const mLp = this.ctx.createBiquadFilter();\n    mLp.type = 'lowpass'; mLp.frequency.value = 340; mLp.Q.value = 0.4;\n    this.murmurGain = this.ctx.createGain();\n    this.murmurGain.gain.value = 0.05;\n    mSrc.connect(mLp).connect(this.murmurGain).connect(this.master);\n    mSrc.start();\n    const lfo = this.ctx.createOscillator();\n    lfo.frequency.value = 0.13;\n    const lfoAmt = this.ctx.createGain();\n    lfoAmt.gain.value = 0.018;\n    lfo.connect(lfoAmt).connect(this.murmurGain.gain);\n    lfo.start();\n\n    // cheer swell channel\n    const cSrc = this.ctx.createBufferSource();\n    cSrc.buffer = this._white; cSrc.loop = true;\n    const cBp = this.ctx.createBiquadFilter();\n    cBp.type = 'bandpass'; cBp.frequency.value = 850; cBp.Q.value = 0.6;\n    this.cheerGain = this.ctx.createGain();\n    this.cheerGain.gain.value = 0;\n    cSrc.connect(cBp).connect(this.cheerGain).connect(this.master);\n    cSrc.start();\n  },\n\n  _out(pan) {\n    // panned route into the master bus\n    if (!this.ctx.createStereoPanner) return this.master;\n    const p = this.ctx.createStereoPanner();\n    p.pan.value = pan != null ? pan : (Math.random() * 2 - 1) * 0.8;\n    p.connect(this.master);\n    return p;\n  },\n\n  _noiseHit({ buf = 'white', dur = 0.2, type = 'bandpass', freq = 800, q = 1, vol = 0.1, pan = null, attack = 0.01 }) {\n    if (!this.ctx) return;\n    const src = this.ctx.createBufferSource();\n    src.buffer = buf === 'brown' ? this._brown : this._white;\n    src.loop = true;\n    src.playbackRate.value = 0.9 + Math.random() * 0.2;\n    const f = this.ctx.createBiquadFilter();\n    f.type = type; f.frequency.value = freq; f.Q.value = q;\n    const g = this.ctx.createGain();\n    const t = this.ctx.currentTime;\n    g.gain.setValueAtTime(0.0001, t);\n    g.gain.linearRampToValueAtTime(vol, t + attack);\n    g.gain.exponentialRampToValueAtTime(0.0001, t + attack + dur);\n    src.connect(f).connect(g).connect(this._out(pan));\n    src.start(); src.stop(t + attack + dur + 0.05);\n  },\n\n  beep(freq = 440, dur = 0.14, type = 'square', vol = 0.16) {\n    if (!this.ctx) return;\n    const o = this.ctx.createOscillator();\n    const g = this.ctx.createGain();\n    o.type = type; o.frequency.value = freq;\n    g.gain.setValueAtTime(vol, this.ctx.currentTime);\n    g.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + dur);\n    o.connect(g).connect(this.master);\n    o.start(); o.stop(this.ctx.currentTime + dur);\n  },\n\n  tick() { this.beep(1300, 0.05, 'square', 0.08); },\n\n  gun() {\n    if (!this.ctx) return;\n    // crack + low body\n    this._noiseHit({ dur: 0.16, type: 'highpass', freq: 900, vol: 0.4, pan: 0, attack: 0.002 });\n    const o = this.ctx.createOscillator();\n    const g = this.ctx.createGain();\n    o.type = 'sine';\n    const t = this.ctx.currentTime;\n    o.frequency.setValueAtTime(120, t);\n    o.frequency.exponentialRampToValueAtTime(35, t + 0.22);\n    g.gain.setValueAtTime(0.5, t);\n    g.gain.exponentialRampToValueAtTime(0.001, t + 0.25);\n    o.connect(g).connect(this.master);\n    o.start(); o.stop(t + 0.26);\n  },\n\n  cheer() {\n    if (!this.ctx || !this.cheerGain) return;\n    const t = this.ctx.currentTime;\n    this.cheerGain.gain.cancelScheduledValues(t);\n    this.cheerGain.gain.setValueAtTime(Math.max(this.cheerGain.gain.value, 0.001), t);\n    this.cheerGain.gain.linearRampToValueAtTime(0.5, t + 0.12);\n    this.cheerGain.gain.exponentialRampToValueAtTime(0.001, t + 3.6);\n    // a few voices leap out of the roar\n    for (let i = 0; i < 4; i++) setTimeout(() => this.whoop(0.09), i * 160 + Math.random() * 120);\n    setTimeout(() => this.clapBurst(7), 250);\n  },\n\n  thud() {\n    this.beep(80, 0.18, 'sine', 0.32);\n    this._noiseHit({ buf: 'brown', dur: 0.12, type: 'lowpass', freq: 250, vol: 0.25, pan: 0, attack: 0.004 });\n  },\n\n  whoosh() {\n    if (!this.ctx) return;\n    const src = this.ctx.createBufferSource();\n    src.buffer = this._white; src.loop = true;\n    const f = this.ctx.createBiquadFilter();\n    f.type = 'bandpass'; f.Q.value = 1.2;\n    const t = this.ctx.currentTime;\n    f.frequency.setValueAtTime(300, t);\n    f.frequency.exponentialRampToValueAtTime(1600, t + 0.28);\n    const g = this.ctx.createGain();\n    g.gain.setValueAtTime(0.0001, t);\n    g.gain.linearRampToValueAtTime(0.12, t + 0.08);\n    g.gain.exponentialRampToValueAtTime(0.0001, t + 0.32);\n    src.connect(f).connect(g).connect(this.master);\n    src.start(); src.stop(t + 0.36);\n  },\n\n  // footstep on the track — quiet tap + tiny thump, pitch-jittered\n  step() {\n    if (!this.ctx) return;\n    this._noiseHit({ dur: 0.045, type: 'lowpass', freq: 420 + Math.random() * 160, vol: 0.055, pan: 0, attack: 0.003 });\n    this.beep(85 + Math.random() * 25, 0.05, 'sine', 0.05);\n  },\n\n  // ---- crowd one-shots, panned around the bowl\n  whoop(vol = 0.07) {\n    if (!this.ctx) return;\n    const o = this.ctx.createOscillator();\n    o.type = 'triangle';\n    const t = this.ctx.currentTime;\n    const f0 = 320 + Math.random() * 220;\n    o.frequency.setValueAtTime(f0, t);\n    o.frequency.exponentialRampToValueAtTime(f0 * (1.8 + Math.random() * 0.5), t + 0.18);\n    o.frequency.exponentialRampToValueAtTime(f0 * 1.2, t + 0.38);\n    const g = this.ctx.createGain();\n    g.gain.setValueAtTime(0.0001, t);\n    g.gain.linearRampToValueAtTime(vol, t + 0.05);\n    g.gain.exponentialRampToValueAtTime(0.0001, t + 0.42);\n    o.connect(g).connect(this._out());\n    o.start(); o.stop(t + 0.45);\n  },\n\n  shout() {\n    this._noiseHit({ dur: 0.28, type: 'bandpass', freq: 550 + Math.random() * 750, q: 1.6, vol: 0.05 + Math.random() * 0.045, attack: 0.02 });\n  },\n\n  whistle() {\n    if (!this.ctx) return;\n    const base = 2100 + Math.random() * 700;\n    for (let i = 0; i < 2; i++) {\n      setTimeout(() => this.beep(base + (Math.random() - 0.5) * 120, 0.09, 'sine', 0.035), i * 150);\n    }\n  },\n\n  clapBurst(n = 5) {\n    if (!this.ctx) return;\n    let at = 0;\n    for (let i = 0; i < n; i++) {\n      at += 45 + Math.random() * 55;\n      setTimeout(() => this._noiseHit({ dur: 0.022, type: 'highpass', freq: 1600, vol: 0.05, attack: 0.002 }), at);\n    }\n  },\n\n  horn() {\n    if (!this.ctx) return;\n    const t = this.ctx.currentTime;\n    const g = this.ctx.createGain();\n    g.gain.setValueAtTime(0.0001, t);\n    g.gain.linearRampToValueAtTime(0.055, t + 0.06);\n    g.gain.setValueAtTime(0.055, t + 0.55);\n    g.gain.exponentialRampToValueAtTime(0.0001, t + 0.75);\n    const lp = this.ctx.createBiquadFilter();\n    lp.type = 'lowpass'; lp.frequency.value = 900;\n    const out = this._out();\n    lp.connect(g).connect(out);\n    for (const f of [233, 236.5]) {\n      const o = this.ctx.createOscillator();\n      o.type = 'sawtooth'; o.frequency.value = f;\n      o.connect(lp);\n      o.start(); o.stop(t + 0.8);\n    }\n  },\n\n  // called every frame: murmur level + random crowd chatter\n  setAmbience(excitement) {\n    if (this.murmurGain) this.murmurGain.gain.value = 0.05 + excitement * 0.16;\n  },\n\n  ambient(dt, excitement) {\n    if (!this.ctx) return;\n    this._ambT -= dt;\n    if (this._ambT > 0) return;\n    this._ambT = 0.35 + Math.random() * Math.max(0.5, 2.8 - excitement * 2.2);\n    const r = Math.random();\n    if (excitement > 0.55 && r < 0.07) this.horn();\n    else if (r < 0.42) this.whoop();\n    else if (r < 0.72) this.shout();\n    else if (r < 0.83) this.whistle();\n    else this.clapBurst(3 + Math.floor(Math.random() * 3 + excitement * 4));\n  },\n\n  // the WORLD RECORD ditty — dah dah da-da dah!\n  fanfare() {\n    if (!this.ctx) return;\n    const notes = [\n      [523.25, 0.00, 0.16],\n      [523.25, 0.20, 0.16],\n      [587.33, 0.40, 0.11],\n      [659.25, 0.52, 0.11],\n      [783.99, 0.66, 0.55],\n    ];\n    for (const [f, at, dur] of notes) {\n      setTimeout(() => {\n        this.beep(f, dur, 'square', 0.2);\n        this.beep(f / 2, dur, 'triangle', 0.14);\n      }, at * 1000);\n    }\n  },\n};\n"},{"name":"career.js","content":"// career.js — persistent per-initials season stats across meets\nconst KEY = 'svo-career-v1';\n\nfunction load() {\n  try { return JSON.parse(localStorage.getItem(KEY) || '{}') || {}; }\n  catch (e) { return {}; }\n}\n\nexport function recordMeet(initials, placement, pts) {\n  const c = load();\n  const s = c[initials] || { meets: 0, golds: 0, silvers: 0, bronzes: 0, seasonPts: 0, bestMeet: 0 };\n  s.meets++;\n  if (placement === 1) s.golds++;\n  else if (placement === 2) s.silvers++;\n  else if (placement === 3) s.bronzes++;\n  s.seasonPts += pts;\n  s.bestMeet = Math.max(s.bestMeet, pts);\n  c[initials] = s;\n  try { localStorage.setItem(KEY, JSON.stringify(c)); } catch (e) { /* session-only */ }\n  return s;\n}\n\nexport function get(initials) { return load()[initials] || null; }\n\nexport function standings(limit = 3) {\n  return Object.entries(load())\n    .sort((a, b) => b[1].seasonPts - a[1].seasonPts)\n    .slice(0, limit);\n}\n\nexport function fmtLine(initials, s) {\n  return initials + ' — MEET ' + s.meets +\n    ' · \\u{1F947}' + s.golds + ' \\u{1F948}' + s.silvers + ' \\u{1F949}' + s.bronzes +\n    ' · SEASON ' + s.seasonPts.toLocaleString() + ' PTS';\n}\n"},{"name":"core.js","content":"// core.js — renderer, scene, camera, shared game state, roster\nimport { THREE, clamp } from './lib.js';\nimport { buildStadium, CENTER } from './world.js';\nimport { Athlete } from './athlete.js';\n\nexport const app = document.getElementById('app');\n\nexport const IS_TOUCH = window.matchMedia('(pointer: coarse)').matches || 'ontouchstart' in window;\n\nexport const renderer = new THREE.WebGLRenderer({ antialias: true });\nrenderer.setPixelRatio(Math.min(window.devicePixelRatio, IS_TOUCH ? 1.3 : 1.75));\nrenderer.setSize(window.innerWidth, window.innerHeight);\nrenderer.shadowMap.enabled = true;\nrenderer.shadowMap.type = THREE.PCFSoftShadowMap;\nrenderer.toneMapping = THREE.ACESFilmicToneMapping;\nrenderer.toneMappingExposure = 1.12;\nrenderer.outputColorSpace = THREE.SRGBColorSpace;\napp.insertBefore(renderer.domElement, app.firstChild);\n\nexport const scene = new THREE.Scene();\nexport const camera = new THREE.PerspectiveCamera(30, window.innerWidth / window.innerHeight, 1, 1200);\ncamera.position.set(0, 170, 210);\ncamera.lookAt(CENTER);\n\nwindow.addEventListener('resize', () => {\n  camera.aspect = window.innerWidth / window.innerHeight;\n  camera.updateProjectionMatrix();\n  renderer.setSize(window.innerWidth, window.innerHeight);\n});\n\nexport const stadium = buildStadium(scene);\n\nexport const ROSTER = [\n  { name: 'YOU', kit: '#ff4b3a', shorts: '#ffffff', skin: '#c98e5a', hair: '#20140e', player: true },\n  { name: 'DALEY T.', kit: '#2563eb', shorts: '#ffffff', skin: '#8a5a33', hair: '#101010' },\n  { name: 'HYPER K.', kit: '#10b981', shorts: '#111827', skin: '#e8b48c', hair: '#3b2b1a', hairStyle: 'spike' },\n  { name: 'SENSI S.', kit: '#f59e0b', shorts: '#111827', skin: '#f0c8a0', hair: '#5b3a1e', hairStyle: 'band' },\n  { name: 'TURBO T.', kit: '#c026d3', shorts: '#ffffff', skin: '#6b4226', hair: '#101010', hairStyle: 'afro' },\n  { name: 'BLAZE B.', kit: '#0ea5e9', shorts: '#0b2540', skin: '#e8b48c', hair: '#c9c9c9' },\n  { name: 'MASH M.', kit: '#1f2937', shorts: '#ffffff', skin: '#f0c8a0', hair: '#7a3b12', hairStyle: 'spike' },\n  { name: 'ROCKET R.', kit: '#f5f1e6', shorts: '#274690', skin: '#8a5a33', hair: '#2a1b12', hairStyle: 'pony' },\n];\n\nexport const athletes = ROSTER.map((r, i) => {\n  const a = new Athlete({ ...r, num: i + 1 });\n  a.player = !!r.player;\n  scene.add(a.group);\n  return a;\n});\nexport const player = athletes[0];\n\n// per-event prop group — cleared between events\nexport const props = new THREE.Group();\nscene.add(props);\nexport function clearProps() { while (props.children.length) props.remove(props.children[0]); }\n\n// ---------------- shared game state\nexport const game = {\n  state: 'title',       // title | card | countdown | play | results | podium\n  eventIndex: 0,\n  totals: {},\n  timeScale: 1, tsTarget: 1, tsTimer: 0,\n  excitement: 0.2,\n  cd: { t: 0, step: -1 },\n  shake: 0,\n  time: 0,\n};\nROSTER.forEach((r) => { game.totals[r.name] = 0; });\n\nexport const cam = {\n  pos: camera.position.clone(),\n  look: CENTER.clone(),\n  mode: 'orbit',\n  orbitAngle: 0.6,\n};\n\n// late-bound hooks set by main.js (avoids circular imports)\nexport const bus = { finishEvent: null };\n\nexport function setSlowmo(scale, realSeconds) {\n  game.tsTarget = scale;\n  game.tsTimer = realSeconds;\n}\n\n// Sensible-Soccer top-down follow camera: nearly straight overhead, trailing\n// just behind the athlete along their direction of travel so they always run\n// \"up the screen\". Pass dx/dz (path tangent) for lap events; defaults to +X.\nexport function followCam(px, pz, opt = {}) {\n  const h = opt.h != null ? opt.h : 70;\n  const back = opt.back != null ? opt.back : 13;\n  const lead = opt.lead != null ? opt.lead : 5;\n  const dx = opt.dx != null ? opt.dx : 1;\n  const dz = opt.dz != null ? opt.dz : 0;\n  return {\n    pos: new THREE.Vector3(px - dx * back, h, pz - dz * back),\n    look: new THREE.Vector3(px + dx * lead, 0, pz + dz * lead),\n  };\n}\n\nexport { clamp };\n"},{"name":"fx.js","content":"// fx.js — instanced particle system (confetti, sand, dirt)\nimport { THREE } from './lib.js';\nimport { scene } from './core.js';\n\nexport const CONFETTI = ['#ff4b3a', '#ffb020', '#4aa3ff', '#7ce34a', '#f5f1e6', '#c084fc'];\nexport const SAND = ['#e6c98c', '#dfc083', '#d4b478'];\nexport const DIRT = ['#5fae3f', '#4b9231', '#7a5c3a'];\nexport const DUST = ['#e5a58c', '#d9927b', '#caa08e'];\n\nconst P_MAX = 800;\nconst pGeo = new THREE.BoxGeometry(0.22, 0.22, 0.22);\nconst pMesh = new THREE.InstancedMesh(pGeo, new THREE.MeshStandardMaterial({ roughness: 0.8 }), P_MAX);\npMesh.frustumCulled = false;\nscene.add(pMesh);\nconst parts = [];\nconst pDummy = new THREE.Object3D();\nconst pColor = new THREE.Color();\n\nexport function spawnParticles(pos, colors, n, opt = {}) {\n  for (let i = 0; i < n; i++) {\n    if (parts.length >= P_MAX) parts.shift();\n    parts.push({\n      x: pos.x + (Math.random() - 0.5) * (opt.spread || 1),\n      y: pos.y + Math.random() * 0.5,\n      z: pos.z + (Math.random() - 0.5) * (opt.spread || 1),\n      vx: (Math.random() - 0.5) * (opt.vel || 5),\n      vy: (opt.up != null ? opt.up : 4) * (0.4 + Math.random() * 0.8),\n      vz: (Math.random() - 0.5) * (opt.vel || 5),\n      g: opt.g != null ? opt.g : 14,\n      life: (opt.life || 1) * (0.6 + Math.random() * 0.7),\n      rot: Math.random() * Math.PI,\n      rv: (Math.random() - 0.5) * 8,\n      s: (opt.size || 1) * (0.6 + Math.random() * 0.8),\n      color: colors[Math.floor(Math.random() * colors.length)],\n    });\n  }\n}\n\nexport function updateParticles(dt) {\n  for (let i = parts.length - 1; i >= 0; i--) {\n    const p = parts[i];\n    p.life -= dt;\n    if (p.life <= 0 || p.y < -1) { parts.splice(i, 1); continue; }\n    p.vy -= p.g * dt;\n    p.x += p.vx * dt; p.y += p.vy * dt; p.z += p.vz * dt;\n    p.rot += p.rv * dt;\n  }\n  for (let i = 0; i < P_MAX; i++) {\n    if (i < parts.length) {\n      const p = parts[i];\n      pDummy.position.set(p.x, p.y, p.z);\n      pDummy.rotation.set(p.rot, p.rot * 0.7, 0);\n      pDummy.scale.setScalar(p.s);\n      pDummy.updateMatrix();\n      pMesh.setMatrixAt(i, pDummy.matrix);\n      pMesh.setColorAt(i, pColor.set(p.color));\n    } else {\n      pDummy.position.set(0, -50, 0);\n      pDummy.scale.setScalar(0.001);\n      pDummy.updateMatrix();\n      pMesh.setMatrixAt(i, pDummy.matrix);\n    }\n  }\n  pMesh.instanceMatrix.needsUpdate = true;\n  if (pMesh.instanceColor) pMesh.instanceColor.needsUpdate = true;\n}\n"},{"name":"gamepad.js","content":"// gamepad.js — poll pads each frame and translate button edges into the same\n// synthetic key events the touch controls use. D-pad ◀ ▶ (or the X/B face\n// buttons) mash, A = SPACE, Start = ENTER, stick also steers menus.\nconst MAP = [\n  [14, 'ArrowLeft'], [15, 'ArrowRight'],  // d-pad\n  [2, 'ArrowLeft'], [1, 'ArrowRight'],    // X / B — two-button face mashing\n  [0, 'Space'],                           // A\n  [9, 'Enter'], [8, 'Enter'],             // start / select\n  [12, 'ArrowUp'], [13, 'ArrowDown'],     // d-pad up/down (menus)\n];\n\nconst state = new Map();\nlet announced = false;\n\nconst fire = (code, type) =>\n  window.dispatchEvent(new KeyboardEvent(type, { code, bubbles: true }));\n\nexport function pollGamepads(onConnect) {\n  const pads = navigator.getGamepads ? navigator.getGamepads() : [];\n  for (const pad of pads) {\n    if (!pad || !pad.connected) continue;\n    if (!announced) { announced = true; if (onConnect) onConnect(pad.id); }\n    let st = state.get(pad.index);\n    if (!st) { st = {}; state.set(pad.index, st); }\n    for (const [btn, code] of MAP) {\n      const k = btn + ':' + code;\n      const down = !!(pad.buttons[btn] && pad.buttons[btn].pressed);\n      if (down !== !!st[k]) {\n        st[k] = down;\n        fire(code, down ? 'keydown' : 'keyup');\n      }\n    }\n    // left stick as ◀ ▶ with a dead zone\n    const ax = pad.axes[0] || 0;\n    const dir = ax < -0.55 ? 'ArrowLeft' : ax > 0.55 ? 'ArrowRight' : null;\n    if (st._axis !== dir) {\n      if (st._axis) fire(st._axis, 'keyup');\n      if (dir) fire(dir, 'keydown');\n      st._axis = dir;\n    }\n  }\n}\n"},{"name":"hud.js","content":"// hud.js — DOM overlay helpers\nconst $ = (id) => document.getElementById(id);\nconst clamp01 = (v) => Math.max(0, Math.min(1, v));\n\nexport const hud = {\n  root: $('hud'), timer: $('timer'), eventNo: $('event-no'), eventName: $('event-name'),\n  attempt: $('attempt-chip'), msg: $('center-msg'), popup: $('popup'),\n  powerWrap: $('power-wrap'), powerFill: $('power-fill'), powerLabel: $('power-label'),\n  angleWrap: $('angle-wrap'), angleFill: $('angle-fill'), angleLabel: $('angle-label'),\n  card: $('card'), cardNo: $('card-no'), cardName: $('card-name'), cardDesc: $('card-desc'),\n  cardRecords: $('card-records'), cardQual: $('card-qual'),\n  replayBadge: $('replay-badge'), careerLine: $('career-line'), titleStandings: $('title-standings'),\n  nameEntry: $('name-entry'),\n  wheels: [$('wheel-0'), $('wheel-1'), $('wheel-2')],\n  eventSelect: $('event-select'), evGrid: $('ev-grid'), evCount: $('ev-count'), evStart: $('ev-start'),\n  results: $('results'), resultsTitle: $('results-title'), resultsTable: $('results-table'),\n  resultsKicker: $('results-kicker'), resultsHint: $('results-hint'),\n  title: $('title'),\n};\n\nexport function showMsg(text, cls = '') {\n  hud.msg.textContent = text;\n  hud.msg.className = '';\n  void hud.msg.offsetWidth;\n  hud.msg.className = 'pop ' + cls;\n}\n\nexport function showPopup(text, bad = false) {\n  hud.popup.textContent = text;\n  hud.popup.className = bad ? 'bad' : '';\n  void hud.popup.offsetWidth;\n  hud.popup.className = (bad ? 'bad ' : '') + 'rise';\n}\n\nexport function setPower(v01, label) {\n  hud.powerFill.style.width = (clamp01(v01) * 100).toFixed(1) + '%';\n  if (label && hud.powerLabel) hud.powerLabel.textContent = label;\n}\n\n// second gauge: angle for jumps/throws, stamina for distance races\nexport function setGauge(pct01, label, show) {\n  hud.angleWrap.classList.toggle('hidden', !show);\n  if (show) {\n    hud.angleFill.style.width = (clamp01(pct01) * 100).toFixed(1) + '%';\n    hud.angleLabel.textContent = label;\n  }\n}\n\nexport function showTimer(show) { hud.timer.classList.toggle('hidden', !show); }\nexport function setTimer(text) { hud.timer.textContent = text; }\n\nexport function setAttempt(n, total) {\n  if (n == null) { hud.attempt.classList.add('hidden'); return; }\n  hud.attempt.textContent = 'ATTEMPT ' + n + ' / ' + total;\n  hud.attempt.classList.remove('hidden');\n}\n"},{"name":"lib.js","content":"// lib.js — three.js + tiny shared helpers used by every module\nimport * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js';\n\nexport { THREE };\n\nexport const BOX = new THREE.BoxGeometry(1, 1, 1);\n\nexport const clamp = (v, a, b) => Math.max(a, Math.min(b, v));\n\nexport function stdMat(color, opts = {}) {\n  return new THREE.MeshStandardMaterial(Object.assign({ color, roughness: 0.92, metalness: 0.0 }, opts));\n}\n\nexport function boxMesh(w, h, d, color, opts = {}) {\n  const m = new THREE.Mesh(BOX, stdMat(color, opts));\n  m.scale.set(w, h, d);\n  m.castShadow = true;\n  m.receiveShadow = true;\n  return m;\n}\n\n// Build one InstancedMesh from a list of {x,y,z,sx,sy,sz,color,ry}\nexport function buildTiles(list, { shadowCast = false } = {}) {\n  const mat = new THREE.MeshStandardMaterial({ roughness: 0.95, metalness: 0 });\n  const mesh = new THREE.InstancedMesh(BOX, mat, list.length);\n  const d = new THREE.Object3D();\n  const c = new THREE.Color();\n  list.forEach((t, i) => {\n    d.position.set(t.x, t.y, t.z);\n    d.scale.set(t.sx, t.sy, t.sz);\n    d.rotation.set(0, t.ry || 0, 0);\n    d.updateMatrix();\n    mesh.setMatrixAt(i, d.matrix);\n    mesh.setColorAt(i, c.set(t.color));\n  });\n  if (mesh.instanceColor) mesh.instanceColor.needsUpdate = true;\n  mesh.castShadow = shadowCast;\n  mesh.receiveShadow = true;\n  return mesh;\n}\n"},{"name":"main.js","content":"// main.js — Super Voxel Olympics: game flow, input, camera, render loop\nimport { THREE, clamp } from './lib.js';\nimport {\n  renderer, scene, camera, game, cam, bus, stadium,\n  athletes, player, setSlowmo, clearProps, ROSTER, IS_TOUCH,\n} from './core.js';\nimport * as records from './records.js';\nimport { buildNPCs, updateNPCs } from './npcs.js';\nimport { CENTER } from './world.js';\nimport * as hud from './hud.js';\nimport { audio } from './audio.js';\nimport { spawnParticles, updateParticles, CONFETTI } from './fx.js';\nimport { sprint100, hurdles110 } from './events/races.js';\nimport { run200, run400, run800, relay } from './events/laps.js';\nimport { longJump, tripleJump, highJump } from './events/jumps.js';\nimport { javelin, hammer, discus, shotPut } from './events/throws.js';\nimport { poleVault } from './events/vault.js';\nimport { QUALS, QUAL_BONUS } from './events/common.js';\nimport { pollGamepads } from './gamepad.js';\nimport * as career from './career.js';\n\n// ---------------------------------------------------------------- post FX\n// depth of field on desktop only. The addons are loaded dynamically because\n// they import the bare specifier 'three' (resolved via the import map) —\n// which older iOS Safari doesn't support. Phones skip DOF anyway, so they\n// never touch the import map and render directly.\nlet renderFrame = () => renderer.render(scene, camera);\nlet bokeh = null;\nif (!IS_TOUCH) {\n  Promise.all([\n    import('three/addons/postprocessing/EffectComposer.js'),\n    import('three/addons/postprocessing/RenderPass.js'),\n    import('three/addons/postprocessing/BokehPass.js'),\n    import('three/addons/postprocessing/OutputPass.js'),\n  ]).then(([ec, rp, bp, op]) => {\n    const composer = new ec.EffectComposer(renderer);\n    composer.addPass(new rp.RenderPass(scene, camera));\n    bokeh = new bp.BokehPass(scene, camera, { focus: 70, aperture: 0.00012, maxblur: 0.0075 });\n    composer.addPass(bokeh);\n    composer.addPass(new op.OutputPass());\n    window.addEventListener('resize', () => composer.setSize(window.innerWidth, window.innerHeight));\n    renderFrame = () => composer.render();\n  }).catch(() => { /* no import-map support — plain rendering is fine */ });\n}\n\n// victory spotlight: fades in over the athlete during low celebration shots\n// and on the podium, while the stadium lights dim around them\nconst spot = new THREE.SpotLight(0xfff2cc, 0, 90, Math.PI / 6.5, 0.5, 2);\nscene.add(spot, spot.target);\nlet spotI = 0;\nconst PODIUM_ANCHOR = new THREE.Vector3(0, 1.5, 12);\ngame.fovKick = 0;\ngame.arrowT = 99;\n\n// \"that's you\" marker: your initials float behind your runner at the start\n// of track races (and after relay handoffs), then fade out\nconst markerCanvas = document.createElement('canvas');\nmarkerCanvas.width = 256;\nmarkerCanvas.height = 128;\nconst markerCtx = markerCanvas.getContext('2d');\nconst markerTex = new THREE.CanvasTexture(markerCanvas);\nmarkerTex.colorSpace = THREE.SRGBColorSpace;\nfunction drawMarker(text) {\n  markerCtx.clearRect(0, 0, 256, 128);\n  markerCtx.font = '900 82px \"Archivo Black\", sans-serif';\n  markerCtx.textAlign = 'center';\n  markerCtx.textBaseline = 'middle';\n  markerCtx.lineWidth = 16;\n  markerCtx.lineJoin = 'round';\n  markerCtx.strokeStyle = 'rgba(10, 16, 30, 0.92)';\n  markerCtx.strokeText(text, 128, 66);\n  markerCtx.fillStyle = '#ffd24a';\n  markerCtx.fillText(text, 128, 66);\n  markerTex.needsUpdate = true;\n}\ndrawMarker('YOU');\nconst marker = new THREE.Sprite(new THREE.SpriteMaterial({ map: markerTex, transparent: true }));\nmarker.scale.set(1.9, 0.95, 1);\nmarker.visible = false;\nscene.add(marker);\n\nconst EVENTS = [\n  sprint100, longJump, javelin, hurdles110, highJump,\n  run200, tripleJump, shotPut, poleVault, run400,\n  hammer, discus, relay, run800,\n];\n\nlet currentEvent = null;\nlet lineup = EVENTS.slice(); // the events you chose on the select screen\n\n// ---------------------------------------------------------------- replay\n// during play we record a lightweight trace of the player (and the event's\n// projectile, if it exposes one). When a mark lands on the record board the\n// last few seconds play back in slow motion before the results.\nconst trace = [];\nconst replay = { t: 0, end: 0, idx: 0, cb: null, prop: null, propMoved: false };\n\nfunction recordTrace() {\n  const rp = currentEvent && currentEvent.replayProp ? currentEvent.replayProp() : null;\n  const s = {\n    t: game.time,\n    px: player.group.position.x, py: player.group.position.y, pz: player.group.position.z,\n    ry: player.group.rotation.y,\n  };\n  if (rp) { s.ex = rp.position.x; s.ey = rp.position.y; s.ez = rp.position.z; }\n  trace.push(s);\n  if (trace.length > 900) trace.shift();\n}\n\nfunction startReplay(cb) {\n  if (trace.length < 10) { cb(); return; }\n  const endT = trace[trace.length - 1].t;\n  replay.t = Math.max(trace[0].t, endT - 5.2);\n  replay.end = endT;\n  replay.idx = 0;\n  replay.cb = cb;\n  replay.prop = currentEvent && currentEvent.replayProp ? currentEvent.replayProp() : null;\n  const first = trace.find((s) => s.t >= replay.t) || trace[0];\n  const last = trace[trace.length - 1];\n  replay.propMoved = !!(replay.prop && first.ex != null && last.ex != null &&\n    Math.hypot(last.ex - first.ex, last.ez - first.ez) > 4);\n  game.state = 'replay';\n  hud.hud.replayBadge.classList.remove('hidden');\n  audio.beep(520, 0.15, 'triangle', 0.14);\n}\n\nfunction updateReplay(rdt) {\n  replay.t += rdt * 0.35;\n  while (replay.idx < trace.length - 1 && trace[replay.idx + 1].t <= replay.t) replay.idx++;\n  const s = trace[replay.idx];\n  const nx = trace[Math.min(replay.idx + 1, trace.length - 1)];\n  player.group.position.set(s.px, s.py, s.pz);\n  player.group.rotation.y = s.ry;\n  const spd = Math.hypot(nx.px - s.px, nx.pz - s.pz) / Math.max(nx.t - s.t, 0.001);\n  player.update(rdt * 0.35, s.py > 0.12 ? 'air' : spd > 0.8 ? 'run' : 'idle', { speed: spd });\n  if (replay.prop && s.ex != null) replay.prop.position.set(s.ex, s.ey, s.ez);\n  // the state stays 'replay' until the callback's reveal flips it, so only\n  // fire the callback once\n  if (replay.t >= replay.end && replay.cb) {\n    hud.hud.replayBadge.classList.add('hidden');\n    const cb = replay.cb;\n    replay.cb = null;\n    cb();\n  }\n}\n\n// ---------------------------------------------------------------- camera\nfunction camDesired() {\n  if (game.state === 'replay' && trace.length) {\n    const s = trace[Math.min(replay.idx, trace.length - 1)];\n    const fx = replay.propMoved && s.ex != null ? s.ex : s.px;\n    const fy = replay.propMoved && s.ey != null ? s.ey : s.py;\n    const fz = replay.propMoved && s.ez != null ? s.ez : s.pz;\n    return {\n      pos: new THREE.Vector3(fx - 5, Math.max(fy, 1.5) + 3.4, fz + 9.5),\n      look: new THREE.Vector3(fx, Math.max(fy, 1) + 0.8, fz),\n    };\n  }\n  if (cam.mode === 'orbit') {\n    return {\n      pos: new THREE.Vector3(\n        CENTER.x + Math.cos(cam.orbitAngle) * 175,\n        150,\n        CENTER.z + Math.sin(cam.orbitAngle) * 140\n      ),\n      look: new THREE.Vector3(CENTER.x, 2, CENTER.z),\n    };\n  }\n  if (cam.mode === 'podium') {\n    const a = game.time * 0.15;\n    return {\n      pos: new THREE.Vector3(Math.sin(a) * 10, 22, 46),\n      look: new THREE.Vector3(0, 2, 12),\n    };\n  }\n  if (currentEvent && currentEvent.camTarget) return currentEvent.camTarget();\n  return { pos: cam.pos.clone(), look: cam.look.clone() };\n}\n\n// ---------------------------------------------------------------- initials\nconst ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';\nconst nameState = { slot: 0, letters: [0, 0, 0] };\n\nfunction renderWheels() {\n  hud.hud.wheels.forEach((w, i) => {\n    const li = nameState.letters[i];\n    w.querySelector('.prev').textContent = ALPHA[(li + 25) % 26];\n    w.querySelector('.cur').textContent = ALPHA[li];\n    w.querySelector('.next').textContent = ALPHA[(li + 1) % 26];\n    w.classList.toggle('active', i === nameState.slot);\n    w.classList.toggle('locked', i < nameState.slot);\n  });\n}\n\nfunction openNameEntry() {\n  hud.hud.title.classList.add('hidden');\n  hud.hud.nameEntry.classList.remove('hidden');\n  nameState.slot = 0;\n  game.state = 'name';\n  renderWheels();\n}\n\nfunction confirmName() {\n  const initials = nameState.letters.map((i) => ALPHA[i]).join('');\n  ROSTER[0].name = initials;\n  athletes[0].name = initials;\n  drawMarker(initials);\n  game.totals = {};\n  ROSTER.forEach((r) => { game.totals[r.name] = 0; });\n  hud.hud.nameEntry.classList.add('hidden');\n  audio.beep(880, 0.2, 'square', 0.18);\n  openEventSelect();\n}\n\n// ---------------------------------------------------------------- event select\nconst LINEUP_KEY = 'svo-lineup-v1';\nconst sel = { cursor: 0, on: EVENTS.map(() => true) };\ntry {\n  const savedIds = JSON.parse(localStorage.getItem(LINEUP_KEY) || 'null');\n  if (Array.isArray(savedIds) && savedIds.length) {\n    const ids = new Set(savedIds);\n    sel.on = EVENTS.map((e) => ids.has(e.id));\n    if (!sel.on.some(Boolean)) sel.on = EVENTS.map(() => true);\n  }\n} catch (e) { /* fresh default: everything on */ }\n\nconst evItems = EVENTS.map((e, i) => {\n  const el = document.createElement('div');\n  el.className = 'ev-item';\n  el.innerHTML = '<span class=\"tick\">&#10003;</span><span>' + e.name + '</span>';\n  el.addEventListener('click', () => {\n    if (game.state !== 'select') return;\n    audio.ensure();\n    sel.cursor = i;\n    sel.on[i] = !sel.on[i];\n    audio.tick();\n    refreshSelect();\n  });\n  hud.hud.evGrid.appendChild(el);\n  return el;\n});\nhud.hud.evStart.addEventListener('click', () => {\n  if (game.state === 'select') { audio.ensure(); confirmSelection(); }\n});\n\nfunction refreshSelect() {\n  evItems.forEach((el, i) => {\n    el.classList.toggle('on', sel.on[i]);\n    el.classList.toggle('cur', i === sel.cursor);\n  });\n  const n = sel.on.filter(Boolean).length;\n  hud.hud.evCount.textContent = n === 0\n    ? 'PICK AT LEAST ONE EVENT'\n    : n + ' OF ' + EVENTS.length + ' EVENTS SELECTED';\n}\n\nfunction openEventSelect() {\n  const cs = career.get(ROSTER[0].name);\n  hud.hud.careerLine.textContent = cs\n    ? career.fmtLine(ROSTER[0].name, cs)\n    : 'FIRST MEET OF THE SEASON — GOOD LUCK, ' + ROSTER[0].name + '!';\n  hud.hud.eventSelect.classList.remove('hidden');\n  game.state = 'select';\n  refreshSelect();\n}\n\nfunction confirmSelection() {\n  if (!sel.on.some(Boolean)) { refreshSelect(); return; }\n  lineup = EVENTS.filter((e, i) => sel.on[i]);\n  try { localStorage.setItem(LINEUP_KEY, JSON.stringify(lineup.map((e) => e.id))); } catch (e) { /* ok */ }\n  hud.hud.eventSelect.classList.add('hidden');\n  audio.beep(880, 0.2, 'square', 0.18);\n  game.eventIndex = 0;\n  showCard();\n}\n\nfunction selectKey(code) {\n  const N = EVENTS.length;\n  const move = (d) => {\n    sel.cursor = (sel.cursor + d + N) % N;\n    audio.tick();\n    refreshSelect();\n  };\n  if (code === 'ArrowLeft' || code === 'KeyA') move(-1);\n  else if (code === 'ArrowRight' || code === 'KeyD') move(1);\n  else if (code === 'ArrowUp' || code === 'KeyW') move(-2);\n  else if (code === 'ArrowDown' || code === 'KeyS') move(2);\n  else if (code === 'Space') {\n    sel.on[sel.cursor] = !sel.on[sel.cursor];\n    audio.beep(sel.on[sel.cursor] ? 660 : 330, 0.08, 'square', 0.12);\n    refreshSelect();\n  } else if (code === 'Enter') {\n    confirmSelection();\n  }\n}\n\nfunction nameKey(code) {\n  if (code === 'ArrowLeft' || code === 'KeyA') {\n    nameState.letters[nameState.slot] = (nameState.letters[nameState.slot] + 25) % 26;\n    audio.tick();\n    renderWheels();\n  } else if (code === 'ArrowRight' || code === 'KeyD') {\n    nameState.letters[nameState.slot] = (nameState.letters[nameState.slot] + 1) % 26;\n    audio.tick();\n    renderWheels();\n  } else if (code === 'Space' || code === 'Enter') {\n    audio.beep(660, 0.1, 'square', 0.14);\n    nameState.slot++;\n    if (nameState.slot >= 3) confirmName();\n    else renderWheels();\n  } else if (code === 'Backspace' && nameState.slot > 0) {\n    nameState.slot--;\n    renderWheels();\n  }\n}\n\n// ---------------------------------------------------------------- flow\nfunction showCard() {\n  const e = lineup[game.eventIndex];\n  currentEvent = e;\n  finishing = false;\n  trace.length = 0;\n  hud.hud.cardNo.textContent = 'EVENT ' + (game.eventIndex + 1) + ' OF ' + lineup.length;\n  hud.hud.cardName.textContent = e.name;\n  hud.hud.cardDesc.textContent = e.desc;\n  const q = QUALS[e.id];\n  hud.hud.cardQual.classList.toggle('hidden', q == null);\n  if (q != null) {\n    hud.hud.cardQual.textContent = 'QUALIFYING MARK: ' +\n      (records.isTimed(e.id) ? 'UNDER ' : 'OVER ') + records.fmtRecord(e.id, q) +\n      ' · +' + QUAL_BONUS + ' PTS';\n  }\n  hud.hud.cardRecords.innerHTML = records.getTop3(e.id).map((r, i) =>\n    '<div class=\"rec-row\">' +\n    '<span class=\"rp\">' + (i + 1) + '</span>' +\n    '<span class=\"rn\">' + r.n + '</span>' +\n    '<span class=\"rv\">' + records.fmtRecord(e.id, r.v) + '</span>' +\n    '</div>'\n  ).join('');\n  hud.hud.card.classList.remove('hidden');\n  hud.hud.results.classList.add('hidden');\n  hud.hud.root.classList.remove('hidden');\n  hud.hud.eventNo.textContent = 'EVENT ' + (game.eventIndex + 1) + ' / ' + lineup.length;\n  hud.hud.eventName.textContent = e.name;\n  stadium.scoreboard(e.name, 'EVENT ' + (game.eventIndex + 1) + ' OF ' + lineup.length);\n  e.enter();\n  cam.mode = 'orbit';\n  game.state = 'card';\n}\n\nfunction startCountdown() {\n  hud.hud.card.classList.add('hidden');\n  game.cd.t = 0;\n  game.cd.step = -1;\n  game.state = 'countdown';\n  cam.mode = 'follow';\n}\n\nfunction falseStart() {\n  hud.showMsg('FALSE START!', 'bad');\n  audio.beep(180, 0.4, 'sawtooth', 0.2);\n  game.cd.t = -1.4;\n  game.cd.step = -1;\n}\n\nfunction updateCountdown(rdt) {\n  game.cd.t += rdt;\n  const step = Math.floor(game.cd.t);\n  if (step > game.cd.step && step >= 0) {\n    game.cd.step = step;\n    if (step < 3) {\n      hud.showMsg(String(3 - step));\n      audio.beep(440, 0.12);\n    } else {\n      hud.showMsg('GO!', 'go');\n      audio.beep(880, 0.3, 'square', 0.2);\n      game.state = 'play';\n      game.fovKick = 1; // camera punch off the gun\n      currentEvent.go();\n    }\n  }\n}\n\n// during a world-record reveal the game stays in 'play' for ~2s, so an event's\n// update() can call finish() again every frame — only the first call counts\nlet finishing = false;\n\nfunction finishEvent(rows) {\n  if (finishing) return;\n  finishing = true;\n  for (const r of rows) game.totals[r.name] += r.pts;\n  const e = currentEvent;\n\n  // record check — only the player's mark goes on the boards\n  const pRow = rows.find((r) => r.player);\n  let recordRank = 0;\n  if (pRow && pRow.val > 0) {\n    recordRank = records.submit(e.id, ROSTER[0].name, pRow.val);\n  }\n\n  // qualifying bonus\n  const q = QUALS[e.id];\n  let qualified = null;\n  if (q != null && pRow) {\n    qualified = records.isTimed(e.id) ? (pRow.val > 0 && pRow.val <= q) : pRow.val >= q;\n    if (qualified) game.totals[pRow.name] += QUAL_BONUS;\n  }\n  if (recordRank > 0) {\n    const rankMsg = ['1ST IN THE WORLD!', '2ND IN THE WORLD!', '3RD IN THE WORLD!'][recordRank - 1];\n    hud.showMsg(rankMsg, 'go');\n    audio.fanfare();\n    game.excitement = 1;\n    if (recordRank === 1) {\n      audio.cheer();\n      setSlowmo(0.4, 1.0);\n    }\n  }\n\n  const reveal = () => {\n    hud.hud.resultsKicker.textContent = 'RESULTS — EVENT ' + (game.eventIndex + 1) + ' OF ' + lineup.length;\n    hud.hud.resultsTitle.textContent = e.name;\n    const qualStrip = qualified == null ? '' :\n      '<div class=\"qual-strip' + (qualified ? '' : ' missed') + '\">' +\n      (qualified\n        ? 'QUALIFIED — +' + QUAL_BONUS + ' BONUS PTS'\n        : 'MISSED THE QUALIFYING MARK (' + (records.isTimed(e.id) ? 'UNDER ' : 'OVER ') + records.fmtRecord(e.id, q) + ')') +\n      '</div>';\n    hud.hud.resultsTable.innerHTML = qualStrip + rows.map((r, i) =>\n      '<div class=\"res-row' + (r.player ? ' player' : '') + '\">' +\n      '<span class=\"res-pos\">' + (i + 1) + '</span>' +\n      '<span class=\"res-name\">' + r.name + '</span>' +\n      '<span class=\"res-mark\">' + r.mark + (r.player && recordRank === 1 ? ' &#127942;' : '') + '</span>' +\n      '<span class=\"res-pts\">+' + r.pts + '<span class=\"res-total\">' + game.totals[r.name] + '</span></span>' +\n      '</div>'\n    ).join('');\n    hud.hud.resultsHint.innerHTML = game.eventIndex + 1 < lineup.length\n      ? 'PRESS <b>ENTER</b> FOR NEXT EVENT'\n      : 'PRESS <b>ENTER</b> FOR MEDAL CEREMONY';\n    hud.hud.results.classList.remove('hidden');\n    hud.setAttempt(null);\n    hud.setGauge(0, '', false);\n    cam.mode = 'orbit';\n    game.state = 'results';\n  };\n  // record-board marks earn a slow-mo replay before the results\n  // (relay's runners live in props, not the traced player, so it skips)\n  if (recordRank > 0 && e.id !== 'relay' && trace.length > 10) {\n    setTimeout(() => startReplay(() => setTimeout(reveal, 300)), 1600);\n  } else if (recordRank > 0) {\n    setTimeout(reveal, 2100);\n  } else {\n    reveal();\n  }\n\n  const top = rows[0];\n  stadium.scoreboard(recordRank === 1 ? 'WORLD RECORD!' : e.name, top.name + '  ' + top.mark);\n}\nbus.finishEvent = finishEvent;\n\nfunction startPodium() {\n  hud.hud.results.classList.add('hidden');\n  clearProps();\n  game.state = 'podium';\n  cam.mode = 'podium';\n  stadium.podium.visible = true;\n  game.excitement = 1;\n  audio.cheer();\n\n  const standings = Object.entries(game.totals).sort((a, b) => b[1] - a[1]);\n  const top3 = standings.slice(0, 3);\n  athletes.forEach((a) => { a.group.visible = false; });\n  top3.forEach(([name], i) => {\n    const a = athletes.find((x) => x.name === name);\n    if (!a) return;\n    a.group.visible = true;\n    a.group.position.copy(stadium.podiumSpots[i]);\n    a.group.rotation.y = 0;\n  });\n\n  const medals = ['\\u{1F947}', '\\u{1F948}', '\\u{1F949}'];\n  const pName = ROSTER[0].name;\n  const placement = standings.findIndex(([n]) => n === pName) + 1;\n  const cs = career.recordMeet(pName, placement, game.totals[pName] || 0);\n  hud.hud.resultsKicker.textContent = 'FINAL STANDINGS';\n  hud.hud.resultsTitle.textContent = 'MEDAL CEREMONY';\n  hud.hud.resultsTable.innerHTML = standings.map(([name, pts], i) =>\n    '<div class=\"res-row' + (name === pName ? ' player' : '') + '\">' +\n    '<span class=\"res-pos\">' + (medals[i] || (i + 1)) + '</span>' +\n    '<span class=\"res-name\">' + name + '</span>' +\n    '<span class=\"res-mark\"></span>' +\n    '<span class=\"res-pts\">' + pts + ' PTS</span>' +\n    '</div>'\n  ).join('') +\n  '<div class=\"career-strip\">CAREER · ' + career.fmtLine(pName, cs) + '</div>';\n  hud.hud.resultsHint.innerHTML = 'PRESS <b>ENTER</b> TO PLAY AGAIN';\n  hud.hud.results.classList.remove('hidden');\n  const winner = standings[0][0];\n  stadium.scoreboard('CHAMPION', winner + '  ' + standings[0][1] + ' PTS');\n  hud.showMsg(winner === pName ? 'CHAMPION!' : winner + ' WINS', winner === pName ? 'go' : '');\n}\n\nfunction onEnter() {\n  if (game.state === 'title') {\n    openNameEntry();\n  } else if (game.state === 'card') {\n    startCountdown();\n  } else if (game.state === 'results') {\n    game.eventIndex++;\n    if (game.eventIndex < lineup.length) showCard();\n    else startPodium();\n  } else if (game.state === 'podium') {\n    location.reload();\n  }\n}\n\n// ---------------------------------------------------------------- input\nlet lastSide = null;\nlet spaceDown = false;\n\nfunction mash(side) {\n  if (side === lastSide) return false;\n  lastSide = side;\n  return true;\n}\n\nwindow.addEventListener('keydown', (e) => {\n  if (['ArrowLeft', 'ArrowRight', 'Space', 'ArrowUp', 'ArrowDown'].includes(e.code)) e.preventDefault();\n  audio.ensure();\n  if (e.repeat) return;\n\n  if (game.state === 'name') { nameKey(e.code); return; }\n  if (game.state === 'select') { selectKey(e.code); return; }\n  if (e.code === 'Enter') { onEnter(); return; }\n\n  const side = (e.code === 'ArrowLeft' || e.code === 'KeyA') ? 'L'\n    : (e.code === 'ArrowRight' || e.code === 'KeyD') ? 'R' : null;\n\n  if (game.state === 'countdown') {\n    if ((side || e.code === 'Space') && currentEvent.falseStart && game.cd.step < 3 && game.cd.t > -0.5) falseStart();\n    return;\n  }\n  if (game.state !== 'play' || !currentEvent) return;\n\n  if (side && mash(side) && currentEvent.onMash) currentEvent.onMash();\n  if (e.code === 'Space' && !spaceDown) {\n    spaceDown = true;\n    if (currentEvent.onSpace) currentEvent.onSpace(true);\n  }\n});\n\nwindow.addEventListener('keyup', (e) => {\n  if (e.code === 'Space') {\n    spaceDown = false;\n    if (game.state === 'play' && currentEvent && currentEvent.onSpace) currentEvent.onSpace(false);\n  }\n});\n\n// ---------------------------------------------------------------- touch\nif (IS_TOUCH) {\n  document.body.classList.add('touch');\n  document.getElementById('touch-ui').classList.remove('hidden');\n  const fire = (code, type) => window.dispatchEvent(new KeyboardEvent(type, { code, bubbles: true }));\n  const bindPad = (id, code) => {\n    const el = document.getElementById(id);\n    el.addEventListener('pointerdown', (e) => {\n      e.preventDefault();\n      audio.ensure(); // real user gesture — unlocks audio on iOS\n      fire(code, 'keydown');\n    });\n    el.addEventListener('pointerup', (e) => { e.preventDefault(); fire(code, 'keyup'); });\n    el.addEventListener('pointercancel', () => fire(code, 'keyup'));\n  };\n  bindPad('t-run-l', 'ArrowLeft');\n  bindPad('t-run-r', 'ArrowRight');\n  bindPad('t-action', 'Space');\n  // tapping anywhere else advances menus (title, cards, results, name lock)\n  window.addEventListener('pointerdown', (e) => {\n    if (e.target.closest && e.target.closest('#touch-ui')) return;\n    if (['title', 'card', 'results', 'name', 'podium'].includes(game.state)) {\n      audio.ensure();\n      fire('Enter', 'keydown');\n    }\n  });\n}\n\n// ---------------------------------------------------------------- idle staging\nathletes.forEach((a, i) => {\n  a.group.position.set(-14 + i * 4, 0, 14);\n  a.group.rotation.y = 0;\n});\nstadium.scoreboard('VOXEL GAMES', 'PRESS ENTER');\nbuildNPCs();\n\n// season standings on the title screen (only once careers exist)\nconst seasonRows = career.standings(3);\nif (seasonRows.length) {\n  hud.hud.titleStandings.classList.remove('hidden');\n  hud.hud.titleStandings.innerHTML =\n    '<span class=\"ts-k\">SEASON STANDINGS</span>' +\n    seasonRows.map(([n, s], i) =>\n      '<span class=\"ts-row\">' + (i + 1) + '. ' + n + ' — ' + s.seasonPts.toLocaleString() + ' PTS</span>'\n    ).join('');\n}\n\n// ---------------------------------------------------------------- main loop\nlet last = performance.now();\n\nfunction loop(now) {\n  const rdt = Math.min((now - last) / 1000, 0.05);\n  last = now;\n  game.time += rdt;\n\n  pollGamepads(() => hud.showPopup('\\u{1F3AE} GAMEPAD CONNECTED'));\n\n  if (game.tsTimer > 0) {\n    game.tsTimer -= rdt;\n    if (game.tsTimer <= 0) game.tsTarget = 1;\n  }\n  game.timeScale += (game.tsTarget - game.timeScale) * Math.min(1, rdt * 8);\n  const dt = rdt * game.timeScale;\n\n  game.excitement = Math.max(0.15, game.excitement - rdt * 0.18);\n  audio.setAmbience(game.excitement);\n  audio.ambient(rdt, game.excitement);\n\n  if (game.state === 'countdown') {\n    updateCountdown(rdt);\n    if (currentEvent) currentEvent.update(0);\n  } else if (game.state === 'play' && currentEvent) {\n    currentEvent.update(dt);\n    recordTrace();\n  } else if (game.state === 'replay') {\n    updateReplay(rdt);\n  } else if (game.state === 'title' || game.state === 'card' || game.state === 'results') {\n    cam.orbitAngle += rdt * 0.07;\n    athletes.forEach((a) => { if (a.group.visible) a.update(rdt, 'idle'); });\n  } else if (game.state === 'podium') {\n    athletes.forEach((a) => { if (a.group.visible) a.update(rdt, 'celebrate'); });\n    if (Math.random() < 0.3) {\n      spawnParticles(\n        new THREE.Vector3(-12 + Math.random() * 24, 20, 2 + Math.random() * 18),\n        CONFETTI, 3, { up: -1, vel: 3, g: 2.5, life: 6, size: 0.9 }\n      );\n    }\n    // fireworks over the stands\n    game._fw = (game._fw || 0) - rdt;\n    if (game._fw <= 0) {\n      game._fw = 0.7 + Math.random() * 0.9;\n      spawnParticles(\n        new THREE.Vector3(-50 + Math.random() * 100, 32 + Math.random() * 18, -30 + Math.random() * 55),\n        CONFETTI, 28, { up: 0, vel: 16, g: 4, life: 1.7, size: 0.85, spread: 0.5 }\n      );\n      audio.beep(160 + Math.random() * 120, 0.3, 'sine', 0.12);\n    }\n  }\n\n  const target = camDesired();\n  const k = 1 - Math.exp(-3.4 * rdt);\n  cam.pos.lerp(target.pos, k);\n  cam.look.lerp(target.look, k);\n  camera.position.copy(cam.pos);\n\n  // victory spotlight + stadium dim during low hero shots and the podium\n  const heroShot = target.pos.y < 12 || game.state === 'podium';\n  spotI += ((heroShot ? 1100 : 0) - spotI) * Math.min(1, rdt * 4);\n  spot.intensity = spotI;\n  if (spotI > 1) {\n    const anchor = game.state === 'podium' ? PODIUM_ANCHOR : cam.look;\n    spot.position.set(anchor.x + 5, anchor.y + 26, anchor.z + 7);\n    spot.target.position.copy(anchor);\n  }\n  const dim = Math.min(1, rdt * 3);\n  stadium.hemi.intensity += ((heroShot ? 0.5 : 0.95) - stadium.hemi.intensity) * dim;\n  stadium.sun.intensity += ((heroShot ? 1.0 : 1.7) - stadium.sun.intensity) * dim;\n\n  // FOV punch on the starting gun\n  game.fovKick *= Math.exp(-3.5 * rdt);\n  const fov = 30 + game.fovKick * 5;\n  if (Math.abs(fov - camera.fov) > 0.01) {\n    camera.fov = fov;\n    camera.updateProjectionMatrix();\n  }\n  if (game.shake > 0.01) {\n    game.shake *= Math.exp(-6 * rdt);\n    camera.position.x += (Math.random() - 0.5) * game.shake;\n    camera.position.y += (Math.random() - 0.5) * game.shake * 0.6;\n  }\n  camera.lookAt(cam.look);\n\n  // player marker arrow during track-race starts\n  if (game.state === 'countdown') game.arrowT = 0;\n  else if (game.state === 'play') game.arrowT += rdt;\n  const anchor = currentEvent && currentEvent.playerAnchor &&\n    (game.state === 'countdown' || game.state === 'play')\n    ? currentEvent.playerAnchor() : null;\n  const arrowVis = anchor ? clamp(1 - (game.arrowT - 2.6) / 0.7, 0, 1) : 0;\n  if (arrowVis > 0.01) {\n    // trail BEHIND the runner: with the top-down camera, anything overhead\n    // would cover the player on screen — behind keeps them fully visible\n    const adx = anchor.dx != null ? anchor.dx : 1;\n    const adz = anchor.dz != null ? anchor.dz : 0;\n    marker.position.set(\n      anchor.x - adx * 2.2,\n      anchor.y + 0.8 + Math.sin(game.time * 3.2) * 0.14,\n      anchor.z - adz * 2.2\n    );\n    marker.material.opacity = arrowVis;\n    marker.visible = true;\n  } else {\n    marker.visible = false;\n  }\n\n  stadium.tick(game.time, rdt, game.excitement);\n  updateNPCs(rdt);\n  updateParticles(dt);\n\n  if (bokeh) {\n    const focusTarget = camera.position.distanceTo(cam.look);\n    bokeh.uniforms.focus.value += (focusTarget - bokeh.uniforms.focus.value) * Math.min(1, rdt * 6);\n  }\n  renderFrame();\n  requestAnimationFrame(loop);\n}\nrequestAnimationFrame(loop);\n\n// debug handle\nwindow.__game = { game, EVENTS, athletes, cam, bus, get currentEvent() { return currentEvent; } };\n"},{"name":"npcs.js","content":"// npcs.js — ambient stadium staff with actual jobs, placed in zones no\n// event ever enters so they can never clip the competitors:\n//  - starter official inside the first bend, arm raised through the countdown\n//  - two finish-line judges on the outer apron at the line\n//  - kneeling photographers along the home-straight apron\n//  - athletes warming up on the east D-zone grass (jog laps + stretches)\nimport { Athlete } from './athlete.js';\nimport { scene, game } from './core.js';\n\nconst OFFICIAL = { kit: '#2b3a4e', shorts: '#e8e8e8', skin: '#e8b48c', hair: '#2b3a4e' };\nconst PRESS = { kit: '#3d4a41', shorts: '#22292a', skin: '#c98e5a', hair: '#1a1a1a' };\nconst TRACKSUIT_A = { kit: '#7c8aa0', shorts: '#7c8aa0', skin: '#8a5a33', hair: '#101010' };\nconst TRACKSUIT_B = { kit: '#a0637c', shorts: '#a0637c', skin: '#f0c8a0', hair: '#5b3a1e', hairStyle: 'pony' };\n\nconst npcs = [];\n\nfunction spawn(opts, x, z, ry, role, extra = {}) {\n  const a = new Athlete(opts);\n  a.group.position.set(x, 0, z);\n  a.group.rotation.y = ry;\n  scene.add(a.group);\n  npcs.push(Object.assign({ a, role, x, z }, extra));\n}\n\nexport function buildNPCs() {\n  // starter: inside the west bend, a few metres off the 100m start\n  spawn(OFFICIAL, -55, 25.5, 0.4, 'starter');\n  // finish judges on the outer apron, eyes on the line\n  spawn(OFFICIAL, 43, 41.8, Math.PI, 'judge');\n  spawn(OFFICIAL, 45.8, 41.8, Math.PI + 0.15, 'judge');\n  // photographers kneeling along the home straight apron\n  spawn(PRESS, 12, 41.9, Math.PI, 'photo');\n  spawn(PRESS, 20, 41.9, Math.PI - 0.1, 'photo');\n  spawn(PRESS, 28, 41.9, Math.PI + 0.1, 'photo');\n  // warm-up zone: east D-zone grass, clear of every landing sector\n  spawn(TRACKSUIT_A, 37, 18, Math.PI / 2, 'jogger', { jx0: 36, jx1: 44, jdir: 1, jz: 18 });\n  spawn(TRACKSUIT_B, 43, 20.5, -Math.PI / 2, 'jogger', { jx0: 36, jx1: 44, jdir: -1, jz: 20.5 });\n  spawn(TRACKSUIT_A, 40, 22.5, Math.PI, 'stretcher');\n}\n\nexport function updateNPCs(dt) {\n  for (const n of npcs) {\n    if (n.role === 'starter') {\n      // arm shoots up for the countdown, drops on the gun\n      n.a.update(dt, game.state === 'countdown' ? 'charge' : 'idle', { jav: true });\n    } else if (n.role === 'judge') {\n      n.a.update(dt, 'idle');\n    } else if (n.role === 'photo') {\n      n.a.update(dt, 'crouch'); // hunched over the lens\n    } else if (n.role === 'jogger') {\n      n.x += n.jdir * 3.2 * dt;\n      if (n.x > n.jx1) { n.x = n.jx1; n.jdir = -1; }\n      if (n.x < n.jx0) { n.x = n.jx0; n.jdir = 1; }\n      n.a.group.position.set(n.x, 0, n.jz);\n      n.a.group.rotation.y = n.jdir > 0 ? Math.PI / 2 : -Math.PI / 2;\n      n.a.update(dt, 'run', { speed: 3.2 });\n    } else if (n.role === 'stretcher') {\n      // stretch cycle: deep lunge, stand and shake out, repeat\n      const c = n.a._t % 6;\n      n.a.update(dt, c < 2.6 ? 'crouch' : c < 3.2 ? 'idle' : 'charge');\n    }\n  }\n}\n"},{"name":"props.js","content":"// props.js — event equipment, real-world sizes (1 unit = 1m)\nimport { THREE, boxMesh } from './lib.js';\n\nexport function makeHurdle() {\n  const g = new THREE.Group();\n  const inner = new THREE.Group();\n  const legL = boxMesh(0.08, 0.95, 0.35, '#e8e8e8', { roughness: 0.5 });\n  legL.position.set(-0.5, 0.475, 0);\n  const legR = legL.clone();\n  legR.position.x = 0.5;\n  const bar = boxMesh(1.15, 0.2, 0.08, '#ffffff', { roughness: 0.5 });\n  bar.position.y = 0.98;\n  const stripe = boxMesh(1.16, 0.08, 0.09, '#ff4b3a');\n  stripe.position.y = 1.03;\n  inner.add(legL, legR, bar, stripe);\n  g.add(inner);\n  g.userData.inner = inner;\n  return g;\n}\n\nexport function makeJavelin() {\n  const g = new THREE.Group();\n  const shaft = boxMesh(2.6, 0.06, 0.06, '#d9d9d9', { roughness: 0.35, metalness: 0.4 });\n  const tip = boxMesh(0.35, 0.09, 0.09, '#4a4f58', { roughness: 0.3, metalness: 0.5 });\n  tip.position.x = 1.4;\n  const grip = boxMesh(0.4, 0.085, 0.085, '#ff4b3a');\n  g.add(shaft, tip, grip);\n  return g;\n}\n\nexport function makeHammer() {\n  // ball + short voxel \"wire\" segments; group origin = the athlete's hands\n  const g = new THREE.Group();\n  const ball = boxMesh(0.35, 0.35, 0.35, '#3a3f47', { roughness: 0.4, metalness: 0.5 });\n  ball.position.x = 1.2;\n  const wire = boxMesh(1.2, 0.045, 0.045, '#c9cdd6', { roughness: 0.4, metalness: 0.6 });\n  wire.position.x = 0.6;\n  g.add(ball, wire);\n  g.userData.ball = ball;\n  return g;\n}\n\nexport function makeDiscus() {\n  const g = new THREE.Group();\n  const core = boxMesh(0.55, 0.1, 0.55, '#d9a03c', { roughness: 0.5, metalness: 0.3 });\n  const rim = boxMesh(0.62, 0.06, 0.62, '#8a6420', { roughness: 0.5 });\n  g.add(core, rim);\n  return g;\n}\n\nexport function makeShot() {\n  const g = new THREE.Group();\n  const ball = boxMesh(0.24, 0.24, 0.24, '#3a3f47', { roughness: 0.35, metalness: 0.6 });\n  g.add(ball);\n  return g;\n}\n\nexport function makeHighJumpKit() {\n  // origin = bar line; mat extends +x (the landing side)\n  const g = new THREE.Group();\n  const upL = boxMesh(0.14, 2.9, 0.14, '#e8e8e8', { roughness: 0.5 });\n  upL.position.set(0, 1.45, -2.7);\n  const upR = upL.clone();\n  upR.position.z = 2.7;\n  const baseL = boxMesh(0.6, 0.12, 0.6, '#4a4f58', { roughness: 0.5 });\n  baseL.position.set(0, 0.06, -2.7);\n  const baseR = baseL.clone();\n  baseR.position.z = 2.7;\n  const bar = boxMesh(0.09, 0.09, 5.2, '#ff4b3a', { roughness: 0.4 });\n  bar.position.y = 1.9;\n  const mat = boxMesh(4.6, 0.72, 6.6, '#2563eb', { roughness: 0.9 });\n  mat.position.set(3.0, 0.36, 0);\n  const matTop = boxMesh(4.6, 0.1, 6.6, '#60a5fa', { roughness: 0.9 });\n  matTop.position.set(3.0, 0.77, 0);\n  g.add(upL, upR, baseL, baseR, bar, mat, matTop);\n  g.userData.bar = bar;\n  return g;\n}\n\nexport function makeVaultPole() {\n  // origin = butt end, shaft along +x, so rotation.z pivots around the plant\n  const g = new THREE.Group();\n  const shaft = boxMesh(4.6, 0.07, 0.07, '#ffb020', { roughness: 0.45 });\n  shaft.position.x = 2.3;\n  g.add(shaft);\n  return g;\n}\n\nexport function makeVaultKit() {\n  // origin = bar line; plant box before it, tall mat behind\n  const g = new THREE.Group();\n  const upL = boxMesh(0.16, 6.6, 0.16, '#e8e8e8', { roughness: 0.5 });\n  upL.position.set(0, 3.3, -2.7);\n  const upR = upL.clone();\n  upR.position.z = 2.7;\n  const baseL = boxMesh(0.7, 0.14, 0.7, '#4a4f58', { roughness: 0.5 });\n  baseL.position.set(0, 0.07, -2.7);\n  const baseR = baseL.clone();\n  baseR.position.z = 2.7;\n  const bar = boxMesh(0.09, 0.09, 5.2, '#ff4b3a', { roughness: 0.4 });\n  bar.position.y = 4.4;\n  const box = boxMesh(0.9, 0.12, 0.8, '#2b2f38', { roughness: 0.8 });\n  box.position.set(-1.9, 0.06, 0);\n  const mat = boxMesh(5.4, 0.9, 7.4, '#2563eb', { roughness: 0.9 });\n  mat.position.set(3.3, 0.45, 0);\n  const matTop = boxMesh(5.4, 0.12, 7.4, '#60a5fa', { roughness: 0.9 });\n  matTop.position.set(3.3, 0.96, 0);\n  g.add(upL, upR, baseL, baseR, bar, box, mat, matTop);\n  g.userData.bar = bar;\n  return g;\n}\n\nexport function makeDistanceFlag(color = '#ffb020') {\n  const g = new THREE.Group();\n  const pole = boxMesh(0.12, 2.2, 0.12, '#f5f1e6');\n  pole.position.y = 1.1;\n  const fl = boxMesh(0.9, 0.6, 0.08, color);\n  fl.position.set(0.45, 1.9, 0);\n  g.add(pole, fl);\n  return g;\n}\n"},{"name":"records.js","content":"// records.js — persistent top-3 records per event, Track & Field style.\n// Seeds sit right at the edge of what's humanly achievable, so breaking one\n// is a genuine feat. Stored in localStorage; falls back to memory.\nconst KEY = 'svo-records-v1';\n\n// lower-is-better events (times); everything else is distance\nconst TIMED = new Set(['sprint100', 'run200', 'hurdles110', 'run400', 'run800', 'relay']);\n\nconst SEEDS = {\n  sprint100:  [{ n: 'KON', v: 9.85 },  { n: 'AMI', v: 10.10 }, { n: 'T&F', v: 10.35 }],\n  run200:     [{ n: 'KON', v: 19.25 }, { n: 'AMI', v: 19.90 }, { n: 'T&F', v: 20.60 }],\n  hurdles110: [{ n: 'KON', v: 12.20 }, { n: 'AMI', v: 12.80 }, { n: 'T&F', v: 13.40 }],\n  run400:     [{ n: 'KON', v: 47.50 }, { n: 'AMI', v: 49.00 }, { n: 'T&F', v: 50.50 }],\n  run800:     [{ n: 'KON', v: 99.50 }, { n: 'AMI', v: 103.0 }, { n: 'T&F', v: 106.5 }],\n  relay:      [{ n: 'KON', v: 37.50 }, { n: 'AMI', v: 39.00 }, { n: 'T&F', v: 40.50 }],\n  longJump:   [{ n: 'KON', v: 8.55 },  { n: 'AMI', v: 8.25 },  { n: 'T&F', v: 7.95 }],\n  tripleJump: [{ n: 'KON', v: 17.40 }, { n: 'AMI', v: 16.60 }, { n: 'T&F', v: 15.90 }],\n  javelin:    [{ n: 'KON', v: 56.50 }, { n: 'AMI', v: 53.50 }, { n: 'T&F', v: 50.50 }],\n  hammer:     [{ n: 'KON', v: 69.50 }, { n: 'AMI', v: 66.00 }, { n: 'T&F', v: 62.00 }],\n  discus:     [{ n: 'KON', v: 55.50 }, { n: 'AMI', v: 52.00 }, { n: 'T&F', v: 48.00 }],\n  shotPut:    [{ n: 'KON', v: 20.80 }, { n: 'AMI', v: 19.40 }, { n: 'T&F', v: 18.00 }],\n  highJump:   [{ n: 'KON', v: 2.42 },  { n: 'AMI', v: 2.34 },  { n: 'T&F', v: 2.26 }],\n  poleVault:  [{ n: 'KON', v: 6.00 },  { n: 'AMI', v: 5.60 },  { n: 'T&F', v: 5.20 }],\n};\n\nlet table = null;\n\nfunction load() {\n  if (table) return table;\n  table = JSON.parse(JSON.stringify(SEEDS));\n  try {\n    const raw = localStorage.getItem(KEY);\n    if (raw) {\n      const saved = JSON.parse(raw);\n      for (const id in table) if (saved[id]) table[id] = saved[id];\n    }\n  } catch (e) { /* storage unavailable — session-only records */ }\n  return table;\n}\n\nfunction save() {\n  try { localStorage.setItem(KEY, JSON.stringify(table)); } catch (e) { /* ok */ }\n}\n\nexport function isTimed(id) { return TIMED.has(id); }\n\nexport function fmtRecord(id, v) {\n  return v.toFixed(2) + (isTimed(id) ? 's' : 'm');\n}\n\nexport function getTop3(id) {\n  return (load()[id] || []).slice(0, 3);\n}\n\n// returns the rank achieved (1..3) or 0 if the mark doesn't make the board\nexport function submit(id, initials, value) {\n  const t = load();\n  if (!t[id]) return 0;\n  const better = (a, b) => (isTimed(id) ? a < b : a > b);\n  const list = t[id].slice();\n  let rank = 0;\n  for (let i = 0; i < 3; i++) {\n    if (better(value, list[i].v)) { rank = i + 1; break; }\n  }\n  if (rank === 0) return 0;\n  list.splice(rank - 1, 0, { n: initials, v: value });\n  t[id] = list.slice(0, 3);\n  save();\n  return rank;\n}\n"},{"name":"world.js","content":"// world.js — the voxel stadium. 1 unit = 1 metre.\n// 400m oval: two 90m straights + semicircular bends, 8 lanes of 1.25m,\n// west chute for the 100m start, common finish line at x=43.\nimport { THREE, BOX, boxMesh, buildTiles } from './lib.js';\n\nexport const LANE_W = 1.25;\nconst R_IN = 30;          // track inner edge\nconst TRACK_W = 10;       // 8 lanes\nconst STRAIGHT = 45;      // half-length of each straight\nexport const TRACK = { rIn: R_IN, laneW: LANE_W, lanes: 8, straight: STRAIGHT, start: -57, finish: 43 };\nexport const CENTER = new THREE.Vector3(0, 0, 0);\nexport const FINISH_S = 88; // distance of the finish line from the path origin (x=-45 on home straight)\n\n// home straight is the SOUTH side (positive z, nearest the camera)\nexport function laneZ(i) { return R_IN + LANE_W * (i + 0.5); }\n\n// ring coordinate: distance from the oval spine\nfunction ovalDist(x, z) {\n  const ax = Math.abs(x);\n  if (ax <= STRAIGHT) return Math.abs(z);\n  return Math.hypot(ax - STRAIGHT, z);\n}\n\n// lap length for a lane offset o (metres from centre spine)\nexport function lapLength(o) { return 4 * STRAIGHT + 2 * Math.PI * o; }\n\n// point on the oval for lane offset o at arc distance s (counter-clockwise,\n// origin at x=-45 on the home straight). Returns {x, z, dx, dz}.\nexport function lanePath(o, s) {\n  const L = lapLength(o);\n  s = ((s % L) + L) % L;\n  if (s < 2 * STRAIGHT) return { x: -STRAIGHT + s, z: o, dx: 1, dz: 0 };\n  s -= 2 * STRAIGHT;\n  const arc = Math.PI * o;\n  if (s < arc) {\n    const p = s / o;\n    return { x: STRAIGHT + o * Math.sin(p), z: o * Math.cos(p), dx: Math.cos(p), dz: -Math.sin(p) };\n  }\n  s -= arc;\n  if (s < 2 * STRAIGHT) return { x: STRAIGHT - s, z: -o, dx: -1, dz: 0 };\n  s -= 2 * STRAIGHT;\n  const p = s / o;\n  return { x: -STRAIGHT - o * Math.sin(p), z: -o * Math.cos(p), dx: -Math.cos(p), dz: Math.sin(p) };\n}\n\n// heading (group.rotation.y) for a path direction — athlete model faces +X at PI/2\nexport function headingFor(dx, dz) { return Math.PI / 2 - Math.atan2(dz, dx); }\n\n// walk the oval at a given offset, step metres apart; fn(x, z, ry)\nfunction ovalPath(off, step, fn) {\n  for (let x = -STRAIGHT; x < STRAIGHT; x += step) {\n    fn(x + step / 2, off, 0);\n    fn(x + step / 2, -off, 0);\n  }\n  const n = Math.ceil(Math.PI * off / step);\n  for (let i = 0; i < n; i++) {\n    const th = -Math.PI / 2 + Math.PI * (i + 0.5) / n;\n    fn(STRAIGHT + off * Math.cos(th), off * Math.sin(th), Math.atan2(-Math.cos(th), -Math.sin(th)));\n    fn(-STRAIGHT - off * Math.cos(th), off * Math.sin(th), Math.atan2(-Math.cos(th), Math.sin(th)));\n  }\n}\n\n// ---------------------------------------------------------------- stadium\nexport function buildStadium(scene) {\n  const skyCol = new THREE.Color('#a5ddff');\n  scene.background = skyCol;\n  scene.fog = new THREE.Fog(skyCol, 340, 620);\n\n  const hemi = new THREE.HemisphereLight(0xcfe8ff, 0x5e7a4a, 0.95);\n  scene.add(hemi);\n  // cool rim fill from the south-west for shape definition on the voxels\n  const rim = new THREE.DirectionalLight(0x9fc4ff, 0.55);\n  rim.position.set(-70, 55, 85);\n  scene.add(rim);\n  const sun = new THREE.DirectionalLight(0xfff2dd, 1.7);\n  sun.position.set(90, 140, -60);\n  sun.target.position.set(0, 0, 0);\n  sun.castShadow = true;\n  sun.shadow.mapSize.set(2048, 2048);\n  // tighter bounds = crisper shadows from the same map\n  sun.shadow.camera.left = -112;\n  sun.shadow.camera.right = 112;\n  sun.shadow.camera.top = 112;\n  sun.shadow.camera.bottom = -112;\n  sun.shadow.camera.near = 20;\n  sun.shadow.camera.far = 420;\n  sun.shadow.normalBias = 0.4;\n  scene.add(sun, sun.target);\n\n  const ground = boxMesh(430, 1, 380, '#4a8438');\n  ground.position.y = -0.75;\n  ground.castShadow = false;\n  scene.add(ground);\n\n  // -------- oval track: voxel tiles with lane lines baked into the colours\n  const track = [];\n  const tCols = ['#d9553f', '#d05038', '#c74a33'];\n  const TS = 0.62;\n  // the bends reach |x| = STRAIGHT + R_IN + TRACK_W = 85, so sweep the full extent\n  for (let x = -86; x <= 86; x += TS) {\n    for (let z = -(R_IN + TRACK_W + 1); z <= R_IN + TRACK_W + 1; z += TS) {\n      const d = ovalDist(x, z);\n      const inOval = d >= R_IN - 0.05 && d <= R_IN + TRACK_W + 0.05;\n      const inChute = !inOval && z >= R_IN - 0.05 && z <= R_IN + TRACK_W + 0.05 && x >= -58.5 && x < -STRAIGHT;\n      if (!inOval && !inChute) continue;\n      track.push({ x, y: -0.16, z, sx: TS, sy: 0.5, sz: TS, color: tCols[Math.floor(Math.random() * 3)] });\n    }\n  }\n  scene.add(buildTiles(track));\n\n  // crisp lane lines that follow the curve: walk each lane boundary analytically\n  const lanes = [];\n  for (let j = 0; j <= 8; j++) {\n    const off = R_IN + j * LANE_W;\n    ovalPath(off, 0.5, (x, z, ry) => {\n      lanes.push({ x, y: 0.1, z, sx: 0.56, sy: 0.05, sz: 0.13, ry, color: '#f3efe2' });\n    });\n    // west chute extension (straight segment)\n    lanes.push({ x: -51.75, y: 0.1, z: off, sx: 13.5, sy: 0.05, sz: 0.13, color: '#f3efe2' });\n  }\n  scene.add(buildTiles(lanes));\n\n  // classic white/red kerb hugging the inside of lane 1\n  const kerb = [];\n  let ki = 0;\n  ovalPath(R_IN - 0.25, 0.6, (x, z, ry) => {\n    kerb.push({ x, y: 0.14, z, sx: 0.55, sy: 0.22, sz: 0.3, ry, color: ki++ % 2 ? '#e23b34' : '#f5f1e6' });\n  });\n  scene.add(buildTiles(kerb));\n\n  // start line (100m, west chute) + checkered common finish line + start blocks\n  const marks = [];\n  for (let i = 0; i < 8; i++) {\n    marks.push({ x: TRACK.start - 1.0, y: 0.2, z: laneZ(i), sx: 0.85, sy: 0.25, sz: 0.5, color: '#2a2f38' });\n  }\n  marks.push({ x: TRACK.start, y: 0.12, z: R_IN + TRACK_W / 2, sx: 0.35, sy: 0.06, sz: TRACK_W, color: '#ffffff' });\n  for (let z = R_IN; z < R_IN + TRACK_W - 0.01; z += 0.45) {\n    const k = Math.round(z / 0.45) % 2 === 0;\n    marks.push({ x: TRACK.finish, y: 0.12, z: z + 0.225, sx: 0.45, sy: 0.06, sz: 0.45, color: k ? '#14171c' : '#f3efe2' });\n    marks.push({ x: TRACK.finish + 0.45, y: 0.12, z: z + 0.225, sx: 0.45, sy: 0.06, sz: 0.45, color: k ? '#f3efe2' : '#14171c' });\n  }\n  scene.add(buildTiles(marks));\n\n  // -------- infield grass (mown checker) + grey apron ring outside the track\n  const grass = [];\n  for (let x = -90; x <= 90; x += 2) {\n    for (let z = -46; z <= 46; z += 2) {\n      const d = ovalDist(x, z);\n      if (d < R_IN - 0.2) {\n        const checker = (Math.floor(x / 5) + Math.floor(z / 5)) % 2 === 0;\n        let col = checker ? '#5fae3f' : '#54a137';\n        if (Math.random() < 0.03) col = '#4b9231';\n        grass.push({ x, y: -0.5, z, sx: 2, sy: 1, sz: 2, color: col });\n      }\n    }\n  }\n  // smooth analytic rings so the curved boundaries never look jagged:\n  // grass fringe inside the kerb, then a grey apron band outside the track\n  ovalPath(R_IN - 1.1, 1.0, (x, z, ry) => {\n    grass.push({ x, y: -0.48, z, sx: 1.05, sy: 1, sz: 2.3, ry, color: '#58a83b' });\n  });\n  ovalPath(R_IN + TRACK_W + 1.75, 1.1, (x, z, ry) => {\n    grass.push({ x, y: -0.45, z, sx: 1.15, sy: 1, sz: 3.5, ry, color: '#8b94a3' });\n  });\n  ovalPath(R_IN + TRACK_W + 3.85, 1.4, (x, z, ry) => {\n    grass.push({ x, y: -0.47, z, sx: 1.45, sy: 1, sz: 0.9, ry, color: '#7d8694' });\n  });\n  scene.add(buildTiles(grass));\n\n  // -------- long/triple jump: shared runway (z=24), boards, sand pit x 14..23\n  const lj = [];\n  for (let x = -25.5; x < 13.5; x += 0.65) {\n    for (let z = 23.2; z < 24.9; z += 0.65) {\n      lj.push({ x, y: -0.1, z, sx: 0.65, sy: 0.44, sz: 0.65, color: Math.random() < 0.5 ? '#8f9aa8' : '#98a3b1' });\n    }\n  }\n  // long jump board (x=13) and triple jump board (x=1)\n  lj.push({ x: 13, y: 0.14, z: 24, sx: 0.5, sy: 0.06, sz: 1.8, color: '#ffffff' });\n  lj.push({ x: 13.5, y: 0.14, z: 24, sx: 0.35, sy: 0.07, sz: 1.8, color: '#e23b34' });\n  lj.push({ x: 1, y: 0.14, z: 24, sx: 0.5, sy: 0.06, sz: 1.8, color: '#ffffff' });\n  lj.push({ x: 1.5, y: 0.14, z: 24, sx: 0.35, sy: 0.07, sz: 1.8, color: '#39d4c8' });\n  for (let x = 14; x < 23; x += 0.8) {\n    for (let z = 22.2; z < 26.2; z += 0.8) {\n      lj.push({ x, y: -0.06, z, sx: 0.8, sy: 0.5, sz: 0.8, color: Math.random() < 0.5 ? '#e6c98c' : '#dfc083' });\n    }\n  }\n  lj.push({ x: 18.5, y: 0.22, z: 21.9, sx: 9.4, sy: 0.36, sz: 0.18, color: '#f5f1e6' });\n  lj.push({ x: 18.5, y: 0.22, z: 26.3, sx: 9.4, sy: 0.36, sz: 0.18, color: '#f5f1e6' });\n  lj.push({ x: 23.2, y: 0.22, z: 24.1, sx: 0.18, sy: 0.36, sz: 4.6, color: '#f5f1e6' });\n  // runway edge lines\n  lj.push({ x: -6, y: 0.11, z: 23.15, sx: 39, sy: 0.05, sz: 0.12, color: '#f3efe2' });\n  lj.push({ x: -6, y: 0.11, z: 24.95, sx: 39, sy: 0.05, sz: 0.12, color: '#f3efe2' });\n  scene.add(buildTiles(lj));\n\n  // -------- javelin: runway on the infield centreline + sector + arcs\n  const jv = [];\n  for (let x = -43.5; x < -19.6; x += 0.65) {\n    for (let z = -1.0; z < 1.0; z += 0.65) {\n      jv.push({ x, y: -0.1, z, sx: 0.65, sy: 0.44, sz: 0.65, color: Math.random() < 0.5 ? '#8f9aa8' : '#98a3b1' });\n    }\n  }\n  jv.push({ x: -20, y: 0.14, z: 0, sx: 0.4, sy: 0.06, sz: 2.4, color: '#ffffff' });\n  // runway edge lines\n  jv.push({ x: -31.5, y: 0.11, z: -1.05, sx: 23.5, sy: 0.05, sz: 0.12, color: '#f3efe2' });\n  jv.push({ x: -31.5, y: 0.11, z: 1.05, sx: 23.5, sy: 0.05, sz: 0.12, color: '#f3efe2' });\n  for (let r = 3; r < 56; r += 0.75) {\n    for (const s of [-1, 1]) {\n      jv.push({ x: -20 + r * 0.967, y: 0.08, z: s * r * 0.255, sx: 0.28, sy: 0.26, sz: 0.28, color: '#f0ead8' });\n    }\n  }\n  for (const D of [30, 40, 50]) {\n    for (let a = -0.245; a <= 0.245; a += 0.022) {\n      jv.push({ x: -20 + D * Math.cos(a), y: 0.08, z: D * Math.sin(a), sx: 0.28, sy: 0.26, sz: 0.28, color: '#f0ead8' });\n    }\n  }\n  scene.add(buildTiles(jv));\n\n  // -------- throwing circles (hammer SW, discus NW of infield) + cages + sectors\n  const circles = [];\n  function throwingCircle(cx, cz, sectorA0, rayLen) {\n    for (let a = 0; a < Math.PI * 2; a += 0.14) {\n      circles.push({ x: cx + Math.cos(a) * 1.25, y: 0.1, z: cz + Math.sin(a) * 1.25, sx: 0.26, sy: 0.28, sz: 0.26, color: '#f5f1e6' });\n    }\n    for (let x = -0.9; x <= 0.9; x += 0.45) {\n      for (let z = -0.9; z <= 0.9; z += 0.45) {\n        if (Math.hypot(x, z) < 1.1) circles.push({ x: cx + x, y: -0.08, z: cz + z, sx: 0.45, sy: 0.4, sz: 0.45, color: '#9aa3b0' });\n      }\n    }\n    // sector rays (kept short + angled so they stay clear of the javelin fan)\n    for (let r = 2.2; r < rayLen; r += 0.75) {\n      for (const s of [-0.35, 0.35]) {\n        const a = sectorA0 + s;\n        circles.push({ x: cx + r * Math.cos(a), y: 0.06, z: cz + r * Math.sin(a), sx: 0.26, sy: 0.24, sz: 0.26, color: '#f0ead8' });\n      }\n    }\n    // C-shaped cage behind the circle\n    for (let a = sectorA0 + Math.PI * 0.62; a <= sectorA0 + Math.PI * 1.38; a += 0.22) {\n      const px = cx + Math.cos(a) * 3.4, pz = cz + Math.sin(a) * 3.4;\n      circles.push({ x: px, y: 2.1, z: pz, sx: 0.7, sy: 4.2, sz: 0.18, ry: -a, color: '#39404c' });\n    }\n  }\n  throwingCircle(-36, -14, -0.15, 34);   // hammer: aims east, hugging the south infield\n  throwingCircle(-36, 14, 0.1, 19);      // discus: short fan, north of the javelin\n  scene.add(buildTiles(circles, { shadowCast: true }));\n\n  // -------- ad boards ringing the track\n  const ads = [];\n  const adCols = ['#1f9e8e', '#f5f1e6', '#ff4b3a', '#2b6cb0', '#f5f1e6', '#ffb020'];\n  let adI = 0;\n  ovalPath(R_IN + TRACK_W + 1.6, 5.2, (x, z, ry) => {\n    ads.push({ x, y: 0.55, z, sx: 4.6, sy: 1.1, sz: 0.35, ry, color: adCols[adI++ % adCols.length] });\n  });\n  scene.add(buildTiles(ads, { shadowCast: true }));\n\n  // -------- oval grandstands + crowd\n  const tiers = [];\n  const crowdList = [];\n  const crowdCols = ['#ff5a5f', '#ffb020', '#4aa3ff', '#7ce34a', '#f5f1e6', '#c084fc', '#ff8a3c', '#39d4c8', '#e6e6e6', '#274690'];\n  const TIERS = 10, STEP_UP = 1.15, STEP_OUT = 1.9, BASE_OFF = 44;\n\n  for (let t = 0; t < TIERS; t++) {\n    const off = BASE_OFF + t * STEP_OUT;\n    const y = 0.9 + t * STEP_UP;\n    ovalPath(off, 3.2, (x, z, ry) => {\n      tiers.push({ x, y, z, sx: 3.0, sy: 1.15, sz: 1.9, ry, color: t % 2 === 0 ? '#b9bfc9' : '#aab0bb' });\n      const dirX = Math.cos(ry), dirZ = -Math.sin(ry);\n      // fan sections: blocks of supporters share colours around the bowl\n      const secCols = ['#ff4b3a', '#f5f1e6', '#274690', '#ffb020', '#1f9e8e', '#e6e6e6'];\n      const sec = Math.floor(((Math.atan2(z, x) + Math.PI) / (Math.PI * 2)) * 14);\n      for (const s of [-1.05, 0, 1.05]) {\n        if (Math.random() < 0.92) {\n          crowdList.push({\n            x: x + dirX * s + (Math.random() - 0.5) * 0.25,\n            y: y + 0.95,\n            z: z + dirZ * s + (Math.random() - 0.5) * 0.25,\n            color: Math.random() < 0.68\n              ? secCols[sec % secCols.length]\n              : crowdCols[Math.floor(Math.random() * crowdCols.length)],\n          });\n        }\n      }\n    });\n  }\n  ovalPath(BASE_OFF + (TIERS - 1) * STEP_OUT + 1.6, 3.2, (x, z, ry) => {\n    tiers.push({ x, y: 0.9 + (TIERS - 1) * STEP_UP + 1.3, z, sx: 3.2, sy: 1.6, sz: 0.6, ry, color: '#9aa1ad' });\n  });\n  scene.add(buildTiles(tiers, { shadowCast: true }));\n\n  // floating roof canopy: high and pushed outward so the follow camera\n  // always sees over its inner lip\n  const roofTiles = [];\n  const roofY = 0.9 + (TIERS - 1) * STEP_UP + 8.2;\n  ovalPath(BASE_OFF + (TIERS - 1) * STEP_OUT / 2 + 2.5, 3.3, (x, z, ry) => {\n    roofTiles.push({ x, y: roofY, z, sx: 3.3, sy: 0.35, sz: TIERS * STEP_OUT + 1, ry, color: '#e8ebf0' });\n  });\n  ovalPath(BASE_OFF + (TIERS - 1) * STEP_OUT + 2.6, 14, (x, z, ry) => {\n    roofTiles.push({ x, y: roofY / 2, z, sx: 0.8, sy: roofY, sz: 0.8, ry, color: '#9aa1ad' });\n  });\n  scene.add(buildTiles(roofTiles, { shadowCast: true }));\n\n  // outer concourse walkway + trees around the bowl\n  const outer = [];\n  ovalPath(66.5, 2.2, (x, z, ry) => {\n    outer.push({ x, y: -0.3, z, sx: 2.2, sy: 0.5, sz: 4.5, ry, color: '#a7adb8' });\n  });\n  scene.add(buildTiles(outer));\n  ovalPath(74, 15, (x, z) => {\n    if (Math.abs(x) > 76 && Math.abs(z) > 42) return;  // floodlight corners\n    if (x > 104 && Math.abs(z) < 14) return;           // cauldron\n    const g = new THREE.Group();\n    const trunk = boxMesh(0.55, 2.6, 0.55, '#6e4a2a');\n    trunk.position.y = 1.3;\n    const c1 = boxMesh(3.0, 2.2, 3.0, '#4c8f3a');\n    c1.position.y = 3.4;\n    const c2 = boxMesh(2.0, 1.5, 2.0, '#579f43');\n    c2.position.y = 4.9;\n    g.add(trunk, c1, c2);\n    g.position.set(x + (Math.random() - 0.5) * 5, 0, z + (Math.random() - 0.5) * 5);\n    scene.add(g);\n  });\n\n  // drifting voxel clouds\n  const clouds = [];\n  for (let i = 0; i < 8; i++) {\n    const g = new THREE.Group();\n    const n = 3 + Math.floor(Math.random() * 3);\n    for (let k = 0; k < n; k++) {\n      const puff = boxMesh(\n        7 + Math.random() * 9, 2.2 + Math.random() * 1.6, 4 + Math.random() * 4,\n        '#ffffff', {\n          roughness: 1,\n          emissive: new THREE.Color('#dfe9f5'), emissiveIntensity: 0.35,\n          transparent: true, opacity: 0.45, depthWrite: false, // see-through\n        }\n      );\n      puff.castShadow = false;\n      puff.receiveShadow = false;\n      puff.position.set((Math.random() - 0.5) * 14, (Math.random() - 0.5) * 2.4, (Math.random() - 0.5) * 8);\n      g.add(puff);\n    }\n    g.position.set(-220 + Math.random() * 440, 96 + Math.random() * 34, -160 + Math.random() * 320);\n    scene.add(g);\n    clouds.push(g);\n  }\n\n  // little spectator sprites: body + head + hair (three instanced layers)\n  const skinCols = ['#c98e5a', '#8a5a33', '#e8b48c', '#f0c8a0', '#6b4226'];\n  const hairCols = ['#20140e', '#3b2b1a', '#5b3a1e', '#101010', '#c9c9c9', '#8a5a33'];\n  const N = crowdList.length;\n  function crowdLayer() {\n    const m = new THREE.InstancedMesh(BOX, new THREE.MeshStandardMaterial({ roughness: 0.9 }), N);\n    m.castShadow = false;\n    m.receiveShadow = false;\n    // matrices are written each tick, so the auto bounding sphere is wrong —\n    // never cull, or the whole crowd vanishes when the camera swoops low\n    m.frustumCulled = false;\n    scene.add(m);\n    return m;\n  }\n  const crowdBody = crowdLayer();\n  const crowdHead = crowdLayer();\n  const crowdHair = crowdLayer();\n  const crowdArms = crowdLayer();\n  const crowdBase = new Float32Array(N * 3);\n  const crowdPhase = new Float32Array(N);\n  const crowdRot = new Float32Array(N);\n  {\n    const c = new THREE.Color();\n    crowdList.forEach((p, i) => {\n      crowdBase[i * 3] = p.x; crowdBase[i * 3 + 1] = p.y; crowdBase[i * 3 + 2] = p.z;\n      crowdPhase[i] = Math.random() * Math.PI * 2;\n      crowdRot[i] = (Math.random() - 0.5) * 0.8;\n      crowdBody.setColorAt(i, c.set(p.color));\n      const skin = skinCols[Math.floor(Math.random() * skinCols.length)];\n      crowdHead.setColorAt(i, c.set(skin));\n      crowdHair.setColorAt(i, c.set(hairCols[Math.floor(Math.random() * hairCols.length)]));\n      crowdArms.setColorAt(i, c.set(skin));\n    });\n    for (const m of [crowdBody, crowdHead, crowdHair, crowdArms]) {\n      if (m.instanceColor) m.instanceColor.needsUpdate = true;\n    }\n  }\n\n  // -------- flags on the rim\n  const flags = [];\n  const flagCols = ['#ff4b3a', '#ffb020', '#4aa3ff', '#7ce34a', '#f5f1e6', '#c084fc'];\n  const rimY = 0.9 + (TIERS - 1) * STEP_UP + 2.1;\n  let fi = 0;\n  ovalPath(BASE_OFF + (TIERS - 1) * STEP_OUT + 1.6, 13, (x, z, ry) => {\n    const g = new THREE.Group();\n    const pole = boxMesh(0.16, 3.4, 0.16, '#e8e8e8', { roughness: 0.5 });\n    pole.position.y = 1.7;\n    const flag = boxMesh(1.5, 0.9, 0.12, flagCols[fi % flagCols.length]);\n    flag.position.set(0.8, 2.9, 0);\n    g.add(pole, flag);\n    g.position.set(x, rimY, z);\n    g.userData.flag = flag;\n    scene.add(g);\n    flags.push(g);\n    fi++;\n  });\n\n  // -------- floodlight towers\n  for (const [tx, tz] of [[-84, -52], [-84, 52], [84, -52], [84, 52]]) {\n    const g = new THREE.Group();\n    const pole = boxMesh(1.4, 30, 1.4, '#8b93a0', { roughness: 0.6 });\n    pole.position.y = 15;\n    g.add(pole);\n    const head = new THREE.Group();\n    for (let r = 0; r < 3; r++) for (let c = 0; c < 4; c++) {\n      const lamp = boxMesh(1.4, 1.4, 0.5, '#ffffff', { emissive: new THREE.Color('#fff7d6'), emissiveIntensity: 1.6, roughness: 0.3 });\n      lamp.position.set((c - 1.5) * 1.6, r * 1.6, 0);\n      head.add(lamp);\n    }\n    const back = boxMesh(7, 5.2, 0.5, '#39404c');\n    back.position.set(0, 1.6, -0.5);\n    head.add(back);\n    head.position.y = 30.5;\n    head.rotation.y = Math.atan2(-tx, -tz);\n    head.rotation.x = 0.35;\n    g.add(head);\n    g.position.set(tx, 0, tz);\n    scene.add(g);\n  }\n\n  // -------- scoreboard above the north stand\n  const sbCanvas = document.createElement('canvas');\n  sbCanvas.width = 512; sbCanvas.height = 288;\n  const sbCtx = sbCanvas.getContext('2d');\n  const sbTex = new THREE.CanvasTexture(sbCanvas);\n  sbTex.colorSpace = THREE.SRGBColorSpace;\n  function drawScoreboard(line1, line2) {\n    sbCtx.fillStyle = '#0c1220';\n    sbCtx.fillRect(0, 0, 512, 288);\n    sbCtx.strokeStyle = '#1f2c48';\n    sbCtx.lineWidth = 8;\n    sbCtx.strokeRect(8, 8, 496, 272);\n    sbCtx.fillStyle = '#ffb020';\n    sbCtx.font = '900 44px \"Archivo Black\", sans-serif';\n    sbCtx.textAlign = 'center';\n    sbCtx.fillText(line1, 256, 120);\n    sbCtx.fillStyle = '#e8eefc';\n    sbCtx.font = '700 30px \"Space Grotesk\", sans-serif';\n    sbCtx.fillText(line2, 256, 190);\n    sbTex.needsUpdate = true;\n  }\n  drawScoreboard('VOXEL GAMES', 'WELCOME');\n  const sbGroup = new THREE.Group();\n  const sbFrame = boxMesh(20, 11, 1.4, '#232a36', { roughness: 0.5 });\n  const sbScreen = new THREE.Mesh(new THREE.PlaneGeometry(18.5, 9.8), new THREE.MeshBasicMaterial({ map: sbTex }));\n  sbScreen.position.z = 0.75;\n  sbGroup.add(sbFrame, sbScreen);\n  const sLegL = boxMesh(0.9, 9, 0.9, '#5a6270'); sLegL.position.set(-7, -9.5, 0);\n  const sLegR = boxMesh(0.9, 9, 0.9, '#5a6270'); sLegR.position.set(7, -9.5, 0);\n  sbGroup.add(sLegL, sLegR);\n  sbGroup.position.set(0, rimY + 9, -(BASE_OFF + (TIERS - 1) * STEP_OUT + 4));\n  scene.add(sbGroup);\n\n  // -------- olympic cauldron behind the east stand\n  const cauldron = new THREE.Group();\n  const cPole = boxMesh(2.4, 20, 2.4, '#c9cdd6', { roughness: 0.4, metalness: 0.3 });\n  cPole.position.y = 10;\n  const bowl = boxMesh(5.4, 1.8, 5.4, '#8d93a0', { roughness: 0.4, metalness: 0.4 });\n  bowl.position.y = 21;\n  cauldron.add(cPole, bowl);\n  const flameMat = new THREE.MeshStandardMaterial({ roughness: 0.6, emissive: new THREE.Color('#ff7b1f'), emissiveIntensity: 1.4 });\n  const flame = new THREE.InstancedMesh(BOX, flameMat, 14);\n  const flameCols = ['#ffdd55', '#ffab3d', '#ff7b1f', '#ff5722'];\n  {\n    const c = new THREE.Color();\n    for (let i = 0; i < 14; i++) flame.setColorAt(i, c.set(flameCols[i % flameCols.length]));\n    if (flame.instanceColor) flame.instanceColor.needsUpdate = true;\n  }\n  flame.position.y = 22.5;\n  flame.frustumCulled = false; // matrices set per-tick, same culling trap\n  cauldron.add(flame);\n  cauldron.position.set(112, 0, 0);\n  scene.add(cauldron);\n\n  // -------- podium (hidden until the ceremony)\n  const podium = new THREE.Group();\n  const p1 = boxMesh(2.2, 1.1, 2.2, '#f5d24a', { roughness: 0.5 }); p1.position.set(0, 0.55, 0);\n  const p2 = boxMesh(2.2, 0.75, 2.2, '#cfd8e6', { roughness: 0.5 }); p2.position.set(-2.3, 0.375, 0);\n  const p3 = boxMesh(2.2, 0.5, 2.2, '#e0925a', { roughness: 0.5 }); p3.position.set(2.3, 0.25, 0);\n  podium.add(p1, p2, p3);\n  podium.position.set(0, 0, 12);\n  podium.visible = false;\n  scene.add(podium);\n  const podiumSpots = [\n    new THREE.Vector3(0, 1.1, 12),\n    new THREE.Vector3(-2.3, 0.75, 12),\n    new THREE.Vector3(2.3, 0.5, 12),\n  ];\n\n  // ---------------- per-frame tick\n  const dummy = new THREE.Object3D();\n  let tickFrame = 0;\n  function tick(time, dt, excitement) {\n    // clouds drift every frame (cheap)\n    for (const cl of clouds) {\n      cl.position.x += dt * 1.4;\n      if (cl.position.x > 240) cl.position.x = -240;\n    }\n\n    // crowd animates at half rate — 4 layers x N instances,\n    // chibi proportions matching the athletes: big head, hair-dominant crown\n    tickFrame++;\n    if (tickFrame % 2 === 0) {\n      const amp = 0.12 + excitement * 0.45;\n      const freq = 4 + excitement * 4;\n      for (let i = 0; i < N; i++) {\n        const wave = Math.max(0, Math.sin(time * freq + crowdPhase[i]));\n        const bx = crowdBase[i * 3], by = crowdBase[i * 3 + 1] + wave * amp, bz = crowdBase[i * 3 + 2];\n        const ry = crowdRot[i] + Math.sin(time * 0.7 + crowdPhase[i]) * 0.25;\n        dummy.rotation.set(0, ry, 0);\n        // stocky body\n        dummy.position.set(bx, by, bz);\n        dummy.scale.set(0.64, 0.72, 0.46);\n        dummy.updateMatrix();\n        crowdBody.setMatrixAt(i, dummy.matrix);\n        // big head, straight on the shoulders\n        dummy.position.set(bx, by + 0.58, bz);\n        dummy.scale.set(0.56, 0.56, 0.56);\n        dummy.updateMatrix();\n        crowdHead.setMatrixAt(i, dummy.matrix);\n        // thick hair crown (the main read from above)\n        dummy.position.set(bx, by + 0.87, bz);\n        dummy.scale.set(0.62, 0.3, 0.62);\n        dummy.updateMatrix();\n        crowdHair.setMatrixAt(i, dummy.matrix);\n        // arms: resting bar across the body, thrown overhead on the wave\n        const lift = Math.max(0, wave - 0.35) * (0.9 + excitement * 0.5);\n        dummy.position.set(bx, by + 0.3 + lift, bz);\n        dummy.scale.set(1.0, 0.18, 0.24);\n        dummy.updateMatrix();\n        crowdArms.setMatrixAt(i, dummy.matrix);\n      }\n      crowdBody.instanceMatrix.needsUpdate = true;\n      crowdHead.instanceMatrix.needsUpdate = true;\n      crowdHair.instanceMatrix.needsUpdate = true;\n      crowdArms.instanceMatrix.needsUpdate = true;\n    }\n\n    for (let i = 0; i < flags.length; i++) {\n      const f = flags[i].userData.flag;\n      f.rotation.y = Math.sin(time * 2.2 + i * 1.3) * 0.35;\n      f.scale.y = 1 + Math.sin(time * 5 + i) * 0.06;\n    }\n\n    for (let i = 0; i < 14; i++) {\n      const a = time * (6 + (i % 4)) + i * 2.4;\n      dummy.position.set(Math.sin(a) * 1.0, 0.8 + Math.abs(Math.sin(a * 0.7)) * 1.8 + (i % 3) * 0.8, Math.cos(a * 0.9) * 1.0);\n      const s = 0.6 + Math.abs(Math.sin(a * 1.3)) * 1.3;\n      dummy.scale.set(s, s * 1.4, s);\n      dummy.rotation.set(0, a, 0);\n      dummy.updateMatrix();\n      flame.setMatrixAt(i, dummy.matrix);\n    }\n    flame.instanceMatrix.needsUpdate = true;\n  }\n\n  return { tick, scoreboard: drawScoreboard, podium, podiumSpots, hemi, sun };\n}\n"}],"folders":[{"folder":"events","files":[{"name":"common.js","content":"// events/common.js — formatting + scoring tables shared by all events\nimport { clamp } from '../lib.js';\n\nexport function fmtTime(t) { return t.toFixed(2) + 's'; }\nexport function fmtDist(d) { return d.toFixed(2) + 'm'; }\n\nconst lin = (v, worst, best) => Math.round(clamp((v - worst) / (best - worst), 0, 1) * 1000);\n\nexport const points = {\n  sprint100: (t) => lin(t, 12.8, 9.6),\n  run200: (t) => lin(t, 27.0, 19.2),\n  hurdles110: (t) => lin(t, 17.0, 12.4),\n  run400: (t) => lin(t, 72, 46),\n  run800: (t) => lin(t, 128, 92),\n  relay: (t) => lin(t, 50, 37.5),\n  longJump: (d) => lin(d, 5.2, 8.9),\n  tripleJump: (d) => lin(d, 11.0, 18.0),\n  javelin: (d) => lin(d, 28, 58),\n  hammer: (d) => lin(d, 38, 72),\n  discus: (d) => lin(d, 30, 58),\n  shotPut: (d) => lin(d, 10, 22),\n  highJump: (h) => lin(h, 1.70, 2.46),\n  poleVault: (h) => lin(h, 4.0, 6.2),\n};\n\n// qualifying marks — reach the mark (time under / distance over) for a bonus\nexport const QUAL_BONUS = 100;\nexport const QUALS = {\n  sprint100: 10.80, run200: 21.50, hurdles110: 14.50, run400: 52.0,\n  run800: 108.0, relay: 42.0,\n  longJump: 7.20, tripleJump: 14.50, highJump: 2.06, poleVault: 4.80,\n  javelin: 46.0, hammer: 58.0, discus: 46.0, shotPut: 16.0,\n};\n"},{"name":"jumps.js","content":"// events/jumps.js — long jump + triple jump (shared runway z=24, pit x 14..23)\nimport { THREE, clamp } from '../lib.js';\nimport { game, athletes, player, props, clearProps, followCam, bus } from '../core.js';\nimport * as hud from '../hud.js';\nimport { audio } from '../audio.js';\nimport { spawnParticles, SAND } from '../fx.js';\nimport { makeHighJumpKit } from '../props.js';\nimport { fmtDist, points } from './common.js';\n\nconst RUNWAY_Z = 24;\nconst PIT = [14, 23];\nconst G_JUMP = 15.5;\n\nfunction parkAI(xBase) {\n  athletes.filter((a) => !a.player).forEach((a, i) => {\n    a.group.position.set(xBase + i * 2.5, 0, 29);\n    a.group.rotation.y = Math.PI;\n  });\n}\n\n// ---------------------------------------------------------------- long jump\nexport const longJump = {\n  id: 'longJump', name: 'LONG JUMP', falseStart: false,\n  desc: 'Sprint down the runway with ◀ ▶. Hold SPACE to set your angle, release to leap — take off before the board or it is a foul. Two attempts.',\n  attempt: 0, best: 0, marks: [], phase: 'pre',\n  x: 0, v: 0, y: 0, vx: 0, vy: 0, angle: 0, angleDir: 1, foul: false,\n  landTimer: 0, aiRolls: [],\n\n  enter() {\n    clearProps();\n    this.attempt = 0;\n    this.best = 0;\n    this.marks = [];\n    athletes.forEach((a) => { a.group.visible = true; });\n    this.aiRolls = athletes.filter((a) => !a.player).map((a) => ({\n      name: a.name, d: 7.15 + Math.random() * 1.25,\n    }));\n    this.resetAttempt();\n    hud.showTimer(false);\n    hud.setPower(0, 'SPEED — MASH ◀ ▶');\n    parkAI(16);\n  },\n\n  resetAttempt() {\n    this.phase = this.attempt > 0 ? 'run' : 'pre';\n    this.x = -25; this.v = 0; this.y = 0; this.angle = 0; this.foul = false;\n    this.landTimer = 0;\n    player.group.position.set(this.x, 0, RUNWAY_Z);\n    player.group.rotation.y = Math.PI / 2;\n    hud.setAttempt(this.attempt + 1, 2);\n    hud.setGauge(0, '', false);\n    if (this.attempt > 0) hud.showMsg('ATTEMPT 2', 'go');\n  },\n\n  go() { this.phase = 'run'; game.excitement = 0.6; },\n\n  onMash() {\n    if (this.phase === 'run' || this.phase === 'charge') this.v = Math.min(11.5, this.v + 1.9);\n  },\n\n  onSpace(down) {\n    if (down && this.phase === 'run') {\n      this.phase = 'charge';\n      this.angle = 24;\n      this.angleDir = 1;\n    } else if (!down && this.phase === 'charge') {\n      this.takeoff();\n    }\n  },\n\n  takeoff() {\n    this.foul = this.x > 13.6;\n    const rad = this.angle * Math.PI / 180;\n    const vj = this.v * 1.04;\n    this.vx = Math.cos(rad) * vj;\n    this.vy = Math.max(Math.sin(rad) * vj, 2);\n    this.phase = 'flight';\n    audio.whoosh();\n  },\n\n  land() {\n    this.phase = 'land';\n    this.landTimer = 0;\n    audio.thud();\n    const d = this.foul ? 0 : this.x - 13;\n    if (this.foul || d <= 0.2) {\n      this.marks.push(null);\n      hud.showMsg('FOUL', 'bad');\n      hud.showPopup(this.foul ? 'OVER THE BOARD' : 'TOO SHORT', true);\n    } else {\n      this.marks.push(d);\n      this.best = Math.max(this.best, d);\n      hud.showPopup(fmtDist(d) + '!');\n      game.excitement = Math.min(1, 0.5 + d / 12);\n      if (d > 7.6) audio.cheer();\n    }\n    if (this.x > PIT[0] && this.x < PIT[1]) {\n      spawnParticles(new THREE.Vector3(this.x, 0.3, RUNWAY_Z), SAND, 45, { up: 4, vel: 5, life: 0.9, g: 18 });\n    }\n  },\n\n  update(dt) {\n    if (this.phase === 'pre') { player.update(dt, 'crouch'); return; }\n\n    if (this.phase === 'run' || this.phase === 'charge') {\n      this.v *= Math.exp(-1.45 * dt);\n      this.x += this.v * dt;\n      if (this.phase === 'charge') {\n        this.angle += this.angleDir * 75 * dt;\n        if (this.angle > 68) { this.angle = 68; this.angleDir = -1; }\n        if (this.angle < 22) { this.angle = 22; this.angleDir = 1; }\n        hud.setGauge(this.angle / 90, 'ANGLE ' + Math.round(this.angle) + '°', true);\n      }\n      if (this.x > 15) {\n        this.foul = true;\n        this.vx = Math.max(this.v, 2); this.vy = 2.5;\n        this.phase = 'flight';\n        hud.showMsg('FOUL', 'bad');\n      }\n      player.group.position.set(this.x, 0, RUNWAY_Z);\n      player.update(dt, this.phase === 'charge' ? 'charge' : this.v > 0.5 ? 'run' : 'idle', { speed: this.v });\n      hud.setPower(this.v / 11.5);\n      if (this.v > 1) {\n        this._stepT = (this._stepT || 0) + dt;\n        if (this._stepT > Math.max(0.16, 3.1 / (4 + this.v * 1.9))) { this._stepT = 0; audio.step(); }\n      }\n    } else if (this.phase === 'flight') {\n      this.vy -= G_JUMP * dt;\n      this.x += this.vx * dt;\n      this.y += this.vy * dt;\n      if (this.y <= 0) { this.y = 0; this.land(); }\n      player.group.position.set(this.x, this.y, RUNWAY_Z);\n      player.update(dt, 'air');\n    } else if (this.phase === 'land') {\n      player.update(dt, this.marks[this.marks.length - 1] ? 'celebrate' : 'dejected');\n      this.landTimer += dt;\n      if (this.landTimer > 2.4) {\n        this.attempt++;\n        if (this.attempt < 2) this.resetAttempt();\n        else this.finish();\n      }\n    }\n  },\n\n  finish() {\n    const rows = [\n      { name: player.name, mark: this.best > 0 ? fmtDist(this.best) : 'FOUL', val: this.best, player: true, pts: this.best > 0 ? points.longJump(this.best) : 0 },\n      ...this.aiRolls.map((r) => ({ name: r.name, mark: fmtDist(r.d), val: r.d, player: false, pts: points.longJump(r.d) })),\n    ].sort((a, b) => b.val - a.val);\n    bus.finishEvent(rows);\n  },\n\n  camTarget() {\n    if (this.phase === 'land') {\n      return {\n        pos: new THREE.Vector3(this.x + 7, 2.6, RUNWAY_Z + 2.5),\n        look: new THREE.Vector3(this.x, 2.3, RUNWAY_Z),\n      };\n    }\n    if (this.phase === 'flight') {\n      return followCam(clamp(this.x, 5, 26), RUNWAY_Z, { h: 42, back: 20, lead: 2 });\n    }\n    return followCam(clamp(this.x, -25, 20), RUNWAY_Z);\n  },\n};\n\n// ---------------------------------------------------------------- triple jump\n// Hop-step-jump: charge the first takeoff at the cyan board (x=1), then tap\n// SPACE just before each touchdown to keep your speed through the phases.\nexport const tripleJump = {\n  id: 'tripleJump', name: 'TRIPLE JUMP', falseStart: false,\n  desc: 'Run up with ◀ ▶, hold + release SPACE at the cyan board to HOP, then tap SPACE on each landing to chain the STEP and JUMP. Two attempts.',\n  attempt: 0, best: 0, marks: [], phase: 'pre',\n  x: 0, v: 0, y: 0, vx: 0, vy: 0, angle: 0, angleDir: 1, foul: false,\n  hopIdx: 0, tapAgo: 99, landTimer: 0, aiRolls: [],\n\n  enter() {\n    clearProps();\n    this.attempt = 0;\n    this.best = 0;\n    this.marks = [];\n    athletes.forEach((a) => { a.group.visible = true; });\n    this.aiRolls = athletes.filter((a) => !a.player).map((a) => ({\n      name: a.name, d: 13.4 + Math.random() * 2.7,\n    }));\n    this.resetAttempt();\n    hud.showTimer(false);\n    hud.setPower(0, 'SPEED — MASH ◀ ▶');\n    parkAI(16);\n  },\n\n  resetAttempt() {\n    this.phase = this.attempt > 0 ? 'run' : 'pre';\n    this.x = -25; this.v = 0; this.y = 0; this.angle = 0; this.foul = false;\n    this.hopIdx = 0; this.tapAgo = 99;\n    this.landTimer = 0;\n    player.group.position.set(this.x, 0, RUNWAY_Z);\n    player.group.rotation.y = Math.PI / 2;\n    hud.setAttempt(this.attempt + 1, 2);\n    hud.setGauge(0, '', false);\n    if (this.attempt > 0) hud.showMsg('ATTEMPT 2', 'go');\n  },\n\n  go() { this.phase = 'run'; game.excitement = 0.6; },\n\n  onMash() {\n    if (this.phase === 'run' || this.phase === 'charge') this.v = Math.min(11.2, this.v + 1.9);\n  },\n\n  onSpace(down) {\n    if (down && this.phase === 'run') {\n      this.phase = 'charge';\n      this.angle = 18;\n      this.angleDir = 1;\n    } else if (!down && this.phase === 'charge') {\n      this.launch(0);\n    } else if (down && this.phase === 'flight') {\n      this.tapAgo = 0; // timing tap for the next phase\n    }\n  },\n\n  launch(idx) {\n    this.hopIdx = idx;\n    if (idx === 0) {\n      this.foul = this.x > 1.6;\n      audio.whoosh();\n    }\n    const phaseAngles = [Math.min(this.angle, 34), 25, 37];\n    const rad = phaseAngles[idx] * Math.PI / 180;\n    const vj = this.v * 1.02;\n    this.vx = Math.cos(rad) * vj;\n    this.vy = Math.max(Math.sin(rad) * vj, 2);\n    this.phase = 'flight';\n    hud.showPopup(['HOP!', 'STEP!', 'JUMP!'][idx]);\n  },\n\n  touchdown() {\n    if (this.hopIdx < 2) {\n      // timing: a fresh SPACE tap keeps speed through the phase change\n      const good = this.tapAgo < 0.35;\n      this.v *= good ? 0.88 : 0.7;\n      if (!good) hud.showPopup('LATE!', true);\n      this.tapAgo = 99;\n      audio.beep(good ? 700 : 260, 0.1, 'square', 0.14);\n      spawnParticles(new THREE.Vector3(this.x, 0.2, RUNWAY_Z), SAND, 8, { up: 2.5, vel: 3, life: 0.5, g: 18 });\n      this.launch(this.hopIdx + 1);\n    } else {\n      this.phase = 'land';\n      this.landTimer = 0;\n      audio.thud();\n      const d = this.foul ? 0 : this.x - 1;\n      if (this.foul || d <= 2) {\n        this.marks.push(null);\n        hud.showMsg('FOUL', 'bad');\n      } else {\n        this.marks.push(d);\n        this.best = Math.max(this.best, d);\n        hud.showPopup(fmtDist(d) + '!');\n        game.excitement = Math.min(1, 0.5 + d / 24);\n        if (d > 15) audio.cheer();\n      }\n      if (this.x > PIT[0] && this.x < PIT[1]) {\n        spawnParticles(new THREE.Vector3(this.x, 0.3, RUNWAY_Z), SAND, 45, { up: 4, vel: 5, life: 0.9, g: 18 });\n      }\n    }\n  },\n\n  update(dt) {\n    if (this.phase === 'pre') { player.update(dt, 'crouch'); return; }\n    this.tapAgo += dt;\n\n    if (this.phase === 'run' || this.phase === 'charge') {\n      this.v *= Math.exp(-1.45 * dt);\n      this.x += this.v * dt;\n      if (this.phase === 'charge') {\n        this.angle += this.angleDir * 70 * dt;\n        if (this.angle > 40) { this.angle = 40; this.angleDir = -1; }\n        if (this.angle < 14) { this.angle = 14; this.angleDir = 1; }\n        hud.setGauge(this.angle / 90, 'ANGLE ' + Math.round(this.angle) + '°', true);\n      }\n      if (this.x > 3) {\n        this.foul = true;\n        this.vx = Math.max(this.v, 2); this.vy = 2.5;\n        this.hopIdx = 2;\n        this.phase = 'flight';\n        hud.showMsg('FOUL', 'bad');\n      }\n      player.group.position.set(this.x, 0, RUNWAY_Z);\n      player.update(dt, this.phase === 'charge' ? 'charge' : this.v > 0.5 ? 'run' : 'idle', { speed: this.v });\n      hud.setPower(this.v / 11.2);\n      if (this.v > 1) {\n        this._stepT = (this._stepT || 0) + dt;\n        if (this._stepT > Math.max(0.16, 3.1 / (4 + this.v * 1.9))) { this._stepT = 0; audio.step(); }\n      }\n    } else if (this.phase === 'flight') {\n      this.vy -= G_JUMP * dt;\n      this.x += this.vx * dt;\n      this.y += this.vy * dt;\n      if (this.y <= 0) { this.y = 0; this.touchdown(); }\n      player.group.position.set(this.x, this.y, RUNWAY_Z);\n      player.update(dt, 'air');\n    } else if (this.phase === 'land') {\n      player.update(dt, this.marks[this.marks.length - 1] ? 'celebrate' : 'dejected');\n      this.landTimer += dt;\n      if (this.landTimer > 2.4) {\n        this.attempt++;\n        if (this.attempt < 2) this.resetAttempt();\n        else this.finish();\n      }\n    }\n  },\n\n  finish() {\n    const rows = [\n      { name: player.name, mark: this.best > 0 ? fmtDist(this.best) : 'FOUL', val: this.best, player: true, pts: this.best > 0 ? points.tripleJump(this.best) : 0 },\n      ...this.aiRolls.map((r) => ({ name: r.name, mark: fmtDist(r.d), val: r.d, player: false, pts: points.tripleJump(r.d) })),\n    ].sort((a, b) => b.val - a.val);\n    bus.finishEvent(rows);\n  },\n\n  camTarget() {\n    if (this.phase === 'land') {\n      return {\n        pos: new THREE.Vector3(this.x + 7, 2.6, RUNWAY_Z + 2.5),\n        look: new THREE.Vector3(this.x, 2.3, RUNWAY_Z),\n      };\n    }\n    if (this.phase === 'flight') {\n      return followCam(clamp(this.x, -2, 26), RUNWAY_Z, { h: 42, back: 20, lead: 3 });\n    }\n    return followCam(clamp(this.x, -25, 20), RUNWAY_Z);\n  },\n};\n\n// ---------------------------------------------------------------- high jump\n// The bar keeps rising while you still have misses in hand. Speed comes from\n// the mash, height from a steep launch angle — but a steep leap barely\n// travels a metre forward, so take off tight to the bar.\nconst HJ_BAR_X = 8;\nconst HJ_G = 15.5;\n\nexport const highJump = {\n  id: 'highJump', name: 'HIGH JUMP', falseStart: false,\n  desc: 'The bar starts at 1.90m and rises 8cm with every clearance. Sprint with ◀ ▶, hold SPACE to steepen your angle, release close to the bar to leap. Three misses ends it.',\n  phase: 'pre', barH: 1.9, best: 0, misses: 0,\n  x: 0, v: 0, y: 0, vx: 0, vy: 0, angle: 0, angleDir: 1,\n  cleared: false, crossed: false, landTimer: 0, barFall: null,\n  aiRolls: [], kit: null, started: false,\n\n  enter() {\n    clearProps();\n    this.barH = 1.9;\n    this.best = 0;\n    this.misses = 0;\n    this.started = false;\n    athletes.forEach((a) => { a.group.visible = true; });\n    // AI clears a quantised bar height, same ladder as the player's\n    this.aiRolls = athletes.filter((a) => !a.player).map((a) => ({\n      name: a.name, d: 1.9 + Math.floor(Math.random() * 6) * 0.08,\n    }));\n    this.kit = makeHighJumpKit();\n    this.kit.position.set(HJ_BAR_X, 0, RUNWAY_Z);\n    props.add(this.kit);\n    this.resetAttempt();\n    hud.showTimer(false);\n    hud.setPower(0, 'SPEED — MASH ◀ ▶');\n    parkAI(-20);\n  },\n\n  resetAttempt() {\n    this.phase = this.started ? 'run' : 'pre';\n    this.x = -22; this.v = 0; this.y = 0; this.vx = 0; this.vy = 0;\n    this.angle = 0;\n    this.cleared = false; this.crossed = false;\n    this.landTimer = 0; this.barFall = null;\n    player.group.position.set(this.x, 0, RUNWAY_Z);\n    player.group.rotation.y = Math.PI / 2;\n    const bar = this.kit.userData.bar;\n    bar.position.set(0, this.barH, 0);\n    bar.rotation.set(0, 0, 0);\n    bar.visible = true;\n    hud.setGauge(0, '', false);\n    hud.hud.attempt.textContent = 'BAR ' + this.barH.toFixed(2) + 'M · MISSES ' + this.misses + ' / 3';\n    hud.hud.attempt.classList.remove('hidden');\n  },\n\n  go() { this.phase = 'run'; this.started = true; game.excitement = 0.6; },\n\n  onMash() {\n    if (this.phase === 'run' || this.phase === 'charge') this.v = Math.min(8.8, this.v + 1.9);\n  },\n\n  onSpace(down) {\n    if (down && this.phase === 'run') {\n      this.phase = 'charge';\n      this.angle = 40;\n      this.angleDir = 1;\n    } else if (!down && this.phase === 'charge') {\n      this.takeoff(false);\n    }\n  },\n\n  takeoff(panic) {\n    const rad = (panic ? 28 : this.angle) * Math.PI / 180;\n    const vj = this.v * 1.06;\n    this.vx = Math.max(Math.cos(rad) * vj, 1.6);\n    this.vy = panic ? 3 : Math.max(Math.sin(rad) * vj, 2.5);\n    this.phase = 'flight';\n    audio.whoosh();\n  },\n\n  knockBar() {\n    this.barFall = 0;\n    audio.thud();\n    game.shake = 0.5;\n  },\n\n  update(dt) {\n    if (this.phase === 'pre') { player.update(dt, 'crouch'); return; }\n\n    // the dislodged bar tumbles off the uprights\n    if (this.barFall != null && this.barFall < 1.3) {\n      this.barFall += dt;\n      const bar = this.kit.userData.bar;\n      bar.position.y = Math.max(0.86, this.barH - this.barFall * 2.2);\n      bar.position.x += dt * 1.6;\n      bar.rotation.x += dt * 2.6;\n    }\n\n    if (this.phase === 'run' || this.phase === 'charge') {\n      this.v *= Math.exp(-1.45 * dt);\n      this.x += this.v * dt;\n      if (this.phase === 'charge') {\n        this.angle += this.angleDir * 85 * dt;\n        if (this.angle > 82) { this.angle = 82; this.angleDir = -1; }\n        if (this.angle < 38) { this.angle = 38; this.angleDir = 1; }\n        hud.setGauge(this.angle / 90, 'ANGLE ' + Math.round(this.angle) + '°', true);\n      }\n      if (this.x > HJ_BAR_X - 1.1) {\n        this.takeoff(true);\n        hud.showPopup('TOO LATE!', true);\n      }\n      player.group.position.set(this.x, 0, RUNWAY_Z);\n      player.update(dt, this.phase === 'charge' ? 'charge' : this.v > 0.5 ? 'run' : 'idle', { speed: this.v });\n      hud.setPower(this.v / 8.8);\n      if (this.v > 1) {\n        this._stepT = (this._stepT || 0) + dt;\n        if (this._stepT > Math.max(0.16, 3.1 / (4 + this.v * 1.9))) { this._stepT = 0; audio.step(); }\n      }\n    } else if (this.phase === 'flight') {\n      const px = this.x;\n      this.vy -= HJ_G * dt;\n      this.x += this.vx * dt;\n      this.y += this.vy * dt;\n      if (!this.crossed && px < HJ_BAR_X && this.x >= HJ_BAR_X) {\n        this.crossed = true;\n        if (this.y >= this.barH - 0.25) this.cleared = true; // tucked legs buy 25cm\n        else this.knockBar();\n      }\n      const onMat = this.x > HJ_BAR_X + 0.8;\n      const floor = onMat ? 0.82 : 0;\n      if (this.y <= floor) { this.y = floor; this.land(); }\n      player.group.position.set(this.x, this.y, RUNWAY_Z);\n      player.update(dt, 'air');\n    } else if (this.phase === 'land') {\n      player.update(dt, this.cleared ? 'celebrate' : 'dejected');\n      this.landTimer += dt;\n      if (this.landTimer > 2.2) this.resolve();\n    } else if (this.phase === 'done') {\n      player.update(dt, this.cleared ? 'celebrate' : 'dejected');\n    }\n  },\n\n  land() {\n    this.phase = 'land';\n    this.landTimer = 0;\n    audio.thud();\n    if (!this.crossed) {\n      this.cleared = false;\n      hud.showPopup('TOO EARLY!', true);\n    }\n    if (this.cleared) {\n      hud.showPopup(this.barH.toFixed(2) + 'M CLEARED!');\n      game.excitement = Math.min(1, 0.4 + (this.barH - 1.8) / 0.7);\n      if (this.barH > 2.2) audio.cheer();\n    } else {\n      hud.showMsg('MISS', 'bad');\n    }\n    spawnParticles(new THREE.Vector3(this.x, this.y + 0.2, RUNWAY_Z), SAND, 18, { up: 2.5, vel: 3, life: 0.6, g: 16 });\n  },\n\n  resolve() {\n    if (this.cleared) {\n      this.best = this.barH;\n      this.barH = Math.round((this.barH + 0.08) * 100) / 100;\n    } else {\n      this.misses++;\n    }\n    if (this.misses >= 3 || this.barH > 2.62) { this.phase = 'done'; this.finish(); }\n    else this.resetAttempt();\n  },\n\n  finish() {\n    const rows = [\n      { name: player.name, mark: this.best > 0 ? fmtDist(this.best) : 'NO MARK', val: this.best, player: true, pts: this.best > 0 ? points.highJump(this.best) : 0 },\n      ...this.aiRolls.map((r) => ({ name: r.name, mark: fmtDist(r.d), val: r.d, player: false, pts: points.highJump(r.d) })),\n    ].sort((a, b) => b.val - a.val);\n    bus.finishEvent(rows);\n  },\n\n  camTarget() {\n    if (this.phase === 'flight' || this.phase === 'land') {\n      // low side-on shot so the height of the leap reads clearly\n      return {\n        pos: new THREE.Vector3(this.x - 3.5, Math.max(this.y, 1.4) + 2.6, RUNWAY_Z + 13),\n        look: new THREE.Vector3(this.x, Math.max(this.y, 1.2), RUNWAY_Z),\n      };\n    }\n    return followCam(clamp(this.x, -22, 9), RUNWAY_Z);\n  },\n};\n"},{"name":"laps.js","content":"// events/laps.js — full-oval races: 400m, 800m (stamina) + 4x100m relay\nimport { THREE, clamp, boxMesh } from '../lib.js';\nimport { game, athletes, props, clearProps, followCam, setSlowmo, bus, ROSTER } from '../core.js';\nimport { laneZ, lanePath, lapLength, headingFor, FINISH_S } from '../world.js';\nimport { Athlete } from '../athlete.js';\nimport * as hud from '../hud.js';\nimport { audio } from '../audio.js';\nimport { spawnParticles, CONFETTI, DUST } from '../fx.js';\nimport { fmtTime, points } from './common.js';\n\n// ---------------------------------------------------------------- 400 / 800\nfunction makeLapRace(cfg) {\n  return {\n    id: cfg.id, name: cfg.name, desc: cfg.desc, falseStart: true,\n    racers: [], clock: 0, phase: 'pre', endTimer: 0, D: 0,\n\n    enter() {\n      clearProps();\n      this.phase = 'pre';\n      this.clock = 0;\n      this.endTimer = 0;\n      this.D = cfg.laps * lapLength(laneZ(0));\n      athletes.forEach((a) => { a.group.visible = true; });\n      this.racers = athletes.map((a, i) => ({\n        a, lane: i, o: 0, p: 0, v: 0, st: 1,\n        finished: false, time: 0,\n        ai: a.player ? null : {\n          vmax: cfg.aiV[0] + Math.random() * (cfg.aiV[1] - cfg.aiV[0]),\n          kick: 1.05 + Math.random() * 0.08,\n        },\n      }));\n      const pi = this.racers.findIndex((r) => r.a.player);\n      const tmp = this.racers[pi].lane;\n      this.racers[pi].lane = this.racers[3].lane;\n      this.racers[3].lane = tmp;\n      for (const r of this.racers) {\n        r.o = laneZ(r.lane);\n        this.place(r, 0);\n      }\n      hud.showTimer(true);\n      hud.setPower(0, 'PACE — MASH ◀ ▶');\n      hud.setGauge(1, 'STAMINA', true);\n      hud.setAttempt(null);\n      if (cfg.laps > 1) {\n        hud.hud.attempt.textContent = 'LAP 1 / ' + cfg.laps;\n        hud.hud.attempt.classList.remove('hidden');\n      }\n    },\n\n    place(r, dt, mode, opt) {\n      const s = FINISH_S - this.D + r.p;\n      const pt = lanePath(r.o, s);\n      r.a.group.position.set(pt.x, 0, pt.z);\n      r.a.group.rotation.y = headingFor(pt.dx, pt.dz);\n      r.a.update(dt, mode || 'crouch', opt);\n      r._pt = pt;\n    },\n\n    go() {\n      this.phase = 'race';\n      audio.gun();\n      game.excitement = 0.7;\n    },\n\n    onMash() {\n      if (this.phase !== 'race') return;\n      const p = this.racers.find((r) => r.a.player);\n      if (p.finished) return;\n      const capEff = 5.5 + p.st * (cfg.vcap - 5.5);\n      p.v = Math.min(capEff, p.v + 1.35);\n    },\n\n    onSpace() {},\n\n    update(dt) {\n      if (this.phase === 'pre') {\n        for (const r of this.racers) this.place(r, dt, 'crouch');\n        return;\n      }\n      this.clock += dt;\n\n      for (const r of this.racers) {\n        if (r.ai) {\n          const remaining = this.D - r.p;\n          let vt = r.finished ? 2 : r.ai.vmax;\n          if (!r.finished && remaining < 90) vt *= r.ai.kick;\n          vt *= (1 - Math.exp(-(this.clock + 0.001) / 1.4));\n          r.v += (vt - r.v) * Math.min(1, dt * 2.2);\n        } else if (r.finished && !r.ai) {\n          r.v += (0 - r.v) * dt * 2.5;\n        } else if (!r.finished) {\n          r.v *= Math.exp(-0.85 * dt);\n          // stamina: only a flat-out pace burns, easing off recovers quickly\n          r.st = clamp(r.st - dt * Math.max(0, r.v - 8.0) * cfg.drain + (r.v < 7.2 ? dt * 0.045 : 0), 0, 1);\n          const capEff = 6.5 + r.st * (cfg.vcap - 6.5);\n          r.v = Math.min(r.v, capEff);\n        } else {\n          r.v += (2 - r.v) * dt * 2;\n        }\n\n        if (r.p < this.D + 10) r.p += r.v * dt;\n\n        if (!r.finished && r.p >= this.D) {\n          r.finished = true;\n          r.time = this.clock - (r.p - this.D) / Math.max(r.v, 0.1);\n          if (r.a.player) {\n            game.excitement = 1;\n            audio.cheer();\n            const pos = this.racers.filter((o) => o.finished && o !== r && o.time < r.time).length + 1;\n            hud.showMsg(['1ST!', '2ND', '3RD', '4TH'][pos - 1], pos === 1 ? 'go' : '');\n            setSlowmo(0.35, 0.8);\n            spawnParticles(new THREE.Vector3(r._pt.x, 1.5, r._pt.z), CONFETTI, 40, { up: 6, vel: 6, life: 1.4 });\n          }\n        }\n        const doneMode = r.st < 0.35 ? 'exhausted' : 'celebrate'; // spent runners pant\n        this.place(r, dt, r.v > 0.6 ? 'run' : (r.finished && !r.ai ? doneMode : 'idle'), { speed: r.v });\n\n        if (r.v > 7 && !r.finished && r._pt) {\n          r._dust = (r._dust || 0) + dt;\n          if (r._dust > 0.11) {\n            r._dust = 0;\n            spawnParticles(new THREE.Vector3(r._pt.x - r._pt.dx * 0.5, 0.12, r._pt.z - r._pt.dz * 0.5), DUST, 2, { up: 1.4, vel: 1.8, life: 0.5, g: 3, size: 0.55, spread: 0.4 });\n          }\n        }\n        if (!r.ai && r.v > 1 && !r.finished) {\n          r._stepT = (r._stepT || 0) + dt;\n          if (r._stepT > Math.max(0.16, 3.1 / (4 + r.v * 1.9))) {\n            r._stepT = 0;\n            audio.step();\n          }\n        }\n      }\n\n      const p = this.racers.find((r) => r.a.player);\n      hud.setPower(p.v / cfg.vcap);\n      hud.setGauge(p.st, 'STAMINA', true);\n      hud.setTimer((p.finished ? p.time : this.clock).toFixed(2));\n      if (cfg.laps > 1 && !p.finished) {\n        const lap = Math.min(cfg.laps, Math.floor(p.p / lapLength(laneZ(0))) + 1);\n        hud.hud.attempt.textContent = 'LAP ' + lap + ' / ' + cfg.laps;\n        if (lap === cfg.laps && !this._bell) { this._bell = true; hud.showMsg('BELL LAP!', 'go'); audio.beep(1200, 0.5, 'triangle', 0.2); }\n      }\n\n      if (p.finished) {\n        this.endTimer += dt;\n        const allDone = this.racers.every((r) => r.finished);\n        if ((allDone && this.endTimer > 3.2) || this.endTimer > 5.5) {\n          for (const r of this.racers) {\n            if (!r.finished) { r.finished = true; r.time = this.clock + (this.D - r.p) / Math.max(r.v, 1); }\n          }\n          const rows = this.racers\n            .map((r) => ({ name: r.a.name, mark: fmtTime(r.time), val: r.time, player: r.a.player, pts: cfg.pts(r.time) }))\n            .sort((a, b) => a.val - b.val);\n          this._bell = false;\n          bus.finishEvent(rows);\n        }\n      }\n    },\n\n    playerAnchor() {\n      const p = this.racers.find((r) => r.a.player);\n      if (p.finished) return null;\n      const pt = p._pt || lanePath(p.o, FINISH_S - this.D);\n      return { x: pt.x, y: 2.1, z: pt.z, dx: pt.dx, dz: pt.dz };\n    },\n\n    camTarget() {\n      const p = this.racers.find((r) => r.a.player);\n      const pt = p._pt || lanePath(p.o, FINISH_S - this.D);\n      if (p.finished && this.phase === 'race') {\n        const drift = Math.sin(game.time * 0.6) * 2.2;\n        return {\n          pos: new THREE.Vector3(pt.x + pt.dx * 7.5 - pt.dz * drift, 2.7, pt.z + pt.dz * 7.5 + pt.dx * drift),\n          look: new THREE.Vector3(pt.x, 2.4, pt.z),\n        };\n      }\n      return followCam(pt.x, pt.z, { dx: pt.dx, dz: pt.dz });\n    },\n  };\n}\n\nexport const run200 = makeLapRace({\n  id: 'run200', name: '200M',\n  desc: 'Half a lap — the final bend, then the home straight. Stamina barely bites at this distance, so empty the tank.',\n  laps: 0.5, vcap: 11.0, aiV: [9.3, 10.1], drain: 0.008, pts: points.run200,\n});\n\nexport const run400 = makeLapRace({\n  id: 'run400', name: '400M',\n  desc: 'One full lap in lanes. Mash ◀ ▶ but watch your STAMINA — burn it all on the back straight and you will crawl home.',\n  laps: 1, vcap: 10.6, aiV: [7.9, 8.7], drain: 0.013, pts: points.run400,\n});\n\nexport const run800 = makeLapRace({\n  id: 'run800', name: '800M',\n  desc: 'Two laps. Pace yourself — keep a rhythm, save a kick for the bell lap.',\n  laps: 2, vcap: 10.0, aiV: [7.0, 7.7], drain: 0.017, pts: points.run800,\n});\n\n// ---------------------------------------------------------------- 4 x 100m relay\nexport const relay = {\n  id: 'relay', name: '4 × 100M RELAY', falseStart: true,\n  desc: 'Four legs around the oval. Mash ◀ ▶ to sprint; press SPACE in the exchange zone to pass the baton — miss it and you fumble.',\n  teams: [], clock: 0, phase: 'pre', endTimer: 0, D: 0, legLen: 0,\n\n  enter() {\n    clearProps();\n    this.phase = 'pre';\n    this.clock = 0;\n    this.endTimer = 0;\n    this.D = lapLength(laneZ(0));\n    this.legLen = this.D / 4;\n    athletes.forEach((a) => { a.group.visible = false; });\n    this.teams = ROSTER.map((ros, i) => {\n      const runners = [];\n      for (let k = 0; k < 4; k++) {\n        const a = new Athlete({ ...ros, num: k + 1 });\n        props.add(a.group);\n        runners.push(a);\n      }\n      const baton = boxMesh(0.35, 0.05, 0.05, '#ffd24a', { roughness: 0.4 });\n      props.add(baton);\n      return {\n        name: ros.name, player: !!ros.player, lane: i, o: laneZ(i),\n        runners, baton, p: 0, v: 0, leg: 0, finished: false, time: 0,\n        nextP: null, nextV: 0,\n        frozen: [null, null, null, null], zoneMsg: false,\n        ai: ros.player ? null : {\n          vmax: 8.6 + Math.random() * 1.0,\n          fumble: [1, 2, 3].map(() => Math.random() < 0.08),\n        },\n      };\n    });\n    // player team in lane 3, mid-track\n    const pi = this.teams.findIndex((t) => t.player);\n    const tmp = this.teams[pi].lane;\n    this.teams[pi].lane = this.teams[3].lane;\n    this.teams[3].lane = tmp;\n    this.teams[pi].o = laneZ(this.teams[pi].lane);\n    this.teams[3].o = laneZ(this.teams[3].lane);\n\n    hud.showTimer(true);\n    hud.setPower(0, 'SPEED — MASH ◀ ▶');\n    hud.setGauge(0, '', false);\n    hud.setAttempt(null);\n\n    // paint the exchange zones on each lane — yellow edges, pale ticks between\n    for (const t of this.teams) {\n      for (let k = 1; k <= 3; k++) {\n        const zs = this.legLen * k - 12, ze = this.legLen * k + 9;\n        for (let pp = zs; pp <= ze + 0.01; pp += 2.6) {\n          const pt = this.posFor(t, pp);\n          const edge = pp === zs || pp + 2.6 > ze;\n          const m = boxMesh(edge ? 0.7 : 0.32, 0.05, 1.15, edge ? '#ffd24a' : '#ffedb0');\n          m.position.set(pt.x, 0.11, pt.z);\n          m.rotation.y = -Math.atan2(pt.dz, pt.dx);\n          m.castShadow = false;\n          props.add(m);\n        }\n      }\n    }\n\n    for (const t of this.teams) this.layout(t, 0);\n  },\n\n  posFor(t, p) { return lanePath(t.o, FINISH_S - this.D + p); },\n\n  layout(t, dt) {\n    for (let k = 0; k < 4; k++) {\n      const a = t.runners[k];\n      if (k < t.leg) {\n        if (t.frozen[k]) a.update(dt, 'idle');\n      } else if (k === t.leg) {\n        const pt = this.posFor(t, t.p);\n        a.group.position.set(pt.x, 0, pt.z);\n        a.group.rotation.y = headingFor(pt.dx, pt.dz);\n        const mode = this.phase === 'pre' ? 'crouch'\n          : t.v > 0.6 ? 'run'\n          : (t.finished && t.player ? 'celebrate' : 'idle');\n        a.update(dt, mode, { speed: t.v });\n        t._pt = pt;\n        t.baton.position.set(pt.x + pt.dx * 0.3, 1.05, pt.z + pt.dz * 0.3);\n        t.baton.rotation.y = headingFor(pt.dx, pt.dz);\n      } else {\n        // the receiver actually runs through the zone; others wait at their marks\n        const isReceiver = k === t.leg + 1 && t.nextP != null;\n        const pp = isReceiver ? t.nextP : this.legLen * k;\n        const pt = this.posFor(t, pp);\n        a.group.position.set(pt.x, 0, pt.z);\n        a.group.rotation.y = headingFor(pt.dx, pt.dz);\n        a.update(dt, isReceiver && t.nextV > 0.5 ? 'run' : 'idle', { speed: t.nextV || 0 });\n      }\n    }\n  },\n\n  handoff(t, quality) {\n    t.frozen[t.leg] = true;\n    if (t.nextP != null) {\n      // the baton carries on from wherever the receiver is — no teleporting\n      t.p = Math.max(t.p, t.nextP);\n      t.v = Math.max(t.v * quality, t.nextV);\n    } else {\n      t.v *= quality;\n    }\n    t.leg++;\n    t.nextP = null;\n    t.nextV = 0;\n    t.zoneMsg = false;\n    if (t.player) {\n      game.arrowT = 0; // re-show the marker over your new runner\n      if (quality > 0.9) { hud.showPopup('CLEAN PASS!'); audio.beep(880, 0.15, 'square', 0.15); }\n      else { hud.showPopup('FUMBLED!', true); audio.thud(); game.shake = 0.6; }\n    }\n  },\n\n  go() {\n    this.phase = 'race';\n    audio.gun();\n    game.excitement = 0.7;\n  },\n\n  onMash() {\n    if (this.phase !== 'race') return;\n    const t = this.teams.find((t) => t.player);\n    if (t.finished) return;\n    t.v = Math.min(11.5, t.v + 1.9);\n  },\n\n  onSpace(down) {\n    if (!down || this.phase !== 'race') return;\n    const t = this.teams.find((t) => t.player);\n    if (t.finished || t.leg >= 3 || t.nextP == null) return;\n    // you must actually be within reach of your receiver\n    const gap = t.nextP - t.p;\n    if (gap <= 1.8 && gap >= -1.5) {\n      this.handoff(t, 0.99 - Math.max(0, gap - 0.4) * 0.05);\n    } else if (gap < 9) {\n      hud.showPopup('GET CLOSER!', true);\n    }\n  },\n\n  update(dt) {\n    if (this.phase === 'pre') {\n      for (const t of this.teams) this.layout(t, dt);\n      return;\n    }\n    this.clock += dt;\n\n    for (const t of this.teams) {\n      // the receiver takes off inside the zone as the incoming runner closes in\n      if (!t.finished && t.leg < 3) {\n        const pk = this.legLen * (t.leg + 1);\n        if (t.nextP == null && t.p > pk - 24) { t.nextP = pk - 4; t.nextV = 0; }\n        if (t.nextP != null) {\n          const gap = t.nextP - t.p;\n          if (gap < 11 && gap > 0) t.nextV = Math.min(t.nextV + 7 * dt, Math.max(3.5, t.v * 0.93));\n          t.nextP += t.nextV * dt;\n          if (t.nextP > pk + 9) { t.nextP = pk + 9; t.nextV = 0; } // end of the box — must wait\n        }\n      }\n\n      if (t.ai) {\n        const vt = t.finished ? 2 : t.ai.vmax * (1 - Math.exp(-(this.clock + 0.001) / 1.1));\n        t.v += (vt - t.v) * Math.min(1, dt * 3);\n        if (t.leg < 3 && t.nextP != null && t.nextP - t.p < 1.3) {\n          this.handoff(t, t.ai.fumble[t.leg] ? 0.5 : 0.9 + Math.random() * 0.08);\n        }\n      } else if (t.finished) {\n        t.v += (0 - t.v) * dt * 2.5;\n      } else {\n        t.v *= Math.exp(-1.45 * dt);\n        if (t.leg < 3) {\n          const zoneEnd = this.legLen * (t.leg + 1) + 9;\n          if (t.p > zoneEnd - 21 && !t.zoneMsg) { t.zoneMsg = true; hud.showPopup('CATCH YOUR RUNNER — SPACE!'); }\n          if (t.p >= zoneEnd + 0.5) this.handoff(t, 0.45); // overran the box\n        }\n      }\n\n      if (t.p < this.D + 8) t.p += t.v * dt;\n\n      if (!t.finished && t.p >= this.D) {\n        t.finished = true;\n        t.time = this.clock - (t.p - this.D) / Math.max(t.v, 0.1);\n        if (t.player) {\n          game.excitement = 1;\n          audio.cheer();\n          const pos = this.teams.filter((o) => o.finished && o !== t && o.time < t.time).length + 1;\n          hud.showMsg(['1ST!', '2ND', '3RD', '4TH'][pos - 1], pos === 1 ? 'go' : '');\n          setSlowmo(0.35, 0.8);\n          spawnParticles(new THREE.Vector3(t._pt.x, 1.5, t._pt.z), CONFETTI, 40, { up: 6, vel: 6, life: 1.4 });\n        }\n      }\n      this.layout(t, dt);\n    }\n\n    const p = this.teams.find((t) => t.player);\n    hud.setPower(p.v / 11.5);\n    hud.setGauge(0, 'LEG ' + Math.min(4, p.leg + 1) + ' / 4', true);\n    hud.setTimer((p.finished ? p.time : this.clock).toFixed(2));\n    if (p.v > 1 && !p.finished) {\n      p._stepT = (p._stepT || 0) + dt;\n      if (p._stepT > Math.max(0.16, 3.1 / (4 + p.v * 1.9))) {\n        p._stepT = 0;\n        audio.step();\n      }\n    }\n\n    if (p.finished) {\n      this.endTimer += dt;\n      const allDone = this.teams.every((t) => t.finished);\n      if (allDone || this.endTimer > 4.5) {\n        for (const t of this.teams) {\n          if (!t.finished) { t.finished = true; t.time = this.clock + (this.D - t.p) / Math.max(t.v, 1); }\n        }\n        const rows = this.teams\n          .map((t) => ({ name: t.name, mark: fmtTime(t.time), val: t.time, player: t.player, pts: points.relay(t.time) }))\n          .sort((a, b) => a.val - b.val);\n        bus.finishEvent(rows);\n      }\n    }\n  },\n\n  playerAnchor() {\n    const t = this.teams.find((t) => t.player);\n    if (t.finished) return null;\n    const pt = t._pt || this.posFor(t, t.p);\n    return { x: pt.x, y: 2.1, z: pt.z, dx: pt.dx, dz: pt.dz };\n  },\n\n  camTarget() {\n    const t = this.teams.find((t) => t.player);\n    const pt = t._pt || this.posFor(t, t.p);\n    if (t.finished && this.phase === 'race') {\n      const drift = Math.sin(game.time * 0.6) * 2.2;\n      return {\n        pos: new THREE.Vector3(pt.x + pt.dx * 7.5 - pt.dz * drift, 2.7, pt.z + pt.dz * 7.5 + pt.dx * drift),\n        look: new THREE.Vector3(pt.x, 2.4, pt.z),\n      };\n    }\n    return followCam(pt.x, pt.z, { dx: pt.dx, dz: pt.dz });\n  },\n};\n"},{"name":"races.js","content":"// events/races.js — straight sprints on the home chute: 100m + 110m hurdles\nimport { THREE, clamp } from '../lib.js';\nimport { game, athletes, props, clearProps, followCam, setSlowmo, bus } from '../core.js';\nimport { laneZ, TRACK } from '../world.js';\nimport { makeHurdle } from '../props.js';\nimport * as hud from '../hud.js';\nimport { audio } from '../audio.js';\nimport { spawnParticles, CONFETTI, DUST } from '../fx.js';\nimport { fmtTime, points } from './common.js';\n\nfunction makeStraightRace(cfg) {\n  return {\n    id: cfg.id, name: cfg.name, desc: cfg.desc, falseStart: true,\n    racers: [], clock: 0, phase: 'pre', endTimer: 0, hurdles: [],\n\n    enter() {\n      clearProps();\n      this.phase = 'pre';\n      this.clock = 0;\n      this.endTimer = 0;\n      athletes.forEach((a) => { a.group.visible = true; });\n      this.racers = athletes.map((a, i) => ({\n        a, lane: i, x: TRACK.start - 1.2, v: 0, y: 0, vy: 0, air: false,\n        finished: false, time: 0, _clipped: 0,\n        ai: a.player ? null : {\n          vmax: cfg.aiVmax[0] + Math.random() * (cfg.aiVmax[1] - cfg.aiVmax[0]),\n          ramp: 0.9 + Math.random() * 0.5,\n          mistakes: cfg.hurdleXs ? cfg.hurdleXs.map(() => Math.random() < 0.11) : null,\n        },\n      }));\n      // player runs in lane 3, mid-track\n      const pi = this.racers.findIndex((r) => r.a.player);\n      const tmp = this.racers[pi].lane;\n      this.racers[pi].lane = this.racers[3].lane;\n      this.racers[3].lane = tmp;\n      // guarantee one genuine star in the field\n      const star = this.racers.find((r) => r.ai);\n      if (star) star.ai.vmax = cfg.aiVmax[1] - 0.05;\n\n      this.hurdles = [];\n      if (cfg.hurdleXs) {\n        for (const r of this.racers) {\n          for (const hx of cfg.hurdleXs) {\n            const h = makeHurdle();\n            h.position.set(hx, 0, laneZ(r.lane));\n            h.rotation.y = Math.PI / 2; // bar spans the lane, facing the runners\n            props.add(h);\n            this.hurdles.push({ g: h, x: hx, lane: r.lane, knocked: false, kt: 0 });\n          }\n        }\n      }\n      for (const r of this.racers) {\n        r.a.group.position.set(r.x, 0, laneZ(r.lane));\n        r.a.group.rotation.y = Math.PI / 2;\n      }\n      hud.showTimer(true);\n      hud.setPower(0, 'SPEED — MASH ◀ ▶');\n      hud.setGauge(0, '', false);\n      hud.setAttempt(null);\n    },\n\n    go() {\n      this.phase = 'race';\n      audio.gun();\n      game.excitement = 0.7;\n    },\n\n    onMash() {\n      if (this.phase !== 'race') return;\n      const p = this.racers.find((r) => r.a.player);\n      if (p.finished) return;\n      p.v = Math.min(cfg.vcap, p.v + (p.air ? 0.8 : 1.9));\n    },\n\n    onSpace(down) {\n      if (!down || !cfg.hurdleXs || this.phase !== 'race') return;\n      const p = this.racers.find((r) => r.a.player);\n      if (!p.air && !p.finished) { p.air = true; p.vy = 8.2; audio.whoosh(); }\n    },\n\n    clip(r, hx) {\n      r.v *= 0.5;\n      const h = this.hurdles.find((h) => h.lane === r.lane && h.x === hx);\n      if (h && !h.knocked) { h.knocked = true; audio.thud(); }\n      if (r.a.player) { hud.showPopup('CLIPPED!', true); game.shake = 0.7; }\n    },\n\n    update(dt) {\n      if (this.phase === 'pre') {\n        for (const r of this.racers) r.a.update(dt, 'crouch');\n        return;\n      }\n      this.clock += dt;\n\n      for (const r of this.racers) {\n        if (r.ai) {\n          const vt = r.finished ? 2 : r.ai.vmax * (1 - Math.exp(-this.clock / r.ai.ramp));\n          r.v += (vt - r.v) * Math.min(1, dt * 3);\n        } else if (!r.finished) {\n          r.v *= Math.exp(-1.45 * dt);\n        } else {\n          r.v += (0 - r.v) * dt * 2.5; // pull up and soak in the moment\n        }\n\n        if (cfg.hurdleXs && !r.finished) {\n          const nextHx = cfg.hurdleXs.find((hx) => hx > r.x - 0.6);\n          if (nextHx != null) {\n            const dist = nextHx - r.x;\n            if (r.ai && !r.air && dist < r.v * 0.34 && dist > 0) {\n              const hi = cfg.hurdleXs.indexOf(nextHx);\n              if (!r.ai.mistakes[hi]) { r.air = true; r.vy = 8.2; }\n            }\n            if (Math.abs(dist) < 0.45 && r.y < 0.85 && r._clipped !== nextHx) {\n              r._clipped = nextHx;\n              this.clip(r, nextHx);\n            }\n          }\n        }\n\n        if (r.air) {\n          r.y += r.vy * dt;\n          r.vy -= 24 * dt;\n          if (r.y <= 0) { r.y = 0; r.air = false; }\n        }\n\n        if (r.x < TRACK.finish + 12) r.x += r.v * dt;\n\n        if (!r.finished && r.x >= TRACK.finish) {\n          r.finished = true;\n          r.time = this.clock - (r.x - TRACK.finish) / Math.max(r.v, 0.1);\n          if (r.a.player) {\n            game.excitement = 1;\n            audio.cheer();\n            const pos = this.racers.filter((o) => o.finished && o !== r && o.time < r.time).length + 1;\n            hud.showMsg(['1ST!', '2ND', '3RD', '4TH'][pos - 1], pos === 1 ? 'go' : '');\n            setSlowmo(0.3, 0.9);\n            spawnParticles(new THREE.Vector3(r.x, 1.5, laneZ(r.lane)), CONFETTI, 40, { up: 6, vel: 6, life: 1.4 });\n          }\n        }\n        r.a.group.position.set(r.x, r.y, laneZ(r.lane));\n        const mode = r.air ? 'air' : r.v > 0.6 ? 'run' : (r.finished && !r.ai ? 'celebrate' : 'idle');\n        r.a.update(dt, mode, { speed: r.v });\n\n        if (!r.air && r.v > 7 && !r.finished) {\n          r._dust = (r._dust || 0) + dt;\n          if (r._dust > 0.11) {\n            r._dust = 0;\n            spawnParticles(new THREE.Vector3(r.x - 0.5, 0.12, laneZ(r.lane)), DUST, 2, { up: 1.4, vel: 1.8, life: 0.5, g: 3, size: 0.55, spread: 0.4 });\n          }\n        }\n        // footsteps synced to the player's stride\n        if (!r.ai && !r.air && r.v > 1 && !r.finished) {\n          r._stepT = (r._stepT || 0) + dt;\n          if (r._stepT > Math.max(0.16, 3.1 / (4 + r.v * 1.9))) {\n            r._stepT = 0;\n            audio.step();\n          }\n        }\n      }\n\n      for (const h of this.hurdles) {\n        if (h.knocked && h.kt < 1) {\n          h.kt = Math.min(1, h.kt + dt * 3);\n          h.g.userData.inner.rotation.x = h.kt * 1.35;\n        }\n      }\n\n      const p = this.racers.find((r) => r.a.player);\n      hud.setPower(p.v / cfg.vcap);\n      // the clock freezes on YOUR time the moment you cross the line\n      hud.setTimer((p.finished ? p.time : this.clock).toFixed(2));\n\n      if (p.finished) {\n        this.endTimer += dt;\n        const allDone = this.racers.every((r) => r.finished);\n        if ((allDone && this.endTimer > 3.2) || this.endTimer > 4.6) {\n          for (const r of this.racers) {\n            if (!r.finished) { r.finished = true; r.time = this.clock + (TRACK.finish - r.x) / Math.max(r.v, 1); }\n          }\n          const rows = this.racers\n            .map((r) => ({ name: r.a.name, mark: fmtTime(r.time), val: r.time, player: r.a.player, pts: cfg.pts(r.time) }))\n            .sort((a, b) => a.val - b.val);\n          bus.finishEvent(rows);\n        }\n      }\n    },\n\n    playerAnchor() {\n      const p = this.racers.find((r) => r.a.player);\n      if (p.finished) return null;\n      return { x: p.x, y: p.y + 2.1, z: laneZ(p.lane), dx: 1, dz: 0 };\n    },\n\n    camTarget() {\n      const p = this.racers.find((r) => r.a.player);\n      if (p.finished && this.phase === 'race') {\n        // swoop down in front of the athlete to catch the celebration\n        const drift = Math.sin(game.time * 0.6) * 2.2;\n        return {\n          pos: new THREE.Vector3(p.x + 7.5, 2.7, laneZ(p.lane) + 2.5 + drift),\n          look: new THREE.Vector3(p.x, 2.4, laneZ(p.lane)),\n        };\n      }\n      return followCam(clamp(p.x, TRACK.start - 2, TRACK.finish + 8), laneZ(p.lane));\n    },\n  };\n}\n\nexport const sprint100 = makeStraightRace({\n  id: 'sprint100', name: '100M SPRINT',\n  desc: 'Pure speed. Alternate ◀ ▶ (or A/D) as fast as you can and hit the line first. Jumping the gun restarts the race.',\n  vcap: 11.5, aiVmax: [10.4, 11.4], pts: points.sprint100,\n});\n\nexport const hurdles110 = makeStraightRace({\n  id: 'hurdles110', name: '110M HURDLES',\n  desc: 'Mash ◀ ▶ for speed, tap SPACE to leap each hurdle. Clip one and you stumble.',\n  vcap: 11.2, aiVmax: [8.2, 9.4],\n  hurdleXs: [-43.3, -34.2, -25.1, -15.9, -6.8, 2.3, 11.5, 20.6, 29.8],\n  pts: points.hurdles110,\n});\n"},{"name":"throws.js","content":"// events/throws.js — javelin (runway) + hammer & discus (spin circles)\nimport { THREE, clamp, boxMesh } from '../lib.js';\nimport { game, athletes, player, props, clearProps, followCam, bus } from '../core.js';\nimport * as hud from '../hud.js';\nimport { audio } from '../audio.js';\nimport { spawnParticles, DIRT } from '../fx.js';\nimport { makeJavelin, makeHammer, makeDiscus, makeShot, makeDistanceFlag } from '../props.js';\nimport { fmtDist, points } from './common.js';\n\nfunction parkAI(x, z) {\n  athletes.filter((a) => !a.player).forEach((a, i) => {\n    a.group.position.set(x + i * 2.5, 0, z);\n    a.group.rotation.y = Math.PI;\n  });\n}\n\n// ---------------------------------------------------------------- javelin\nconst JV_LINE = -20; // foul line x; runway z = 0\nexport const javelin = {\n  id: 'javelin', name: 'JAVELIN', falseStart: false,\n  desc: 'Charge down the runway with ◀ ▶, hold SPACE to raise the angle, release to hurl it before the line. Two attempts.',\n  attempt: 0, best: 0, marks: [], phase: 'pre',\n  x: 0, v: 0, angle: 0, angleDir: 1, foul: false,\n  jav: null, jx: 0, jy: 0, jvx: 0, jvy: 0, landTimer: 0, aiRolls: [],\n\n  enter() {\n    clearProps();\n    this.attempt = 0;\n    this.best = 0;\n    this.marks = [];\n    athletes.forEach((a) => { a.group.visible = true; });\n    this.aiRolls = athletes.filter((a) => !a.player).map((a) => ({\n      name: a.name, d: 40 + Math.random() * 15,\n    }));\n    this.jav = makeJavelin();\n    props.add(this.jav);\n    this.resetAttempt();\n    hud.showTimer(false);\n    hud.setPower(0, 'SPEED — MASH ◀ ▶');\n    parkAI(-34, -6);\n  },\n\n  resetAttempt() {\n    this.phase = this.attempt > 0 ? 'run' : 'pre';\n    this.x = -43; this.v = 0; this.angle = 0; this.foul = false; this.landTimer = 0;\n    player.group.position.set(this.x, 0, 0);\n    player.group.rotation.y = Math.PI / 2;\n    hud.setAttempt(this.attempt + 1, 2);\n    hud.setGauge(0, '', false);\n    this.holdJavelin();\n    if (this.attempt > 0) hud.showMsg('ATTEMPT 2', 'go');\n  },\n\n  holdJavelin() {\n    this.jav.position.set(this.x + 0.15, 1.62, 0.32);\n    this.jav.rotation.z = 0.18;\n  },\n\n  go() { this.phase = 'run'; game.excitement = 0.6; },\n\n  onMash() {\n    if (this.phase === 'run' || this.phase === 'charge') this.v = Math.min(11.0, this.v + 1.9);\n  },\n\n  onSpace(down) {\n    if (down && this.phase === 'run') {\n      this.phase = 'charge';\n      this.angle = 20;\n      this.angleDir = 1;\n    } else if (!down && this.phase === 'charge') {\n      this.throwIt(false);\n    }\n  },\n\n  throwIt(forceFoul) {\n    this.foul = forceFoul || this.x > JV_LINE + 0.4;\n    const rad = this.angle * Math.PI / 180;\n    const vt = this.v * 1.0 + 12;\n    this.jvx = Math.cos(rad) * vt;\n    this.jvy = Math.sin(rad) * vt;\n    this.jx = this.x + 0.5;\n    this.jy = 1.7;\n    this.phase = 'flight';\n    audio.whoosh();\n    game.excitement = 0.8;\n  },\n\n  landJav() {\n    this.phase = 'land';\n    this.landTimer = 0;\n    audio.thud();\n    const d = this.foul ? 0 : this.jx - JV_LINE;\n    if (this.foul || d <= 8) {\n      this.marks.push(null);\n      hud.showMsg('FOUL', 'bad');\n    } else {\n      this.marks.push(d);\n      this.best = Math.max(this.best, d);\n      hud.showPopup(fmtDist(d) + '!');\n      game.excitement = Math.min(1, 0.4 + d / 80);\n      if (d > 50) audio.cheer();\n      const fg = makeDistanceFlag();\n      fg.position.set(this.jx, 0, 0);\n      props.add(fg);\n    }\n    spawnParticles(new THREE.Vector3(this.jx, 0.3, 0), DIRT, 25, { up: 3, vel: 4, life: 0.8, g: 16 });\n  },\n\n  update(dt) {\n    if (this.phase === 'pre') { player.update(dt, 'idle'); this.holdJavelin(); return; }\n\n    if (this.phase === 'run' || this.phase === 'charge') {\n      this.v *= Math.exp(-1.45 * dt);\n      this.x += this.v * dt;\n      if (this.phase === 'charge') {\n        this.angle += this.angleDir * 65 * dt;\n        if (this.angle > 80) { this.angle = 80; this.angleDir = -1; }\n        if (this.angle < 18) { this.angle = 18; this.angleDir = 1; }\n        hud.setGauge(this.angle / 90, 'ANGLE ' + Math.round(this.angle) + '°', true);\n      }\n      if (this.x > JV_LINE + 0.4 && this.phase !== 'flight') {\n        this.throwIt(true);\n        hud.showMsg('FOUL', 'bad');\n      } else {\n        player.group.position.set(this.x, 0, 0);\n        player.update(dt, this.phase === 'charge' ? 'charge' : this.v > 0.5 ? 'throwRun' : 'idle', { speed: this.v, jav: true });\n        this.holdJavelin();\n        hud.setPower(this.v / 11.0);\n        if (this.v > 1) {\n          this._stepT = (this._stepT || 0) + dt;\n          if (this._stepT > Math.max(0.16, 3.1 / (4 + this.v * 1.9))) { this._stepT = 0; audio.step(); }\n        }\n      }\n    } else if (this.phase === 'flight') {\n      this.jvy -= 9.1 * dt;\n      this.jvx *= (1 - 0.06 * dt);\n      this.jx += this.jvx * dt;\n      this.jy += this.jvy * dt;\n      this.jav.position.set(this.jx, Math.max(this.jy, 0.15), 0);\n      this.jav.rotation.z = Math.atan2(this.jvy, this.jvx);\n      if (this.jy <= 0.15) this.landJav();\n      this.v *= Math.exp(-2.5 * dt);\n      this.x += this.v * dt;\n      player.group.position.set(Math.min(this.x, JV_LINE - 0.5), 0, 0);\n      player.update(dt, this.v > 0.5 ? 'run' : 'idle', { speed: this.v });\n    } else if (this.phase === 'land') {\n      player.update(dt, this.marks[this.marks.length - 1] ? 'celebrate' : 'dejected');\n      this.landTimer += dt;\n      if (this.landTimer > 2.4) {\n        this.attempt++;\n        if (this.attempt < 2) this.resetAttempt();\n        else this.finish();\n      }\n    }\n  },\n\n  finish() {\n    const rows = [\n      { name: player.name, mark: this.best > 0 ? fmtDist(this.best) : 'FOUL', val: this.best, player: true, pts: this.best > 0 ? points.javelin(this.best) : 0 },\n      ...this.aiRolls.map((r) => ({ name: r.name, mark: fmtDist(r.d), val: r.d, player: false, pts: points.javelin(r.d) })),\n    ].sort((a, b) => b.val - a.val);\n    bus.finishEvent(rows);\n  },\n\n  replayProp() { return this.jav; },\n\n  camTarget() {\n    if (this.phase === 'flight') {\n      return {\n        pos: new THREE.Vector3(this.jx - 4, Math.max(this.jy, 4) + 40, 30),\n        look: new THREE.Vector3(this.jx, Math.max(this.jy, 1), 0),\n      };\n    }\n    if (this.phase === 'land') {\n      const px = Math.min(this.x, JV_LINE - 0.5);\n      return {\n        pos: new THREE.Vector3(px + 6.5, 2.6, 2.5),\n        look: new THREE.Vector3(px, 2.3, 0),\n      };\n    }\n    return followCam(clamp(this.x, -43, -18), 0);\n  },\n};\n\n// ---------------------------------------------------------------- spin throws\n// Mash ◀ ▶ to wind up the spin, SPACE to release. The projectile flies along\n// the release tangent — release as your athlete swings toward the sector.\nfunction makeSpinThrow(cfg) {\n  return {\n    id: cfg.id, name: cfg.name, desc: cfg.desc, falseStart: false,\n    attempt: 0, best: 0, marks: [], phase: 'pre',\n    theta: 0, omega: 0, foul: false,\n    proj: null, px: 0, py: 0, pz: 0, pvx: 0, pvy: 0, pvz: 0,\n    landTimer: 0, aiRolls: [], arrow: null,\n\n    enter() {\n      clearProps();\n      this.attempt = 0;\n      this.best = 0;\n      this.marks = [];\n      athletes.forEach((a) => { a.group.visible = true; });\n      this.aiRolls = athletes.filter((a) => !a.player).map((a) => ({\n        name: a.name, d: cfg.aiRange[0] + Math.random() * (cfg.aiRange[1] - cfg.aiRange[0]),\n      }));\n      this.proj = cfg.makeProj();\n      props.add(this.proj);\n      // release-direction arrow on the ground\n      this.arrow = boxMesh(2.6, 0.12, 0.5, '#7ce34a', { emissive: new THREE.Color('#2f8f1f'), emissiveIntensity: 0.5 });\n      this.arrow.position.set(cfg.cx, 0.25, cfg.cz);\n      props.add(this.arrow);\n      this.resetAttempt();\n      hud.showTimer(false);\n      hud.setPower(0, 'SPIN — MASH ◀ ▶');\n      parkAI(cfg.cx + 4, cfg.cz + 7);\n    },\n\n    resetAttempt() {\n      this.phase = this.attempt > 0 ? 'spin' : 'pre';\n      this.theta = cfg.sector - Math.PI; // start swinging away from the sector\n      this.omega = 0;\n      this.foul = false;\n      this.landTimer = 0;\n      player.group.position.set(cfg.cx, 0, cfg.cz);\n      hud.setAttempt(this.attempt + 1, 2);\n      hud.setGauge(0, '', false);\n      this.arrow.visible = true;\n      if (this.attempt > 0) hud.showMsg('ATTEMPT 2', 'go');\n    },\n\n    go() { this.phase = 'spin'; game.excitement = 0.6; },\n\n    onMash() {\n      if (this.phase !== 'spin') return;\n      this.omega = Math.min(cfg.omegaMax, this.omega + 0.62);\n    },\n\n    onSpace(down) {\n      if (down && this.phase === 'spin' && this.omega > 1.2) this.release();\n    },\n\n    release() {\n      // release direction = tangent of the swing, with a forgiving aim assist\n      let dir = this.theta + Math.PI / 2;\n      let err = Math.atan2(Math.sin(dir - cfg.sector), Math.cos(dir - cfg.sector));\n      const assisted = Math.sign(err) * Math.max(0, Math.abs(err) - 0.35);\n      dir = cfg.sector + assisted;\n      this.foul = Math.abs(assisted) > 0.35;\n      const vt = this.omega * cfg.vFactor * (1 - Math.min(0.3, Math.abs(assisted) * 0.5));\n      const elev = cfg.elev * Math.PI / 180;\n      this.pvx = Math.cos(dir) * Math.cos(elev) * vt;\n      this.pvz = Math.sin(dir) * Math.cos(elev) * vt;\n      this.pvy = Math.sin(elev) * vt;\n      this.px = cfg.cx + Math.cos(this.theta) * 1.2;\n      this.pz = cfg.cz + Math.sin(this.theta) * 1.2;\n      this.py = 1.5;\n      this.phase = 'flight';\n      audio.whoosh();\n      game.excitement = 0.8;\n      this.arrow.visible = false;\n      if (this.foul) hud.showMsg('OUT OF SECTOR', 'bad');\n    },\n\n    landProj() {\n      this.phase = 'land';\n      this.landTimer = 0;\n      audio.thud();\n      player.group.rotation.y = Math.PI / 2 + 0.3; // square up to the camera\n      const d = Math.hypot(this.px - cfg.cx, this.pz - cfg.cz);\n      if (this.foul || d < cfg.minMark) {\n        this.marks.push(null);\n        hud.showMsg('FOUL', 'bad');\n      } else {\n        this.marks.push(d);\n        this.best = Math.max(this.best, d);\n        hud.showPopup(fmtDist(d) + '!');\n        game.excitement = Math.min(1, 0.4 + d / 90);\n        if (d > cfg.cheerAt) audio.cheer();\n        const fg = makeDistanceFlag('#39d4c8');\n        fg.position.set(this.px, 0, this.pz);\n        props.add(fg);\n      }\n      spawnParticles(new THREE.Vector3(this.px, 0.3, this.pz), DIRT, 25, { up: 3, vel: 4, life: 0.8, g: 16 });\n    },\n\n    update(dt) {\n      if (this.phase === 'pre') {\n        player.update(dt, 'idle');\n        this.holdProj();\n        return;\n      }\n      if (this.phase === 'spin') {\n        this.omega = Math.max(0, this.omega - cfg.decay * dt);\n        this.theta += this.omega * dt;\n        player.group.position.set(cfg.cx, 0, cfg.cz);\n        player.group.rotation.y = -this.theta + Math.PI / 2;\n        player.update(dt, this.omega > 0.5 ? 'spin' : 'idle');\n        this.holdProj();\n        // arrow shows the release tangent; green when inside the sector\n        const dir = this.theta + Math.PI / 2;\n        const err = Math.abs(Math.atan2(Math.sin(dir - cfg.sector), Math.cos(dir - cfg.sector)));\n        this.arrow.rotation.y = -dir;\n        this.arrow.position.set(cfg.cx + Math.cos(dir) * 2.6, 0.25, cfg.cz + Math.sin(dir) * 2.6);\n        this.arrow.material.color.set(err < 0.7 ? '#7ce34a' : '#ff4b3a');\n        hud.setPower(this.omega / cfg.omegaMax);\n        hud.setGauge(Math.max(0, 1 - err / Math.PI), err < 0.7 ? 'RELEASE NOW!' : 'WAIT FOR IT…', true);\n      } else if (this.phase === 'flight') {\n        this.pvy -= cfg.g * dt;\n        this.px += this.pvx * dt;\n        this.pz += this.pvz * dt;\n        this.py += this.pvy * dt;\n        this.proj.position.set(this.px, Math.max(this.py, 0.15), this.pz);\n        this.proj.rotation.y += dt * 9;\n        if (this.py <= 0.15) this.landProj();\n        player.update(dt, 'idle');\n      } else if (this.phase === 'land') {\n        player.update(dt, this.marks[this.marks.length - 1] ? 'celebrate' : 'dejected');\n        this.landTimer += dt;\n        if (this.landTimer > 2.4) {\n          this.attempt++;\n          if (this.attempt < 2) this.resetAttempt();\n          else this.finish();\n        }\n      }\n    },\n\n    holdProj() {\n      // held at arm's length while spinning\n      this.proj.position.set(\n        cfg.cx + Math.cos(this.theta) * cfg.holdR,\n        1.35,\n        cfg.cz + Math.sin(this.theta) * cfg.holdR\n      );\n      this.proj.rotation.y = -this.theta;\n    },\n\n    finish() {\n      const rows = [\n        { name: player.name, mark: this.best > 0 ? fmtDist(this.best) : 'FOUL', val: this.best, player: true, pts: this.best > 0 ? cfg.pts(this.best) : 0 },\n        ...this.aiRolls.map((r) => ({ name: r.name, mark: fmtDist(r.d), val: r.d, player: false, pts: cfg.pts(r.d) })),\n      ].sort((a, b) => b.val - a.val);\n      bus.finishEvent(rows);\n    },\n\n    replayProp() { return this.proj; },\n\n    camTarget() {\n      if (this.phase === 'flight') {\n        return {\n          pos: new THREE.Vector3(this.px - 6, Math.max(this.py, 4) + 38, this.pz + 28),\n          look: new THREE.Vector3(this.px, Math.max(this.py, 1), this.pz),\n        };\n      }\n      if (this.phase === 'land') {\n        return {\n          pos: new THREE.Vector3(cfg.cx + 6.5, 2.6, cfg.cz + 3),\n          look: new THREE.Vector3(cfg.cx, 2.3, cfg.cz),\n        };\n      }\n      return followCam(cfg.cx, cfg.cz, { h: 34, back: 18, lead: 1 });\n    },\n  };\n}\n\nexport const hammer = makeSpinThrow({\n  id: 'hammer', name: 'HAMMER THROW',\n  desc: 'Mash ◀ ▶ to whirl the hammer faster and faster, then tap SPACE to let it fly as the green arrow sweeps into the sector. Two attempts.',\n  cx: -36, cz: -14, sector: -0.15,\n  omegaMax: 13, decay: 3.2, vFactor: 2.0, elev: 38, g: 9.8,\n  holdR: 1.5, minMark: 15, cheerAt: 60, aiRange: [48, 66],\n  makeProj: makeHammer, pts: points.hammer,\n});\n\nexport const discus = makeSpinThrow({\n  id: 'discus', name: 'DISCUS',\n  desc: 'Same whirl, different disc — spin up with ◀ ▶ and release with SPACE into the sector. It glides further than you think. Two attempts.',\n  cx: -36, cz: 14, sector: 0.1,\n  omegaMax: 13, decay: 3.2, vFactor: 1.72, elev: 35, g: 8.0,\n  holdR: 1.1, minMark: 12, cheerAt: 48, aiRange: [38, 54],\n  makeProj: makeDiscus, pts: points.discus,\n});\n\nexport const shotPut = makeSpinThrow({\n  id: 'shotPut', name: 'SHOT PUT',\n  desc: 'Whirl in the circle with ◀ ▶ and punch the shot out with SPACE as the arrow sweeps into the sector. It is heavy — no glide, all grunt. Two attempts.',\n  cx: -36, cz: -14, sector: -0.15,\n  omegaMax: 13, decay: 3.4, vFactor: 1.12, elev: 40, g: 9.8,\n  holdR: 0.6, minMark: 6, cheerAt: 19, aiRange: [13.5, 18.5],\n  makeProj: makeShot, pts: points.shotPut,\n});\n"},{"name":"vault.js","content":"// events/vault.js — pole vault: sprint, plant in the box, release at the top\n// of the swing. Shares the jumps runway (z=24), bar line at x=8.\nimport { THREE, clamp } from '../lib.js';\nimport { game, athletes, player, props, clearProps, followCam, bus } from '../core.js';\nimport * as hud from '../hud.js';\nimport { audio } from '../audio.js';\nimport { spawnParticles, SAND } from '../fx.js';\nimport { makeVaultKit, makeVaultPole } from '../props.js';\nimport { fmtDist, points } from './common.js';\n\nconst RUNWAY_Z = 24;\nconst BAR_X = 8;\nconst PIVOT_X = 6.2;      // plant box\nconst POLE_LEN = 4.6;\nconst PLANT_X = PIVOT_X - POLE_LEN * 0.93; // where your hands are when the tip hits the box\nconst G = 12.5;\n\nexport const poleVault = {\n  id: 'poleVault', name: 'POLE VAULT', falseStart: false,\n  desc: 'Sprint with ◀ ▶, press & hold SPACE to plant the pole in the box, then let go at the top of the swing to fire yourself over. Bar starts at 4.40m and rises 20cm per clearance — three misses ends it.',\n  phase: 'pre', barH: 4.4, best: 0, misses: 0, started: false,\n  x: 0, v: 0, y: 0, vx: 0, vy: 0,\n  swing: 0, a0: 0, plantV: 0, poleDrop: null, poleRot0: 0,\n  cleared: false, crossed: false, landTimer: 0, barFall: null,\n  aiRolls: [], kit: null, pole: null,\n\n  enter() {\n    clearProps();\n    this.barH = 4.4; this.best = 0; this.misses = 0; this.started = false;\n    athletes.forEach((a) => { a.group.visible = true; });\n    this.aiRolls = athletes.filter((a) => !a.player).map((a) => ({\n      name: a.name, d: 4.6 + Math.floor(Math.random() * 5) * 0.2,\n    }));\n    this.kit = makeVaultKit();\n    this.kit.position.set(BAR_X, 0, RUNWAY_Z);\n    props.add(this.kit);\n    this.pole = makeVaultPole();\n    props.add(this.pole);\n    this.resetAttempt();\n    hud.showTimer(false);\n    hud.setPower(0, 'SPEED — MASH ◀ ▶');\n    athletes.filter((a) => !a.player).forEach((a, i) => {\n      a.group.position.set(-20 + i * 2.5, 0, 29);\n      a.group.rotation.y = Math.PI;\n    });\n  },\n\n  resetAttempt() {\n    this.phase = this.started ? 'run' : 'pre';\n    this.x = -28; this.v = 0; this.y = 0; this.vx = 0; this.vy = 0;\n    this.swing = 0; this.a0 = 0; this.plantV = 0;\n    this.cleared = false; this.crossed = false;\n    this.landTimer = 0; this.barFall = null; this.poleDrop = null;\n    player.group.position.set(this.x, 0, RUNWAY_Z);\n    player.group.rotation.y = Math.PI / 2;\n    const bar = this.kit.userData.bar;\n    bar.position.set(0, this.barH, 0);\n    bar.rotation.set(0, 0, 0);\n    this.holdPole();\n    hud.setGauge(0, '', false);\n    hud.hud.attempt.textContent = 'BAR ' + this.barH.toFixed(2) + 'M · MISSES ' + this.misses + ' / 3';\n    hud.hud.attempt.classList.remove('hidden');\n  },\n\n  holdPole() {\n    this.pole.position.set(this.x + 0.4, 1.25, RUNWAY_Z + 0.35);\n    this.pole.rotation.z = 0.14;\n  },\n\n  go() { this.phase = 'run'; this.started = true; game.excitement = 0.6; },\n\n  onMash() {\n    if (this.phase === 'run') this.v = Math.min(11.0, this.v + 1.9);\n  },\n\n  onSpace(down) {\n    if (down && this.phase === 'run') {\n      if (this.x >= PLANT_X - 1.6) this.plant();\n      else hud.showPopup('NOT YET!', true);\n    } else if (!down && this.phase === 'swing') {\n      this.launch(this.swing, false);\n    }\n  },\n\n  plant() {\n    this.plantV = this.v;\n    this.a0 = Math.acos(clamp((PIVOT_X - this.x) / POLE_LEN, 0.06, 0.98));\n    this.swing = 0;\n    this.phase = 'swing';\n    audio.thud();\n    game.excitement = 0.75;\n  },\n\n  swingAngle() {\n    return this.a0 + Math.min(this.swing, 1) * (Math.PI / 2 - this.a0);\n  },\n\n  launch(p, weak) {\n    const a = this.a0 + Math.min(p, 1) * (Math.PI / 2 - this.a0);\n    this.x = PIVOT_X - Math.cos(a) * POLE_LEN;\n    this.y = Math.max(0.4, Math.sin(a) * POLE_LEN - 1.5);\n    this.vy = weak ? 1.8 : this.plantV * (0.25 + 0.55 * Math.min(p, 1));\n    this.vx = weak ? 1.0 : 1.8 + Math.min(p, 1) * 0.9;\n    this.phase = 'flight';\n    this.poleDrop = 0;\n    this.poleRot0 = Math.PI - a;\n    audio.whoosh();\n  },\n\n  knockBar() {\n    this.barFall = 0;\n    audio.thud();\n    game.shake = 0.5;\n  },\n\n  update(dt) {\n    if (this.phase === 'pre') { player.update(dt, 'crouch'); this.holdPole(); return; }\n\n    if (this.barFall != null && this.barFall < 1.4) {\n      this.barFall += dt;\n      const bar = this.kit.userData.bar;\n      bar.position.y = Math.max(1.05, this.barH - this.barFall * 3.4);\n      bar.position.x += dt * 1.8;\n      bar.rotation.x += dt * 2.6;\n    }\n    if (this.poleDrop != null) {\n      this.poleDrop += dt;\n      this.pole.position.set(PIVOT_X, 0.12, RUNWAY_Z);\n      this.pole.rotation.z = Math.min(Math.PI - 0.06, this.poleRot0 + this.poleDrop * this.poleDrop * 1.6);\n    }\n\n    if (this.phase === 'run') {\n      this.v *= Math.exp(-1.45 * dt);\n      this.x += this.v * dt;\n      if (this.x > PLANT_X + 0.9) {\n        // ran past the box — a desperate hop into the mat\n        this.plantV = this.v;\n        this.a0 = 0.4;\n        this.launch(0.05, true);\n        hud.showPopup('MISSED THE BOX!', true);\n      } else {\n        player.group.position.set(this.x, 0, RUNWAY_Z);\n        player.update(dt, this.v > 0.5 ? 'throwRun' : 'idle', { speed: this.v });\n        this.holdPole();\n        hud.setPower(this.v / 11.0);\n        if (this.v > 1) {\n          this._stepT = (this._stepT || 0) + dt;\n          if (this._stepT > Math.max(0.16, 3.1 / (4 + this.v * 1.9))) { this._stepT = 0; audio.step(); }\n        }\n      }\n    } else if (this.phase === 'swing') {\n      this.swing += dt * (0.75 + this.plantV * 0.055);\n      if (this.swing > 1.18) {\n        this.launch(1, true);\n        hud.showPopup('HELD TOO LONG!', true);\n        return;\n      }\n      const a = this.swingAngle();\n      this.x = PIVOT_X - Math.cos(a) * POLE_LEN;\n      this.y = Math.max(0, Math.sin(a) * POLE_LEN - 1.5);\n      player.group.position.set(this.x, this.y, RUNWAY_Z);\n      player.update(dt, 'air');\n      this.pole.position.set(PIVOT_X, 0.12, RUNWAY_Z);\n      this.pole.rotation.z = Math.PI - a;\n      hud.setGauge(Math.min(this.swing, 1), this.swing > 0.8 ? 'RELEASE NOW!' : 'SWING…', true);\n    } else if (this.phase === 'flight') {\n      const px = this.x;\n      this.vy -= G * dt;\n      this.x += this.vx * dt;\n      this.y += this.vy * dt;\n      if (!this.crossed && px < BAR_X && this.x >= BAR_X) {\n        this.crossed = true;\n        if (this.y >= this.barH - 0.35) this.cleared = true; // draped clearance\n        else this.knockBar();\n      }\n      const onMat = this.x > BAR_X + 0.5;\n      const floor = onMat ? 1.02 : 0;\n      if (this.y <= floor) { this.y = floor; this.land(); }\n      player.group.position.set(this.x, this.y, RUNWAY_Z);\n      player.update(dt, 'air');\n    } else if (this.phase === 'land') {\n      player.update(dt, this.cleared ? 'celebrate' : 'dejected');\n      this.landTimer += dt;\n      if (this.landTimer > 2.2) this.resolve();\n    } else if (this.phase === 'done') {\n      player.update(dt, this.cleared ? 'celebrate' : 'dejected');\n    }\n  },\n\n  land() {\n    this.phase = 'land';\n    this.landTimer = 0;\n    audio.thud();\n    if (!this.crossed) {\n      this.cleared = false;\n      hud.showPopup('TOO EARLY!', true);\n    }\n    if (this.cleared) {\n      hud.showPopup(this.barH.toFixed(2) + 'M CLEARED!');\n      game.excitement = Math.min(1, 0.4 + (this.barH - 4.2) / 2);\n      if (this.barH > 5.1) audio.cheer();\n    } else {\n      hud.showMsg('MISS', 'bad');\n    }\n    spawnParticles(new THREE.Vector3(this.x, this.y + 0.2, RUNWAY_Z), SAND, 18, { up: 2.5, vel: 3, life: 0.6, g: 16 });\n  },\n\n  resolve() {\n    if (this.cleared) {\n      this.best = this.barH;\n      this.barH = Math.round((this.barH + 0.2) * 100) / 100;\n    } else {\n      this.misses++;\n    }\n    if (this.misses >= 3 || this.barH > 6.4) { this.phase = 'done'; this.finish(); }\n    else this.resetAttempt();\n  },\n\n  finish() {\n    const rows = [\n      { name: player.name, mark: this.best > 0 ? fmtDist(this.best) : 'NO MARK', val: this.best, player: true, pts: this.best > 0 ? points.poleVault(this.best) : 0 },\n      ...this.aiRolls.map((r) => ({ name: r.name, mark: fmtDist(r.d), val: r.d, player: false, pts: points.poleVault(r.d) })),\n    ].sort((a, b) => b.val - a.val);\n    bus.finishEvent(rows);\n  },\n\n  camTarget() {\n    if (this.phase === 'swing' || this.phase === 'flight' || this.phase === 'land') {\n      // pulled-back side shot — the whole vault arc stays in frame\n      return {\n        pos: new THREE.Vector3(this.x - 4.5, Math.max(this.y, 2) + 3.4, RUNWAY_Z + 16),\n        look: new THREE.Vector3(this.x, Math.max(this.y, 1.6), RUNWAY_Z),\n      };\n    }\n    return followCam(clamp(this.x, -28, 7), RUNWAY_Z);\n  },\n};\n"}],"folders":[]}]},{"folder":"other","files":[{"name":"main.sketch","content":""}],"folders":[],"collapsed":true}]},"variants":null,"createdAt":"2026-07-12T18:26:04.259Z","updatedAt":"2026-07-15T13:49:23.986Z"}}