@energy8platform/platform-core 0.26.1 → 0.27.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.
package/src/shell/i18n.ts CHANGED
@@ -1,56 +1,16 @@
1
1
  import { LOCALES } from './locales';
2
+ import { SOCIAL_REPLACEMENTS } from '@energy8platform/game-sdk/social';
2
3
 
3
4
  // Social-casino language. English is the source (and, for now, the only) language; `socialize`
4
5
  // rewrites the restricted gambling vocabulary into social-safe phrasing while preserving case.
5
6
  //
6
- // Ordering matters: the longest / most specific phrases are listed first so they win over their
7
- // constituent words (e.g. "buy bonus" before "buy", "pay out" before "pay"). The JS alternation
8
- // tries entries left-to-right at each position, so a phrase earlier in this list takes priority.
9
- //
10
- // Conflicting duplicates in the source table are resolved to a single replacement here
11
- // (betting→playing, total bet→total play, paid out→won, pays out→win).
12
- const RULES: ReadonlyArray<readonly [string, string]> = [
13
- ['be awarded to player’s accounts', 'appear in player’s accounts'],
14
- ["be awarded to player's accounts", "appear in player's accounts"],
15
- ['place your bets', 'come and play / join in the game'],
16
- ['at the cost of', 'for'],
17
- ['cost of', 'can be played for'],
18
- ['win feature', 'play feature'],
19
- ['total bet', 'total play'],
20
- ['buy bonus', 'get bonus'],
21
- ['bonus buy', 'bonus / feature'],
22
- ['pay out', 'win / won'],
23
- ['paid out', 'won'],
24
- ['pays out', 'win'],
25
- ['payout', 'win'], // single word; "pay out" (spaced) is handled above
26
- ['paytable', 'win table'],
27
- ['paylines', 'winlines'],
28
- ['payline', 'winline'],
29
- ['bet/s', 'play/s'],
30
- ['betting', 'playing'],
31
- ['rebet', 'respin'],
32
- ['stake', 'play amount'],
33
- ['payer', 'winner'],
34
- ['bets', 'plays'],
35
- ['pays', 'wins'],
36
- ['paid', 'won'],
37
- ['bought', 'instantly triggered'],
38
- ['purchase', 'play'],
39
- ['price', 'play'],
40
- ['cost', 'play'], // standalone; the "cost of" / "at the cost of" phrases above win first
41
- ['deposit', 'get coins'],
42
- ['withdraw', 'redeem'],
43
- ['currency', 'token'],
44
- ['gamble', 'play'],
45
- ['wager', 'play'],
46
- ['credit', 'balance'],
47
- ['money', 'coins'],
48
- ['cash', 'coins'],
49
- ['fund', 'balance'],
50
- ['bet', 'play'],
51
- ['pay', 'win'],
52
- ['buy', 'play'],
53
- ];
7
+ // The dictionary is the SHARED, canonical list in `@energy8platform/game-sdk/social` the DOM
8
+ // shell, the Pixi shell and stake-bridge all consume it, so the vocabulary can't drift. We only
9
+ // sort it here: the combined regex below matches the FIRST alternative that fits at a position, so
10
+ // the longest / most specific phrases must come first (e.g. "buy bonus" before "buy").
11
+ const RULES: ReadonlyArray<readonly [string, string]> = [...SOCIAL_REPLACEMENTS]
12
+ .sort((a, b) => b.from.length - a.from.length)
13
+ .map((r) => [r.from, r.to] as const);
54
14
 
55
15
  const MAP = new Map(RULES.map(([k, v]) => [k.toLowerCase(), v] as const));
56
16
  const escapeRe = (s: string): string => s.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
@@ -1,10 +1,11 @@
1
1
  import { SHELL_FONT_CSS } from './fonts';
2
+ import { SHELL_DIGIT_FONT_CSS } from './fonts-digits';
2
3
 
3
4
  export const SHELL_ROOT_ID = '__ge-game-shell__';
4
5
 
5
6
  // Inter (bundled, base64) leads the stack so the shell renders identically on every
6
7
  // platform; the system fonts stay as graceful fallback if the webfont ever fails to load.
