@rydr/game-sdk 3.9.0 → 3.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +64 -0
  2. package/dist/client/PlatformClient.d.ts +6 -0
  3. package/dist/client/PlatformClient.d.ts.map +1 -1
  4. package/dist/client/PlatformClient.js +18 -0
  5. package/dist/client/PlatformClient.js.map +1 -1
  6. package/dist/conversations/index.d.ts +76 -38
  7. package/dist/conversations/index.d.ts.map +1 -1
  8. package/dist/conversations/index.js +136 -58
  9. package/dist/conversations/index.js.map +1 -1
  10. package/dist/conversations/voice-samples.d.ts +45 -0
  11. package/dist/conversations/voice-samples.d.ts.map +1 -0
  12. package/dist/conversations/voice-samples.js +63 -0
  13. package/dist/conversations/voice-samples.js.map +1 -0
  14. package/dist/conversations/voices.d.ts +33 -5
  15. package/dist/conversations/voices.d.ts.map +1 -1
  16. package/dist/conversations/voices.js +59 -31
  17. package/dist/conversations/voices.js.map +1 -1
  18. package/dist/protocol/conversations.d.ts +12 -7
  19. package/dist/protocol/conversations.d.ts.map +1 -1
  20. package/dist/protocol/conversations.js.map +1 -1
  21. package/dist/protocol/version.d.ts +1 -1
  22. package/dist/protocol/version.d.ts.map +1 -1
  23. package/dist/protocol/version.js +1 -1
  24. package/dist/protocol/version.js.map +1 -1
  25. package/dist/ui/README.md +2 -0
  26. package/dist/ui/index.d.ts +2 -0
  27. package/dist/ui/index.d.ts.map +1 -1
  28. package/dist/ui/index.js +2 -0
  29. package/dist/ui/index.js.map +1 -1
  30. package/dist/ui/power-race.d.ts +125 -0
  31. package/dist/ui/power-race.d.ts.map +1 -0
  32. package/dist/ui/power-race.js +726 -0
  33. package/dist/ui/power-race.js.map +1 -0
  34. package/dist/ui/showcase/index.d.ts.map +1 -1
  35. package/dist/ui/showcase/index.js +45 -1
  36. package/dist/ui/showcase/index.js.map +1 -1
  37. package/dist/ui/styles.js +13 -0
  38. package/dist/ui/styles.js.map +1 -1
  39. package/dist/ui/subtitle.d.ts +27 -0
  40. package/dist/ui/subtitle.d.ts.map +1 -0
  41. package/dist/ui/subtitle.js +77 -0
  42. package/dist/ui/subtitle.js.map +1 -0
  43. package/dist/voice-gallery/index.d.ts +31 -0
  44. package/dist/voice-gallery/index.d.ts.map +1 -0
  45. package/dist/voice-gallery/index.js +276 -0
  46. package/dist/voice-gallery/index.js.map +1 -0
  47. package/dist/voiceover-editor/index.d.ts +8 -5
  48. package/dist/voiceover-editor/index.d.ts.map +1 -1
  49. package/dist/voiceover-editor/index.js +158 -76
  50. package/dist/voiceover-editor/index.js.map +1 -1
  51. package/package.json +5 -1