7
- export const SHELL_CSS = SHELL_FONT_CSS + `
8
+ export const SHELL_CSS = SHELL_FONT_CSS + SHELL_DIGIT_FONT_CSS + `
8
9
  #${SHELL_ROOT_ID} {
9
10
  position: absolute; inset: 0;
10
11
  container-type: size; /* query container → centred modals size in cq units (responsive on every screen) */
@@ -55,7 +56,9 @@ export const SHELL_CSS = SHELL_FONT_CSS + `
55
56
  color:#fff; font-weight:800; letter-spacing:.02em; font-size:13px; line-height:1.08; text-align:center;
56
57
  display:flex; align-items:center; justify-content:center; transition:transform .08s ease, box-shadow .12s ease; }
57
58
  #${SHELL_ROOT_ID} .ge-shell-buybonus span { display:inline-block; will-change:transform; }
58
- #${SHELL_ROOT_ID} .ge-shell-buybonus:hover { box-shadow:0 0 0 3px var(--shell-accent), 0 0 16px 1px var(--shell-accent); }
59
+ /* the ticket glyph fills the badge (em-sized off the button's font-size, so it scales per context) */
60
+ #${SHELL_ROOT_ID} .ge-shell-buybonus .ge-bb-tk { display:flex; font-size:3.5em; color:#0b0e16; }
61
+ #${SHELL_ROOT_ID} .ge-shell-buybonus:hover { box-shadow:0 0 11px 1px var(--shell-accent); }
59
62
  #${SHELL_ROOT_ID} .ge-shell-buybonus:hover span { animation:ge-bb-pulse .7s ease-in-out infinite; }
60
63
  #${SHELL_ROOT_ID} .ge-shell-buybonus:active { transform:scale(.96); }
61
64
  #${SHELL_ROOT_ID} .ge-shell-buybonus[disabled] { filter:grayscale(.5) brightness(.72); box-shadow:none; cursor:default; }
@@ -66,9 +69,10 @@ export const SHELL_CSS = SHELL_FONT_CSS + `
66
69
  #${SHELL_ROOT_ID} .ge-shell-barhost { position:absolute; left:0; right:0; bottom:0; pointer-events:none;
67
70
  display:flex; flex-direction:column; align-items:center; justify-content:flex-end; gap:4px;
68
71
  transform-origin:bottom center; }
69
- /* bottom bar: transparent, two zones (wide default) */
70
- #${SHELL_ROOT_ID} .ge-shell-bottom { width:100%; box-sizing:border-box; pointer-events:none;
71
- display:flex; align-items:center; justify-content:space-between; padding:0 18px 6px; gap:14px; }
72
+ /* bottom bar: a row of [BUY BONUS (outside-left)] + [one continuous dark panel] (wide default).
73
+ Capped at 750px and centred (the barhost centres it) so it doesn't stretch edge-to-edge on wide screens. */
74
+ #${SHELL_ROOT_ID} .ge-shell-bottom { width:100%; max-width:850px; box-sizing:border-box; pointer-events:none;
75
+ display:flex; align-items:center; justify-content:flex-start; padding:0 14px 8px; gap:10px; }
72
76
  #${SHELL_ROOT_ID} .ge-zone { display:flex; align-items:center; gap:14px; pointer-events:none; }
73
77
  #${SHELL_ROOT_ID} .ge-zone > * { pointer-events:auto; }
74
78
  #${SHELL_ROOT_ID} .ge-betstep { display:flex; flex-direction:column; gap:2px; }
@@ -76,11 +80,11 @@ export const SHELL_CSS = SHELL_FONT_CSS + `
76
80
  #${SHELL_ROOT_ID} .ge-autoturbo { display:flex; flex-direction:column; gap:2px; }
77
81
  #${SHELL_ROOT_ID} .ge-autoturbo .ge-iconbtn { width:40px; height:30px; }
78
82
 
79
- /* mobile (portrait) — full-width stacked plaques: [balance · win] · [controls] · [− bet +] */
80
- #${SHELL_ROOT_ID}.ge-mobile .ge-shell-bottom { flex-direction:column; align-items:center; gap:14px; padding:8px 12px 8px; }
81
- #${SHELL_ROOT_ID}.ge-mobile .ge-m-top { width:100%; height:46px; justify-content:space-between; }
83
+ /* mobile (portrait) — two levels: [controls bar] over a small [balance · − bet + · win] info pill */
84
+ #${SHELL_ROOT_ID}.ge-mobile .ge-shell-bottom { flex-direction:column; align-items:center; gap:10px; padding:8px 12px 8px; }
85
+ /* level 1 the dark controls bar (white icons, spin disc, buy badge) */
82
86
  #${SHELL_ROOT_ID}.ge-mobile .ge-m-controls { display:flex; align-items:center; justify-content:space-between;
83
- width:100%; box-sizing:border-box; height:62px; border-radius:18px; padding:0 18px; }
87
+ width:100%; box-sizing:border-box; height:62px; border-radius:16px; padding:0 18px; background:var(--shell-bar); }
84
88
  #${SHELL_ROOT_ID}.ge-mobile .ge-m-controls .ge-iconbtn,
85
89
  #${SHELL_ROOT_ID}.ge-mobile .ge-m-controls .ge-rd,
86
90
  #${SHELL_ROOT_ID}.ge-mobile .ge-m-controls .ge-rd .ge-lbl { color:#fff; }
@@ -88,8 +92,28 @@ export const SHELL_CSS = SHELL_FONT_CSS + `
88
92
  /* mobile: restore accent hover + autoplay glow (the white rule above out-specifies the base ones) */
89
93
  #${SHELL_ROOT_ID}.ge-mobile .ge-m-controls .ge-iconbtn:hover,
90
94
  #${SHELL_ROOT_ID}.ge-mobile .ge-m-controls .ge-iconbtn.ge-glow { color:var(--shell-accent); }
91
- #${SHELL_ROOT_ID}.ge-mobile .ge-m-bet { width:100%; height:46px; padding:0 18px; gap:8px; justify-content:space-between; }
92
- #${SHELL_ROOT_ID}.ge-mobile .ge-shell-spin { width:84px; height:84px; font-size:66px; }
95
+ /* level 2 — the small info pill. Readouts are smaller and each can shrink long numbers (see fitBet). */
96
+ #${SHELL_ROOT_ID}.ge-mobile .ge-m-info { display:flex; align-items:center; justify-content:space-between;
97
+ width:100%; box-sizing:border-box; height:40px; border-radius:12px; padding:0 14px; gap:10px;
98
+ background:var(--shell-plaque-glass); }
99
+ #${SHELL_ROOT_ID}.ge-mobile .ge-m-info > .ge-rd { flex:1 1 0; min-width:0; overflow:hidden; color:#fff;
100
+ text-shadow:none; font-size:11px; transform-origin:left center; }
101
+ #${SHELL_ROOT_ID}.ge-mobile .ge-m-info > .ge-win { text-align:right; }
102
+ #${SHELL_ROOT_ID}.ge-mobile .ge-m-info > .ge-win .ge-rd-val { transform-origin:right center; }
103
+ #${SHELL_ROOT_ID}.ge-mobile .ge-m-info .ge-rd .ge-lbl { color:var(--shell-plaque-label); font-size:8px; margin-bottom:2px; }
104
+ /* bet sits in the middle: − value + (compact), fixed so the numbers don't shove balance/win */
105
+ #${SHELL_ROOT_ID}.ge-mobile .ge-m-betgroup { flex:0 0 auto; display:flex; align-items:center; gap:6px; }
106
+ #${SHELL_ROOT_ID}.ge-mobile .ge-m-betgroup .ge-iconbtn { width:26px; height:26px; color:#fff; font-size:18px; }
107
+ #${SHELL_ROOT_ID}.ge-mobile .ge-m-betgroup .ge-iconbtn:hover { color:var(--shell-accent); }
108
+ /* fixed-width box (sized for up to ~€100,000) so +/- never resizes the bet or shifts balance/win;
109
+ bigger amounts shrink the number instead (fitBet) */
110
+ #${SHELL_ROOT_ID}.ge-mobile .ge-m-betgroup .ge-bet-value { flex:0 0 auto; width:76px; font-size:11px;
111
+ text-align:center; color:#fff; text-shadow:none; }
112
+ #${SHELL_ROOT_ID}.ge-mobile .ge-m-betgroup .ge-bet-value .ge-rd-val { transform-origin:center; }
113
+ /* SPIN — same as desktop: white disc, black icon + ring; hover tints the icon accent (no fill/ring) */
114
+ #${SHELL_ROOT_ID}.ge-mobile .ge-shell-spin { width:84px; height:84px; font-size:66px; border-width:4px;
115
+ background:var(--shell-btn); color:var(--shell-btn-ink); }
116
+ #${SHELL_ROOT_ID}.ge-mobile .ge-shell-spin:hover { background:var(--shell-btn); color:var(--shell-accent); box-shadow:none; }
93
117
  #${SHELL_ROOT_ID}.ge-mobile .ge-shell-buybonus { width:50px; height:50px; font-size:9px; border-width:2px; }