@@ -0,0 +1,726 @@
1
+ /**
2
+ * `createPowerRace` — a timed power-race HUD: two bars race each other.
3
+ *
4
+ * · deadline bar — fills at a constant rate over `durationS` (the clock closing in).
5
+ * · charge bar — fills from accumulated effort (∫ power dt) toward
6
+ * `requiredJoules = targetFtpPct × ftp × durationS`.
7
+ *
8
+ * If the charge bar reaches 100% before the deadline bar does, the rider **wins**; otherwise the
9
+ * clock **wins** (the rider loses). Pacing is free — only total work matters. This is the canonical
10
+ * "push NOW or lose it" beat: a surge you must out-pedal before a timer runs out. It began as
11
+ * Tower-Defense's nuke-intercept mini-game and VIVAX's Vivapierre charge; both are the same widget.
12
+ *
13
+ * Pass `mode: "charge"` for the **no-rival** variant — a solo "charge this up" with no deadline bar and
14
+ * no clock that can beat you (it only resolves `won`, on fill). Same visuals, calmer framing.
15
+ *
16
+ * Self-contained DOM/CSS (no three.js), like the rest of `@rydr/game-sdk/ui`: it injects its own
17
+ * scoped `.rpr-*` stylesheet once into `<head>`. It takes **power values**, not a `session` — the
18
+ * game owns its loop and feeds watts in. FTP-relative by construction (`ftp` sets the bar to clear),
19
+ * so a 150 W and a 350 W rider face the same challenge — never hard-code a raw-watt threshold.
20
+ *
21
+ * ── Sizing contract (same as the rest of the kit) ──────────────────────────────────────────────
22
+ * Tuned for a trainer screen viewed from a few feet away while pedaling. Don't restyle the `.rpr-*`
23
+ * type sizes / dimensions or wrap it in `transform: scale()`. Recolour it with the `theme` option and
24
+ * relabel it with `text` — those are the customization surface; the layout is fixed.
25
+ * ────────────────────────────────────────────────────────────────────────────────────────────────
26
+ *
27
+ * Driving it: `start()`, then pump `setPower(w)` + `tick(dt)` every frame. The HUD self-resolves to
28
+ * `won` / `lost` when either bar crosses 100%; poll `getPhase()` or pass `onResolve`. Drive external
29
+ * VFX (a glow, a light) off `getProgress().charge`.
30
+ */
31
+ const STYLE_ID = "rydr-power-race-styles";
32
+ const VARIANTS = ["twin-stacked", "twin-towers", "single-track"];
33
+ const SCORE_VARIANTS = ["tip", "impact", "above", "infill"];
34
+ /** Grade tiers by precision %, highest first — drives the % colour. A fixed semantic palette, not themed. */
35
+ const GRADES = [
36
+ { min: 95, label: "PERFECT", color: "#ffd23f" },
37
+ { min: 80, label: "GREAT", color: "#22c55e" },
38
+ { min: 65, label: "GOOD", color: "#22d3ee" },
39
+ { min: 0, label: "CLEAN", color: "#9aa7c7" },
40
+ ];
41
+ const gradeForPct = (pct) => GRADES.find((g) => pct >= g.min);
42
+ /** Intro sub-phase durations (seconds). The race begins after their sum (3.5 s). */
43
+ const PRIMING_WARNING_S = 0.5;
44
+ const PRIMING_PROMPT_S = 1.0;
45
+ const PRIMING_BARS_S = 0.5;
46
+ /** Total intro duration in seconds. */
47
+ export const INTRO_TOTAL_S = PRIMING_WARNING_S + PRIMING_PROMPT_S + PRIMING_BARS_S; // 3.5
48
+ /** The built-in "alert" skin — the proven Tower-Defense look. Default when no `theme` is passed. */
49
+ export const DEFAULT_POWER_RACE_THEME = {
50
+ deadline: "#ef4444",
51
+ deadlineDeep: "#7f1d1d",
52
+ charge: "#22d3ee",
53
+ chargeDeep: "#0e7490",
54
+ win: "#22c55e",
55
+ winDeep: "#166534",
56
+ };
57
+ const DEFAULT_TEXT = {
58
+ heading: "INCOMING MISSILE",
59
+ prompt: "PUSH TO INTERCEPT",
60
+ deadlineLabel: "NUKE",
61
+ chargeLabel: "INTERCEPTOR",
62
+ win: "INTERCEPTED",
63
+ lose: "MISSED",
64
+ };
65
+ export function createPowerRace(opts = {}) {
66
+ const ftp = opts.ftp ?? 250;
67
+ const durationS = opts.durationS ?? 5;
68
+ const targetFtpPct = opts.targetFtpPct ?? 1.1;
69
+ let variant = opts.variant ?? "twin-stacked";
70
+ let scoreVariant = opts.scoreVariant ?? null;
71
+ const text = { ...DEFAULT_TEXT, ...(opts.text ?? {}) };
72
+ const theme = { ...DEFAULT_POWER_RACE_THEME, ...(opts.theme ?? {}) };
73
+ const onResolve = opts.onResolve;
74
+ const mode = opts.mode === "charge" ? "charge" : "race";
75
+ ensureStyles();
76
+ if (!VARIANTS.includes(variant))
77
+ variant = "twin-stacked";
78
+ if (scoreVariant && !SCORE_VARIANTS.includes(scoreVariant))
79
+ scoreVariant = null;
80
+ const requiredJoules = targetFtpPct * ftp * durationS;
81
+ const root = document.createElement("div");
82
+ root.className = `rpr v-${variant} mode-${mode}${scoreVariant ? ` has-score place-${scoreVariant}` : ""}`;
83
+ root.innerHTML = innerForVariant(variant);
84
+ // per-instance palette → CSS custom properties (cascades to every descendant)
85
+ root.style.setProperty("--rpr-deadline", theme.deadline);
86
+ root.style.setProperty("--rpr-deadline-deep", theme.deadlineDeep);
87
+ root.style.setProperty("--rpr-charge", theme.charge);
88
+ root.style.setProperty("--rpr-charge-deep", theme.chargeDeep);
89
+ root.style.setProperty("--rpr-win", theme.win);
90
+ root.style.setProperty("--rpr-win-deep", theme.winDeep);
91
+ const headingEl = root.querySelector(".rpr-heading");
92
+ const promptEl = root.querySelector(".rpr-prompt");
93
+ const deadlineFillEl = root.querySelector(".rpr-deadline-fill");
94
+ const chargeBarEl = root.querySelector(".rpr-charge-bar");
95
+ const stampEl = root.querySelector(".rpr-stamp");
96
+ // apply the (possibly overridden) bar labels — twin-stacked uses `.rpr-row-label`.
97
+ const deadlineLabelEl = root.querySelector(".rpr-deadline-bar .rpr-row-label, .rpr-deadline-bar .rpr-tower-label");
98
+ const chargeLabelEl = root.querySelector(".rpr-charge-bar .rpr-row-label, .rpr-charge-bar .rpr-tower-label");
99
+ if (deadlineLabelEl)
100
+ deadlineLabelEl.textContent = text.deadlineLabel;
101
+ if (chargeLabelEl)
102
+ chargeLabelEl.textContent = text.chargeLabel;
103
+ let livePctEl = null;
104
+ if (scoreVariant) {
105
+ const liveEl = document.createElement("div");
106
+ liveEl.className = "rpr-live";
107
+ liveEl.innerHTML = '<span class="rl-pct">0%</span>';
108
+ const deadlineTrack = deadlineFillEl?.parentElement || root;
109
+ deadlineTrack.appendChild(liveEl);
110
+ livePctEl = liveEl.querySelector(".rl-pct");
111
+ }
112
+ let phase = "idle";
113
+ let elapsedS = 0;
114
+ let accumulatedJoules = 0;
115
+ let lastPowerW = 0;
116
+ let lastStage = "idle";
117
+ let resolutionTimer = 0;
118
+ let stampRevealTimer = 0;
119
+ let hideTimer = 0;
120
+ let primingS = 0;
121
+ function setStage(stage) {
122
+ if (stage === lastStage)
123
+ return;
124
+ chargeBarEl.classList.remove("stage-warn", "stage-crit", "stage-danger");
125
+ if (stage !== "idle")
126
+ chargeBarEl.classList.add(`stage-${stage}`);
127
+ lastStage = stage;
128
+ }
129
+ function setPrimingClass(name) {
130
+ root.classList.remove("priming-warning", "priming-prompt", "priming-bars");
131
+ if (name)
132
+ root.classList.add(`priming-${name}`);
133
+ }
134
+ function stageFromGap(gap) {
135
+ if (gap <= 0)
136
+ return "idle";
137
+ if (gap < 0.1)
138
+ return "warn";
139
+ if (gap < 0.22)
140
+ return "crit";
141
+ return "danger";
142
+ }
143
+ function paint() {
144
+ const deadlineProgress = Math.min(1, elapsedS / durationS);
145
+ const chargeProgress = Math.min(1, accumulatedJoules / requiredJoules);
146
+ root.style.setProperty("--deadline", deadlineProgress.toFixed(3));
147
+ root.style.setProperty("--deadline-pct", `${(deadlineProgress * 100).toFixed(1)}%`);
148
+ root.style.setProperty("--charge", chargeProgress.toFixed(3));
149
+ root.style.setProperty("--charge-pct", `${(chargeProgress * 100).toFixed(1)}%`);
150
+ if (mode === "race" && phase === "racing") {
151
+ const gap = deadlineProgress - chargeProgress;
152
+ const stage = stageFromGap(gap);
153
+ setStage(stage);
154
+ root.classList.toggle("behind", stage !== "idle");
155
+ }
156
+ if (phase !== "won" && phase !== "lost") {
157
+ // race: precision = how close to the wire; charge: how full the bar is.
158
+ paintScore((mode === "charge" ? chargeProgress : deadlineProgress) * 100, null);
159
+ }
160
+ }
161
+ function paintScore(precision, result) {
162
+ if (!livePctEl)
163
+ return;
164
+ const pct = Math.max(0, Math.min(100, Math.round(precision)));
165
+ if (!result) {
166
+ livePctEl.textContent = `${pct}%`;
167
+ root.classList.remove("score-resolved", "score-miss");
168
+ return;
169
+ }
170
+ const isMiss = result === "miss";
171
+ livePctEl.textContent = isMiss ? "FAILED" : `${pct}%`;
172
+ root.style.setProperty("--score-color", isMiss ? theme.deadline : gradeForPct(pct).color);
173
+ root.classList.add("score-resolved");
174
+ root.classList.toggle("score-miss", isMiss);
175
+ }
176
+ function applyOutcome(outcome) {
177
+ phase = outcome;
178
+ root.classList.remove("racing", "won", "lost", "behind");
179
+ root.classList.add(outcome);
180
+ if (outcome === "won")
181
+ paintScore(mode === "charge" ? 100 : Math.min(1, elapsedS / durationS) * 100, "hit");
182
+ else
183
+ paintScore(100, "miss");
184
+ setStage("idle");
185
+ headingEl.textContent = "";
186
+ promptEl.textContent = "";
187
+ stampEl.innerHTML = "";
188
+ root.classList.remove("stamp-revealed");
189
+ window.clearTimeout(stampRevealTimer);
190
+ window.clearTimeout(hideTimer);
191
+ stampRevealTimer = window.setTimeout(() => {
192
+ stampEl.innerHTML = `<span class="text">${outcome === "won" ? text.win : text.lose}</span>`;
193
+ root.classList.add("stamp-revealed");
194
+ stampRevealTimer = 0;
195
+ }, 250);
196
+ hideTimer = window.setTimeout(() => {
197
+ root.classList.remove("show");
198
+ hideTimer = 0;
199
+ }, 1250);
200
+ onResolve?.(outcome);
201
+ }
202
+ return {
203
+ element: root,
204
+ start() {
205
+ window.clearTimeout(resolutionTimer);
206
+ resolutionTimer = 0;
207
+ window.clearTimeout(stampRevealTimer);
208
+ stampRevealTimer = 0;
209
+ window.clearTimeout(hideTimer);
210
+ hideTimer = 0;
211
+ phase = "priming-warning";
212
+ primingS = 0;
213
+ elapsedS = 0;
214
+ accumulatedJoules = 0;
215
+ lastPowerW = 0;
216
+ root.classList.remove("racing", "won", "lost", "behind", "stamp-revealed", "bars-open");
217
+ root.classList.add("show");
218
+ setPrimingClass("warning");
219
+ headingEl.textContent = text.heading;
220
+ promptEl.textContent = text.prompt;
221
+ stampEl.textContent = "";
222
+ paint();
223
+ },
224
+ setPower(w) {
225
+ lastPowerW = Math.max(0, Number(w) || 0);
226
+ },
227
+ getPhase() {
228
+ return phase;
229
+ },
230
+ getProgress() {
231
+ return {
232
+ deadline: Math.min(1, elapsedS / durationS),
233
+ charge: Math.min(1, accumulatedJoules / requiredJoules),
234
+ };
235
+ },
236
+ tick(dt) {
237
+ if (phase === "priming-warning" || phase === "priming-prompt" || phase === "priming-bars") {
238
+ primingS += dt;
239
+ if (phase === "priming-warning" && primingS >= PRIMING_WARNING_S) {
240
+ phase = "priming-prompt";
241
+ setPrimingClass("prompt");
242
+ }
243
+ if (phase === "priming-prompt" && primingS >= PRIMING_WARNING_S + PRIMING_PROMPT_S) {
244
+ phase = "priming-bars";
245
+ setPrimingClass("bars");
246
+ root.classList.add("bars-open");
247
+ }
248
+ if (phase === "priming-bars" && primingS >= INTRO_TOTAL_S) {
249
+ phase = "racing";
250
+ setPrimingClass(null);
251
+ root.classList.add("racing");
252
+ }
253
+ return phase;
254
+ }
255
+ if (phase !== "racing")
256
+ return phase;
257
+ elapsedS += dt;
258
+ accumulatedJoules += lastPowerW * dt;
259
+ const deadlineProgress = elapsedS / durationS;
260
+ const chargeProgress = accumulatedJoules / requiredJoules;
261
+ if (chargeProgress >= 1) {
262
+ elapsedS = Math.min(elapsedS, durationS);
263
+ accumulatedJoules = requiredJoules;
264
+ paint();
265
+ applyOutcome("won");
266
+ return phase;
267
+ }
268
+ if (mode === "race" && deadlineProgress >= 1) {
269
+ elapsedS = durationS;
270
+ paint();
271
+ applyOutcome("lost");
272
+ return phase;
273
+ }
274
+ paint();
275
+ return phase;
276
+ },
277
+ showResolution(outcome, opts2 = {}) {
278
+ if (outcome !== "won" && outcome !== "lost")
279
+ return;
280
+ applyOutcome(outcome);
281
+ paint();
282
+ if (resolutionTimer) {
283
+ window.clearTimeout(resolutionTimer);
284
+ resolutionTimer = 0;
285
+ }
286
+ if (opts2.durationS != null && opts2.durationS > 0) {
287
+ resolutionTimer = window.setTimeout(() => {
288
+ root.classList.remove("show");
289
+ resolutionTimer = 0;
290
+ }, opts2.durationS * 1000);
291
+ }
292
+ },
293
+ reset() {
294
+ if (resolutionTimer) {
295
+ window.clearTimeout(resolutionTimer);
296
+ resolutionTimer = 0;
297
+ }
298
+ if (stampRevealTimer) {
299
+ window.clearTimeout(stampRevealTimer);
300
+ stampRevealTimer = 0;
301
+ }
302
+ if (hideTimer) {
303
+ window.clearTimeout(hideTimer);
304
+ hideTimer = 0;
305
+ }
306
+ phase = "idle";
307
+ primingS = 0;
308
+ elapsedS = 0;
309
+ accumulatedJoules = 0;
310
+ lastPowerW = 0;
311
+ root.classList.remove("show", "racing", "won", "lost", "behind", "stamp-revealed", "bars-open");
312
+ setPrimingClass(null);
313
+ setStage("idle");
314
+ paint();
315
+ },
316
+ dispose() {
317
+ if (resolutionTimer)
318
+ window.clearTimeout(resolutionTimer);
319
+ if (stampRevealTimer)
320
+ window.clearTimeout(stampRevealTimer);
321
+ if (hideTimer)
322
+ window.clearTimeout(hideTimer);
323
+ root.remove();
324
+ },
325
+ };
326
+ }
327
+ /** Variant DOM. All three expose the same class hooks the factory drives. */
328
+ function innerForVariant(variant) {
329
+ if (variant === "twin-towers") {
330
+ return `
331
+ <div class="rpr-heading">INCOMING MISSILE</div>
332
+ <div class="rpr-towers">
333
+ <div class="rpr-tower rpr-deadline-bar">
334
+ <div class="rpr-tower-label">NUKE</div>
335
+ <div class="rpr-tower-track">
336
+ <div class="rpr-deadline-fill"></div>
337
+ <div class="rpr-tower-crest"></div>
338
+ </div>
339
+ </div>
340
+ <div class="rpr-tower rpr-charge-bar">
341
+ <div class="rpr-tower-label">INTERCEPTOR</div>
342
+ <div class="rpr-tower-track">
343
+ <div class="rpr-charge-fill"></div>
344
+ <div class="rpr-tower-crest"></div>
345
+ </div>
346
+ </div>
347
+ </div>
348
+ <div class="rpr-prompt">PUSH TO INTERCEPT</div>
349
+ <div class="rpr-stamp"></div>`;
350
+ }
351
+ if (variant === "single-track") {
352
+ return `
353
+ <div class="rpr-heading">INCOMING MISSILE</div>
354
+ <div class="rpr-race">
355
+ <div class="rpr-race-rail rpr-charge-bar">
356
+ <div class="rpr-rail-finish"></div>
357
+ <div class="rpr-deadline-chip rpr-deadline-fill"><span>NUKE</span></div>
358
+ <div class="rpr-charge-chip rpr-charge-fill"><span>INTERCEPTOR</span></div>
359
+ </div>
360
+ </div>
361
+ <div class="rpr-prompt">PUSH TO INTERCEPT</div>
362
+ <div class="rpr-stamp"></div>`;
363
+ }
364
+ // 'twin-stacked' default.
365
+ return `
366
+ <div class="rpr-heading">INCOMING MISSILE</div>
367
+ <div class="rpr-stack">
368
+ <div class="rpr-row rpr-deadline-bar">
369
+ <div class="rpr-row-label">NUKE</div>
370
+ <div class="rpr-row-track"><div class="rpr-deadline-fill"></div></div>
371
+ </div>
372
+ <div class="rpr-row rpr-charge-bar">
373
+ <div class="rpr-row-label">INTERCEPTOR</div>
374
+ <div class="rpr-row-track"><div class="rpr-charge-fill"></div></div>
375
+ </div>
376
+ </div>
377
+ <div class="rpr-prompt">PUSH TO INTERCEPT</div>
378
+ <div class="rpr-stamp"></div>`;
379
+ }
380
+ function ensureStyles() {
381
+ let s = document.getElementById(STYLE_ID);
382
+ if (!s) {
383
+ const fontLink = document.createElement("link");
384
+ fontLink.rel = "stylesheet";
385
+ fontLink.href = "https://fonts.googleapis.com/css2?family=Rajdhani:wght@500;600;700&family=Share+Tech+Mono&display=swap";
386
+ document.head.appendChild(fontLink);
387
+ s = document.createElement("style");
388
+ s.id = STYLE_ID;
389
+ document.head.appendChild(s);
390
+ }
391
+ s.textContent = CSS;
392
+ }
393
+ /**
394
+ * `color-mix` gives the theme colours their translucent glows: `mix(C N%, transparent)` == `C` at
395
+ * alpha N/100 with RGB unchanged, so the original rgba glow alphas are preserved while the hue follows
396
+ * the theme. Near-white sheen insets (highlights) stay literal — they're hue-neutral.
397
+ */
398
+ const CSS = `
399
+ .rpr {
400
+ position: fixed; inset: 0; pointer-events: none; z-index: 50;
401
+ opacity: 0; transition: opacity 220ms ease;
402
+ font-family: 'Rajdhani', 'SF Mono', Menlo, sans-serif;
403
+ color: #fff;
404
+ --deadline: 0; --deadline-pct: 0%;
405
+ --charge: 0; --charge-pct: 0%;
406
+ --rpr-deadline: #ef4444; --rpr-deadline-deep: #7f1d1d;
407
+ --rpr-charge: #22d3ee; --rpr-charge-deep: #0e7490;
408
+ --rpr-win: #22c55e; --rpr-win-deep: #166534;
409
+ }
410
+ .rpr.show { opacity: 1; }
411
+ .rpr::before {
412
+ content: ''; position: absolute; inset: 0; pointer-events: none;
413
+ background: radial-gradient(ellipse at center,
414
+ rgba(8,10,22,0.78) 0%, rgba(8,10,22,0.55) 38%, rgba(8,10,22,0) 72%);
415
+ }
416
+ .rpr > * { position: relative; }
417
+ .rpr-heading {
418
+ position: absolute; left: 50%; transform: translate(-50%, -100%);
419
+ font-weight: 700; font-size: 36px; letter-spacing: 8px;
420
+ text-transform: uppercase; white-space: nowrap;
421
+ color: #fff;
422
+ text-shadow: 0 0 16px var(--rpr-deadline), 0 0 4px #000;
423
+ animation: rpr-flash 0.42s steps(2) infinite;
424
+ }
425
+ .rpr.won .rpr-heading,
426
+ .rpr.lost .rpr-heading { display: none; }
427
+ .rpr-prompt {
428
+ position: absolute; left: 50%; transform: translateX(-50%);
429
+ font-weight: 700; font-size: 22px; letter-spacing: 4px;
430
+ text-transform: uppercase; white-space: nowrap;
431
+ color: var(--rpr-deadline);
432
+ text-shadow: 0 0 14px var(--rpr-deadline), 0 0 4px #000;
433
+ animation: rpr-flash 0.28s steps(2) infinite;
434
+ }
435
+ .rpr.won .rpr-prompt,
436
+ .rpr.lost .rpr-prompt { display: none; }
437
+ .rpr-stamp {
438
+ position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
439
+ display: none;
440
+ align-items: center; gap: 18px;
441
+ font-weight: 900; font-size: 64px; letter-spacing: 10px;
442
+ text-transform: uppercase; white-space: nowrap;
443
+ padding: 18px 36px; border-radius: 10px;
444
+ text-shadow: 0 0 18px rgba(0,0,0,0.8);
445
+ }
446
+ .rpr.won .rpr-stamp,
447
+ .rpr.lost .rpr-stamp { display: flex; }
448
+ .rpr.won .rpr-stamp {
449
+ background: color-mix(in srgb, var(--rpr-win) 18%, transparent); color: #fff;
450
+ border: 3px solid var(--rpr-win);
451
+ box-shadow: 0 0 40px color-mix(in srgb, var(--rpr-win) 60%, transparent),
452
+ inset 0 0 24px color-mix(in srgb, var(--rpr-win) 40%, transparent);
453
+ opacity: 0;
454
+ transition: opacity 0.35s ease;
455
+ }
456
+ .rpr.won.stamp-revealed .rpr-stamp { opacity: 1; }
457
+ .rpr.lost .rpr-stamp {
458
+ background: color-mix(in srgb, var(--rpr-deadline) 18%, transparent); color: #fff;
459
+ border: 3px solid var(--rpr-deadline);
460
+ box-shadow: 0 0 40px color-mix(in srgb, var(--rpr-deadline) 70%, transparent),
461
+ inset 0 0 24px color-mix(in srgb, var(--rpr-deadline) 50%, transparent);
462
+ opacity: 0;
463
+ transition: opacity 0.35s ease;
464
+ }
465
+ .rpr.lost.stamp-revealed .rpr-stamp { opacity: 1; }
466
+ .rpr-stamp .icon { font-size: 80px; line-height: 1; }
467
+ @keyframes rpr-flash {
468
+ 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0.45; }
469
+ }
470
+ .rpr-charge-bar.stage-warn { animation: rpr-pulse 1.0s ease-in-out infinite; }
471
+ .rpr-charge-bar.stage-crit { animation: rpr-pulse 0.42s ease-in-out infinite; }
472
+ .rpr-charge-bar.stage-danger { animation: rpr-pulse 0.2s ease-in-out infinite; }
473
+ @keyframes rpr-pulse {
474
+ 0%, 100% { filter: brightness(1); }
475
+ 50% { filter: brightness(1.45) drop-shadow(0 0 14px var(--rpr-deadline)); }
476
+ }
477
+ .rpr.v-twin-stacked.bars-open .rpr-heading { top: calc(50% - 60px); }
478
+ .rpr.v-twin-stacked.bars-open .rpr-prompt { top: calc(50% + 55px); }
479
+ .rpr.v-twin-stacked .rpr-stack {
480
+ position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
481
+ width: 56vw; max-width: 920px; min-width: 480px;
482
+ display: flex; flex-direction: column; gap: 18px;
483
+ }
484
+ .rpr.v-twin-stacked .rpr-row {
485
+ display: grid; grid-template-columns: 140px 1fr; align-items: center; gap: 14px;
486
+ }
487
+ .rpr.v-twin-stacked .rpr-row-label {
488
+ font-weight: 700; font-size: 18px; letter-spacing: 4px; text-align: right;
489
+ text-transform: uppercase;
490
+ }
491
+ .rpr.v-twin-stacked .rpr-deadline-bar .rpr-row-label { color: var(--rpr-deadline); text-shadow: 0 0 10px var(--rpr-deadline); }
492
+ .rpr.v-twin-stacked .rpr-charge-bar .rpr-row-label { color: var(--rpr-charge); text-shadow: 0 0 10px var(--rpr-charge); }
493
+ .rpr.v-twin-stacked .rpr-row-track {
494
+ position: relative; height: 36px; background: #14141a;
495
+ border: 1px solid #2a2a32; border-radius: 6px; overflow: hidden;
496
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.04), inset 0 -1px 0 rgba(0,0,0,0.4);
497
+ }
498
+ .rpr.v-twin-stacked .rpr-deadline-fill {
499
+ position: absolute; inset: 0; width: var(--deadline-pct);
500
+ background: linear-gradient(90deg, var(--rpr-deadline-deep) 0%, var(--rpr-deadline) 100%);
501
+ box-shadow: 0 0 18px color-mix(in srgb, var(--rpr-deadline) 65%, transparent), inset 0 0 12px rgba(255,200,200,0.25);
502
+ transition: width 0.10s linear;
503
+ }
504
+ .rpr.v-twin-stacked .rpr-charge-fill {
505
+ position: absolute; inset: 0; width: var(--charge-pct);
506
+ background: linear-gradient(90deg, var(--rpr-charge-deep) 0%, var(--rpr-charge) 100%);
507
+ box-shadow: 0 0 18px color-mix(in srgb, var(--rpr-charge) 60%, transparent), inset 0 0 12px rgba(180,240,255,0.25);
508
+ transition: width 0.10s linear;
509
+ }
510
+ .rpr.v-twin-stacked.won .rpr-charge-fill {
511
+ background: linear-gradient(90deg, var(--rpr-win-deep) 0%, var(--rpr-win) 100%);
512
+ box-shadow: 0 0 30px var(--rpr-win), inset 0 0 16px rgba(220,255,200,0.4);
513
+ }
514
+ .rpr.v-twin-stacked .rpr-deadline-bar,
515
+ .rpr.v-twin-stacked .rpr-charge-bar {
516
+ transition: opacity 0.4s ease, transform 0.45s cubic-bezier(0.4, 0, 0.2, 1);
517
+ }
518
+ .rpr.v-twin-stacked.won .rpr-deadline-bar {
519
+ opacity: 0;
520
+ transform: translateY(-10px) scale(0.96);
521
+ pointer-events: none;
522
+ }
523
+ .rpr.v-twin-stacked.won .rpr-charge-bar {
524
+ transform: translateY(-27px);
525
+ }
526
+ .rpr.v-twin-stacked.lost .rpr-charge-bar {
527
+ opacity: 0;
528
+ transform: translateY(10px) scale(0.96);
529
+ pointer-events: none;
530
+ }
531
+ .rpr.v-twin-stacked.lost .rpr-deadline-bar {
532
+ transform: translateY(27px);
533
+ }
534
+ .rpr.v-twin-towers.bars-open .rpr-heading { top: calc(50% - 22vh - 12px); }
535
+ .rpr.v-twin-towers.bars-open .rpr-prompt { top: calc(50% + 22vh + 12px); }
536
+ .rpr.v-twin-towers .rpr-towers {
537
+ position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
538
+ display: flex; gap: 56px; align-items: flex-end;
539
+ }
540
+ .rpr.v-twin-towers .rpr-tower {
541
+ display: flex; flex-direction: column; align-items: center; gap: 12px;
542
+ }
543
+ .rpr.v-twin-towers .rpr-tower-label {
544
+ font-weight: 700; font-size: 16px; letter-spacing: 4px; text-transform: uppercase;
545
+ }
546
+ .rpr.v-twin-towers .rpr-deadline-bar .rpr-tower-label { color: var(--rpr-deadline); text-shadow: 0 0 10px var(--rpr-deadline); }
547
+ .rpr.v-twin-towers .rpr-charge-bar .rpr-tower-label { color: var(--rpr-charge); text-shadow: 0 0 10px var(--rpr-charge); }
548
+ .rpr.v-twin-towers .rpr-tower-track {
549
+ position: relative; width: 72px; height: 44vh; min-height: 280px; max-height: 460px;
550
+ background: #14141a; border: 1px solid #2a2a32; border-radius: 8px; overflow: hidden;
551
+ box-shadow: inset 0 0 0 1px rgba(255,255,255,0.04), inset 0 -2px 0 rgba(0,0,0,0.4);
552
+ }
553
+ .rpr.v-twin-towers .rpr-deadline-fill {
554
+ position: absolute; left: 0; right: 0; bottom: 0; height: var(--deadline-pct);
555
+ background: linear-gradient(0deg, var(--rpr-deadline-deep) 0%, var(--rpr-deadline) 100%);
556
+ box-shadow: 0 0 18px color-mix(in srgb, var(--rpr-deadline) 70%, transparent), inset 0 0 12px rgba(255,200,200,0.25);
557
+ transition: height 0.10s linear;
558
+ }
559
+ .rpr.v-twin-towers .rpr-charge-fill {
560
+ position: absolute; left: 0; right: 0; bottom: 0; height: var(--charge-pct);
561
+ background: linear-gradient(0deg, var(--rpr-charge-deep) 0%, var(--rpr-charge) 100%);
562
+ box-shadow: 0 0 18px color-mix(in srgb, var(--rpr-charge) 65%, transparent), inset 0 0 12px rgba(180,240,255,0.25);
563
+ transition: height 0.10s linear;
564
+ }
565
+ .rpr.v-twin-towers .rpr-tower-crest {
566
+ position: absolute; left: -4px; right: -4px; top: 0; height: 3px;
567
+ background: rgba(255,255,255,0.7);
568
+ box-shadow: 0 0 8px rgba(255,255,255,0.7);
569
+ }
570
+ .rpr.v-twin-towers.won .rpr-charge-fill {
571
+ background: linear-gradient(0deg, var(--rpr-win-deep) 0%, var(--rpr-win) 100%);
572
+ box-shadow: 0 0 30px var(--rpr-win), inset 0 0 16px rgba(220,255,200,0.4);
573
+ }
574
+ .rpr.v-single-track.bars-open .rpr-heading { top: calc(50% - 44px); }
575
+ .rpr.v-single-track.bars-open .rpr-prompt { top: calc(50% + 44px); }
576
+ .rpr.v-single-track .rpr-race {
577
+ position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
578
+ width: 60vw; max-width: 980px; min-width: 520px;
579
+ }
580
+ .rpr.v-single-track .rpr-race-rail {
581
+ position: relative; height: 64px;
582
+ background: linear-gradient(180deg, #0c0c12 0%, #14141a 100%);
583
+ border: 1px solid #2a2a32; border-radius: 32px; overflow: hidden;
584
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.05), inset 0 -1px 0 rgba(0,0,0,0.45);
585
+ }
586
+ .rpr.v-single-track .rpr-rail-finish {
587
+ position: absolute; right: 0; top: 0; bottom: 0; width: 8px;
588
+ background: repeating-linear-gradient(0deg, #fff 0 8px, #111 8px 16px);
589
+ box-shadow: 0 0 14px rgba(255,255,255,0.6);
590
+ }
591
+ .rpr.v-single-track .rpr-deadline-chip,
592
+ .rpr.v-single-track .rpr-charge-chip {
593
+ position: absolute; top: 6px; bottom: 6px;
594
+ width: 14%;
595
+ border-radius: 24px;
596
+ display: flex; align-items: center; justify-content: center;
597
+ font-weight: 700; font-size: 14px; letter-spacing: 3px; color: #fff;
598
+ text-transform: uppercase; text-shadow: 0 0 6px #000;
599
+ transition: left 0.10s linear;
600
+ }
601
+ .rpr.v-single-track .rpr-deadline-chip {
602
+ left: calc(var(--deadline) * (100% - 14%));
603
+ background: linear-gradient(180deg, var(--rpr-deadline) 0%, var(--rpr-deadline-deep) 100%);
604
+ box-shadow: 0 0 18px color-mix(in srgb, var(--rpr-deadline) 65%, transparent), inset 0 0 12px rgba(255,200,200,0.25);
605
+ }
606
+ .rpr.v-single-track .rpr-charge-chip {
607
+ left: calc(var(--charge) * (100% - 14%));
608
+ background: linear-gradient(180deg, var(--rpr-charge) 0%, var(--rpr-charge-deep) 100%);
609
+ box-shadow: 0 0 18px color-mix(in srgb, var(--rpr-charge) 60%, transparent), inset 0 0 12px rgba(180,240,255,0.25);
610
+ }
611
+ .rpr.v-single-track.won .rpr-charge-chip {
612
+ background: linear-gradient(180deg, var(--rpr-win) 0%, var(--rpr-win-deep) 100%);
613
+ box-shadow: 0 0 30px var(--rpr-win), inset 0 0 16px rgba(220,255,200,0.4);
614
+ }
615
+ .rpr.v-single-track.behind .rpr-race-rail {
616
+ background: linear-gradient(180deg, #2a0e0e 0%, #14141a 100%);
617
+ }
618
+ .rpr-heading { top: calc(50% - 5px); }
619
+ .rpr-prompt { top: calc(50% + 5px); }
620
+ .rpr-heading,
621
+ .rpr-prompt {
622
+ transition: top 0.45s cubic-bezier(0.4, 0, 0.2, 1),
623
+ opacity 0.3s ease;
624
+ }
625
+ .rpr.priming-warning .rpr-prompt { opacity: 0; pointer-events: none; }
626
+ .rpr .rpr-stack,
627
+ .rpr .rpr-towers,
628
+ .rpr .rpr-race {
629
+ opacity: 0;
630
+ transition: opacity 0.4s ease;
631
+ }
632
+ .rpr.priming-bars .rpr-stack,
633
+ .rpr.priming-bars .rpr-towers,
634
+ .rpr.priming-bars .rpr-race,
635
+ .rpr.racing .rpr-stack,
636
+ .rpr.racing .rpr-towers,
637
+ .rpr.racing .rpr-race,
638
+ .rpr.won .rpr-stack,
639
+ .rpr.won .rpr-towers,
640
+ .rpr.won .rpr-race,
641
+ .rpr.lost .rpr-stack,
642
+ .rpr.lost .rpr-towers,
643
+ .rpr.lost .rpr-race {
644
+ opacity: 1;
645
+ }
646
+ .rpr.priming-warning::after,
647
+ .rpr.priming-prompt::after,
648
+ .rpr.priming-bars::after {
649
+ content: ''; position: absolute; inset: 0; pointer-events: none;
650
+ border: 8px solid var(--rpr-deadline);
651
+ box-shadow: inset 0 0 60px color-mix(in srgb, var(--rpr-deadline) 55%, transparent);
652
+ animation: rpr-klaxon-strobe 0.4s steps(2) infinite;
653
+ z-index: 1;
654
+ }
655
+ @keyframes rpr-klaxon-strobe {
656
+ 0%, 49% { opacity: 1; }
657
+ 50%, 100% { opacity: 0.35; }
658
+ }
659
+ .rpr .rpr-heading {
660
+ animation: rpr-flash 0.42s steps(2) infinite,
661
+ rpr-klaxon-shake 0.08s steps(2) infinite;
662
+ }
663
+ @keyframes rpr-klaxon-shake {
664
+ 0%, 100% { transform: translate(-50%, -100%); }
665
+ 50% { transform: translate(calc(-50% + 1px), -100%); }
666
+ }
667
+ .rpr.has-score .rpr-deadline-bar .rpr-row-track { overflow: visible; }
668
+ .rpr-live {
669
+ position: absolute; pointer-events: none; white-space: nowrap; z-index: 3;
670
+ display: flex; flex-direction: column; line-height: 1;
671
+ font-family: 'Share Tech Mono', 'SF Mono', monospace; font-weight: 700;
672
+ opacity: 0; transition: opacity 0.3s ease;
673
+ }
674
+ .rpr.priming-bars .rpr-live,
675
+ .rpr.racing .rpr-live,
676
+ .rpr.score-resolved .rpr-live { opacity: 1; }
677
+ .rpr-live .rl-pct { color: #ffd9d0; text-shadow: 0 0 8px var(--rpr-deadline), 0 1px 3px #000; }
678
+ .rpr.place-tip .rpr-live {
679
+ top: 50%; left: var(--deadline-pct, 0%); align-items: center;
680
+ transform: translate(-50%, -50%); font-size: 20px;
681
+ }
682
+ .rpr.place-infill .rpr-live {
683
+ top: 50%; left: var(--deadline-pct, 0%); align-items: flex-end;
684
+ transform: translate(-108%, -50%); font-size: 18px;
685
+ }
686
+ .rpr.place-impact .rpr-live {
687
+ top: 50%; right: 10px; align-items: flex-end;
688
+ transform: translateY(-50%); font-size: 20px;
689
+ }
690
+ .rpr.place-above .rpr-live {
691
+ bottom: calc(100% + 12px); left: var(--deadline-pct, 0%); align-items: center;
692
+ transform: translateX(-50%); font-size: 38px;
693
+ }
694
+ .rpr.place-above .rpr-live::after {
695
+ content: '▼'; position: absolute; left: 50%; transform: translateX(-50%);
696
+ bottom: -14px; font-size: 13px; color: #fff; opacity: 0.85;
697
+ }
698
+ .rpr.has-score.v-twin-stacked.won .rpr-deadline-bar,
699
+ .rpr.has-score.v-twin-stacked.lost .rpr-deadline-bar,
700
+ .rpr.has-score.v-twin-stacked.won .rpr-charge-bar,
701
+ .rpr.has-score.v-twin-stacked.lost .rpr-charge-bar {
702
+ opacity: 1 !important; transform: none !important;
703
+ }
704
+ .rpr.score-resolved .rpr-live .rl-pct {
705
+ color: var(--score-color, #fff);
706
+ text-shadow: 0 0 16px var(--score-color), 0 2px 4px #000;
707
+ }
708
+ .rpr.place-impact.score-resolved .rpr-live {
709
+ animation: rpr-pct-bounce 0.55s cubic-bezier(0.2, 1.5, 0.35, 1) both;
710
+ }
711
+ @keyframes rpr-pct-bounce {
712
+ 0% { transform: translateY(-50%) scale(1); }
713
+ 32% { transform: translateY(-50%) scale(1.6); }
714
+ 58% { transform: translateY(-50%) scale(0.92); }
715
+ 100% { transform: translateY(-50%) scale(1.28); }
716
+ }
717
+ /* charge mode — no rival: hide the deadline bar and calm the alarm framing. The single charge bar
718
+ stays centred by the flex/align layout it already lives in; only the effort bar remains. */
719
+ .rpr.mode-charge .rpr-deadline-bar,
720
+ .rpr.mode-charge .rpr-deadline-chip { display: none; }
721
+ .rpr.mode-charge.priming-warning::after,
722
+ .rpr.mode-charge.priming-prompt::after,
723
+ .rpr.mode-charge.priming-bars::after { display: none; }
724
+ .rpr.mode-charge .rpr-heading { animation: rpr-flash 0.42s steps(2) infinite; }
725
+ `;
726
+ //# sourceMappingURL=power-race.js.map