94
118
  /* landscape overflow — size the column to content, centre it; JS scales the whole stack to fit */
95
119
  #${SHELL_ROOT_ID} .ge-shell-barhost.ge-fit { left:50%; right:auto; width:max-content; max-width:none; }
@@ -345,56 +369,82 @@ export const SHELL_CSS = SHELL_FONT_CSS + `
345
369
  #${SHELL_ROOT_ID} [data-ge="buybonus-overlay"] .ge-bb-betval b { font-size:clamp(9px,2.5cqh,11px); }
346
370
  #${SHELL_ROOT_ID} [data-ge="buybonus-overlay"] .ge-bb-betval span { font-size:clamp(5px,1.25cqh,6px); }
347
371
 
348
- /* ═══ base/wide plaque bar — grouped dark + glass panels (reference-style) ═══ */
349
- #${SHELL_ROOT_ID} .ge-zone-plaques { gap:0; } /* panels connect; buttons overlap */
350
- #${SHELL_ROOT_ID} .ge-pl { display:flex; align-items:center; height:56px; box-sizing:border-box;
351
- border-radius:16px; padding:0 20px; gap:18px; }
352
- #${SHELL_ROOT_ID} .ge-pl-dark { background:var(--shell-plaque-dark); }
353
- #${SHELL_ROOT_ID} .ge-pl-glass { background:var(--shell-plaque-glass); }
354
- /* FS/replay left blocks Free Spins counter (compact) + Total Win, standalone glass plaques
355
- sitting just right of the balance pill */
356
- #${SHELL_ROOT_ID} .ge-pl-fs, #${SHELL_ROOT_ID} .ge-pl-totalwin { margin-left:8px; }
357
- #${SHELL_ROOT_ID} .ge-pl-fs { padding:0 16px; }
358
- #${SHELL_ROOT_ID} .ge-pl .ge-rd { color:#fff; text-shadow:none; }
359
- #${SHELL_ROOT_ID} .ge-pl .ge-rd .ge-lbl { color:var(--shell-plaque-label); }
360
- #${SHELL_ROOT_ID} .ge-pl .ge-iconbtn { color:#fff; }
361
- /* LEFT: [menu] coin [balance] coin overlaps both; balance fixed-wide so it doesn't jiggle */
362
- #${SHELL_ROOT_ID} .ge-pl-menu { border-radius:16px 0 0 16px; padding-right:20px; }
363
- #${SHELL_ROOT_ID} .ge-pl-bal { border-radius:0 16px 16px 0; padding-left:24px; min-width:200px; }
364
- #${SHELL_ROOT_ID} .ge-zone-plaques .ge-shell-buybonus { margin:0 -16px; position:relative; z-index:3; }
365
- /* RIGHT: [bet] · |divider| · [auto · SPIN · turbo] */
366
- #${SHELL_ROOT_ID} .ge-pl-bet { border-radius:16px 0 0 16px; justify-content:space-between;
367
- width:210px; padding-right:8px; } /* fixed: bet value never reflows the panel */
368
- #${SHELL_ROOT_ID} .ge-pl-divider { align-self:center; flex:0 0 auto; width:1px; height:30px;
369
- background:var(--shell-plaque-line); }
370
- #${SHELL_ROOT_ID} .ge-spinwrap { display:flex; align-items:center; gap:10px; height:56px;
371
- border-radius:0 16px 16px 0; padding:0 8px 0 14px; }
372
- #${SHELL_ROOT_ID} .ge-spinwrap .ge-iconbtn { color:#fff; }
373
- #${SHELL_ROOT_ID} .ge-spinwrap .ge-shell-spin { margin:0 -2px; position:relative; z-index:3; }
374
- /* tighter bet chevrons, parked next to autoplay */
375
- #${SHELL_ROOT_ID} .ge-pl-bet .ge-betstep { gap:0; }
376
- #${SHELL_ROOT_ID} .ge-pl-bet .ge-betstep .ge-iconbtn { height:24px; }
377
- /* accent hover highlight on every control */
378
- #${SHELL_ROOT_ID} .ge-pl .ge-iconbtn:hover, #${SHELL_ROOT_ID} .ge-spinwrap .ge-iconbtn:hover,
379
- #${SHELL_ROOT_ID} .ge-iconbtn:hover { color:var(--shell-accent); }
380
- /* turbo at rest (level 0) reads as dimmed — clearly off, but lighter than a true [disabled] control */
372
+ /* ═══ desktop control bar — one continuous dark panel; white-disc buttons; BUY BONUS outside-left ═══ */
373
+ /* the dark surface fills the row width (BUY BONUS sits outside, to its left) */
374
+ #${SHELL_ROOT_ID} .ge-bar-panel { flex:1 1 auto; min-width:0; box-sizing:border-box; display:flex;
375
+ align-items:center; justify-content:space-between; gap:10px; height:68px; padding:0 14px;
376
+ border-radius:12px; background:var(--shell-bar); }
377
+ #${SHELL_ROOT_ID} .ge-bar-panel .ge-zone-left, #${SHELL_ROOT_ID} .ge-bar-panel .ge-zone-right { gap:12px; min-width:0; }
378
+ #${SHELL_ROOT_ID} .ge-bar-panel .ge-rd { color:#fff; text-shadow:none; }
379
+ #${SHELL_ROOT_ID} .ge-bar-panel .ge-rd .ge-lbl { color:var(--shell-plaque-label); }
380
+ #${SHELL_ROOT_ID} .ge-bar-panel .ge-balance { min-width:142px; } /* fixed-wide so the digits don't reflow the row */
381
+
382
+ /* white-disc buttons: black icon + black ring; hover tints just the ICON accent (no outer ring) */
383
+ #${SHELL_ROOT_ID} .ge-bar-panel .ge-iconbtn { width:38px; height:38px; border-radius:50%; border:2px solid #000;
384
+ background:var(--shell-btn); color:var(--shell-btn-ink); font-size:19px; }
385
+ /* hover + active/engaged tint the icon AND the ring accent (menu/steppers have no border, so unaffected) */
386
+ #${SHELL_ROOT_ID} .ge-bar-panel .ge-iconbtn:hover { background:var(--shell-btn); color:var(--shell-accent); border-color:var(--shell-accent); }
387
+ #${SHELL_ROOT_ID} .ge-bar-panel .ge-iconbtn.ge-active,
388
+ #${SHELL_ROOT_ID} .ge-bar-panel .ge-iconbtn.ge-glow { color:var(--shell-accent); border-color:var(--shell-accent); }
389
+ /* autoplay glyph reads small in its disc enlarge it */
390
+ #${SHELL_ROOT_ID} .ge-bar-panel .ge-iconbtn[data-ge="autoplay"] { font-size:25px; }
391
+ /* MENU stays a plain (borderless) icon just larger; no white disc */
392
+ #${SHELL_ROOT_ID} .ge-bar-panel .ge-iconbtn[data-ge="menu"] { background:none; border:none;
393
+ width:auto; height:auto; color:#fff; font-size:30px; }
394
+ #${SHELL_ROOT_ID} .ge-bar-panel .ge-iconbtn[data-ge="menu"]:hover { background:none; color:var(--shell-accent); }
395
+ /* turbo at rest reads dimmed clearly off, but lighter than a true [disabled] control */
381
396
  #${SHELL_ROOT_ID} [data-ge="turbo"]:not(.ge-active) { opacity:.5; }
382
397
  #${SHELL_ROOT_ID} [data-ge="turbo"]:not(.ge-active):hover { opacity:1; }
383
- /* autoplay glow while running (beats the white .ge-spinwrap/.ge-pl colour rules) */
384
- #${SHELL_ROOT_ID} .ge-iconbtn.ge-glow { color:var(--shell-accent); filter:drop-shadow(0 0 6px var(--shell-accent)); }
398
+
399
+ /* BET group value + tight stacked chevrons, parked close to the divider (no extra air) */
400
+ #${SHELL_ROOT_ID} .ge-betgroup { display:flex; align-items:center; gap:8px; }
401
+ /* the value box is a FIXED width (sized to hold up to ~€100,000) so changing the stake never shifts
402
+ the steppers/divider/SPIN; bigger amounts shrink the value span instead — see fitBet(). */
403
+ #${SHELL_ROOT_ID} .ge-betgroup .ge-bet-value { flex:0 0 auto; width:90px; border-radius:0; }
404
+ /* every readout's value is an inline-block span so it can be measured & transform-scaled to fit;
405
+ numerals render in OswaldNum (the digit-only subset), text glyphs fall back to Inter */
406
+ #${SHELL_ROOT_ID} .ge-rd-val { display:inline-block; white-space:nowrap; transform-origin:left center;
407
+ font-family:'OswaldNum','Inter',sans-serif; font-size:1.15em; }
408
+ #${SHELL_ROOT_ID} .ge-spin-count { font-family:'OswaldNum','Inter',sans-serif; }
409
+ #${SHELL_ROOT_ID} .ge-betgroup .ge-betstep { gap:2px; }
410
+ /* the +/- stay plain icons (no white disc), just bolder/bigger than the default */
411
+ #${SHELL_ROOT_ID} .ge-betgroup .ge-betstep .ge-iconbtn { background:none; border:none; width:30px; height:22px;
412
+ color:#fff; font-size:22px; }
413
+ #${SHELL_ROOT_ID} .ge-betgroup .ge-betstep .ge-iconbtn:hover { background:none; color:var(--shell-accent); }
385
414
  /* tappable stake readout opens the bet picker */
386
415
  #${SHELL_ROOT_ID} .ge-betbtn { pointer-events:auto; cursor:pointer; border-radius:8px; transition:color .12s ease; }
387
416
  #${SHELL_ROOT_ID} .ge-betbtn:hover { color:var(--shell-accent); }
388
417
  #${SHELL_ROOT_ID} .ge-betbtn.ge-disabled { pointer-events:none; }
389
- /* WIN — inline between the groups, same block height/shape as the other plaques (glass tone) */
390
- #${SHELL_ROOT_ID} .ge-winpill { flex:0 0 auto; align-self:center; display:flex; align-items:center;
391
- justify-content:center; gap:8px; height:56px; box-sizing:border-box; padding:0 24px; border-radius:16px;
392
- white-space:nowrap; pointer-events:none; background:var(--shell-plaque-glass); color:#fff;
393
- font-size:16px; font-weight:800; font-variant-numeric:tabular-nums; text-shadow:none; }
394
- #${SHELL_ROOT_ID} .ge-winpill .ge-lbl { display:inline; margin:0; color:rgba(255,255,255,.7);
395
- font-size:10px; letter-spacing:.12em; }
396
- /* lifted above the bar on overflow compact pill (flex child of the host, scaled with it) */
397
- #${SHELL_ROOT_ID} .ge-winpill.ge-up { height:auto; padding:6px 16px; border-radius:999px; }
418
+
419
+ /* divider short, with air above & below (not full bar height) */
420
+ #${SHELL_ROOT_ID} .ge-pl-divider { align-self:center; flex:0 0 auto; width:1px; height:22px;
421
+ background:var(--shell-plaque-line); }
422
+
423
+ /* SPIN + auto/turbo cluster — SPIN is the hero white disc: larger than the bar so it pops above/below */
424
+ #${SHELL_ROOT_ID} .ge-spinwrap { display:flex; align-items:center; gap:8px; }
425
+ #${SHELL_ROOT_ID} .ge-bar-panel .ge-shell-spin { width:84px; height:84px; font-size:65px; border-width:4px;
426
+ background:var(--shell-btn); color:var(--shell-btn-ink); position:relative; z-index:3; }
427
+ #${SHELL_ROOT_ID} .ge-bar-panel .ge-shell-spin:hover { background:var(--shell-btn); color:var(--shell-accent); box-shadow:none; }
428
+
429
+ /* FS hero — replaces SPIN in free spins: same white/black-ring hero, but a rounded rectangle showing
430
+ the spins counter. Pops above/below the bar like SPIN (height matches the disc). */
431
+ #${SHELL_ROOT_ID} .ge-fs-hero { pointer-events:auto; box-sizing:border-box; height:84px; min-width:96px;
432
+ padding:0 18px; border:4px solid #000; border-radius:20px; background:var(--shell-btn); color:var(--shell-btn-ink);
433
+ display:flex; flex-direction:column; align-items:center; justify-content:center; gap:3px;
434
+ position:relative; z-index:3; }
435
+ /* the label wraps to ≤2 lines so long localizations ("Бесплатные вращения") fit the narrow hero */
436
+ #${SHELL_ROOT_ID} .ge-fs-hero .ge-fs-lbl { font-weight:700; font-size:9px; line-height:1.1; letter-spacing:.08em;
437
+ text-transform:uppercase; color:#454c5a; text-align:center; max-width:11ch; }
438
+ #${SHELL_ROOT_ID} .ge-fs-hero .ge-fs-num { font-family:'OswaldNum','Inter',sans-serif; font-weight:700;
439
+ font-size:30px; line-height:1; font-variant-numeric:tabular-nums; white-space:nowrap; }
440
+
441
+ /* BUY BONUS — floats outside-left of the bar; keeps its accent disc + hover ring (the one exception).
442
+ relative/z-index so it stacks predictably under the full-screen overlays (see overlay-stacking test). */
443
+ #${SHELL_ROOT_ID} .ge-shell-bottom > .ge-shell-buybonus { flex:0 0 auto; width:62px; height:62px;
444
+ font-size:11px; border-width:3px; position:relative; z-index:3; }
445
+
446
+ /* WIN — a plain readout (same as balance/bet), grouped on the left with the other info */
447
+ #${SHELL_ROOT_ID} .ge-bar-panel .ge-win { pointer-events:none; }
398
448
 
399
449
  /* centred CARD modal — opaque card on a frosted backdrop, accent title heading at the top
400
450
  (no ✕), content, full-bleed footer button(s). Shared by the buy-bonus confirm AND the
@@ -44,6 +44,11 @@ export function buildThemeVars(theme: ThemeConfig = {}): string {
44
44
  `--shell-plaque-dark: rgba(6,9,15,.86)`,
45
45
  `--shell-plaque-glass: rgba(30,36,48,.70)`,
46
46
  `--shell-plaque-glass-hover: rgba(40,48,64,.86)`,
47
+ // The desktop control bar is one continuous, slightly-darker surface (vs the lighter plaque
48
+ // panels). Buttons sitting on it are white discs with black icons (see .ge-bar-panel CSS).
49
+ `--shell-bar: rgba(6,9,15,.86)`,
50
+ `--shell-btn: #f4f6fb`,
51
+ `--shell-btn-ink: #0b0e16`,
47
52
  // Opaque surface for centred modals (confirm, bet/autoplay pickers) so they read solid,
48
53
  // not see-through, over the frosted backdrop.
49
54
  `--shell-plaque-solid: #1a2030`,
@@ -1,3 +1,3 @@
1
1
  // AUTO-GENERATED by scripts/gen-version.mjs — do not edit. Mirrors package.json "version".
2
2
  /** The @energy8platform/platform-core package version, stamped at build time. */
3
- export const PACKAGE_VERSION = '0.26.1';
3
+ export const PACKAGE_VERSION = '0.27.0';