vimp-engine 0.7.2 → 0.8.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 (73) hide show
  1. package/package.json +11 -4
  2. package/src/client/InputListener.js +36 -0
  3. package/src/client/SoundManager.js +498 -0
  4. package/src/client/boot.js +113 -0
  5. package/src/client/components/controller/Auth.js +44 -0
  6. package/src/client/components/controller/CanvasManager.js +26 -0
  7. package/src/client/components/controller/Chat.js +56 -0
  8. package/src/client/components/controller/Controls.js +51 -0
  9. package/src/client/components/controller/Game.js +37 -0
  10. package/src/client/components/controller/Lobby.js +81 -0
  11. package/src/client/components/controller/LobbyAuth.js +41 -0
  12. package/src/client/components/controller/Panel.js +23 -0
  13. package/src/client/components/controller/Stat.js +33 -0
  14. package/src/client/components/controller/Vote.js +55 -0
  15. package/src/client/components/model/Auth.js +92 -0
  16. package/src/client/components/model/CanvasManager.js +319 -0
  17. package/src/client/components/model/Chat.js +119 -0
  18. package/src/client/components/model/Controls.js +120 -0
  19. package/src/client/components/model/Game.js +172 -0
  20. package/src/client/components/model/Lobby.js +226 -0
  21. package/src/client/components/model/LobbyAuth.js +161 -0
  22. package/src/client/components/model/Panel.js +70 -0
  23. package/src/client/components/model/Stat.js +77 -0
  24. package/src/client/components/model/Vote.js +216 -0
  25. package/src/client/components/view/Auth.js +167 -0
  26. package/src/client/components/view/CanvasManager.js +44 -0
  27. package/src/client/components/view/Chat.js +90 -0
  28. package/src/client/components/view/Controls.js +30 -0
  29. package/src/client/components/view/Game.js +48 -0
  30. package/src/client/components/view/Lobby.js +303 -0
  31. package/src/client/components/view/LobbyAuth.js +111 -0
  32. package/src/client/components/view/Panel.js +314 -0
  33. package/src/client/components/view/Stat.js +238 -0
  34. package/src/client/components/view/Vote.js +108 -0
  35. package/src/client/debug.js +158 -0
  36. package/src/client/lib/autostart.js +61 -0
  37. package/src/client/lib/contextTracker.js +46 -0
  38. package/src/client/lib/formBuilder.js +280 -0
  39. package/src/client/lib/hostGate.js +16 -0
  40. package/src/client/main.js +2011 -0
  41. package/src/client/network/HostConnectionManager.js +196 -0
  42. package/src/client/network/HostController.js +422 -0
  43. package/src/client/network/InlineHostBridge.js +152 -0
  44. package/src/client/network/LoopbackTransport.js +51 -0
  45. package/src/client/network/SignalingClient.js +146 -0
  46. package/src/client/network/WebRtcManager.js +147 -0
  47. package/src/client/network/WebSocketTransport.js +78 -0
  48. package/src/client/network/policyClose.js +54 -0
  49. package/src/client/network/workerSupport.js +34 -0
  50. package/src/client/providers/BakingProvider.js +88 -0
  51. package/src/client/providers/DependencyProvider.js +41 -0
  52. package/src/client/style.css +898 -0
  53. package/src/client/views/gameShell.js +190 -0
  54. package/src/client/views/includes/auth.pug +20 -0
  55. package/src/client/views/includes/chat.pug +5 -0
  56. package/src/client/views/includes/informer.pug +2 -0
  57. package/src/client/views/includes/lobby.pug +48 -0
  58. package/src/client/views/includes/lobbyAuth.pug +19 -0
  59. package/src/client/views/includes/panel.pug +3 -0
  60. package/src/client/views/includes/stat.pug +2 -0
  61. package/src/client/views/index.pug +8 -0
  62. package/src/config/closeCodes.js +18 -0
  63. package/src/config/env.js +61 -0
  64. package/src/devtools/ScenarioRunner.js +3 -8
  65. package/src/devtools/pluginLoader.js +8 -93
  66. package/src/host/HostGame.js +41 -3
  67. package/src/host/PortMachine.js +294 -0
  68. package/src/host/host.worker.js +29 -246
  69. package/src/host/identity.js +100 -0
  70. package/src/lib/clientIp.js +39 -0
  71. package/src/lib/loadGamePackage.js +122 -0
  72. package/src/lib/offlinePlayerData.js +18 -0
  73. package/src/standalone/index.js +125 -0
@@ -0,0 +1,898 @@
1
+ @charset "UTF-8";
2
+
3
+ /* База страницы: раньше жила инлайном в packages/engine/index.html, но в
4
+ репозитории игры этого файла нет — а без правил ниже интерфейс движка
5
+ остался бы без своей типографики и раскладки. Standalone SDK импортирует
6
+ этот файл целиком, поэтому всё, что здесь есть, применяется и к
7
+ встраивающей странице. */
8
+ html,
9
+ body {
10
+ width: 100%;
11
+ height: 100%;
12
+ padding: 0;
13
+ margin: 0;
14
+ }
15
+
16
+ body {
17
+ background-color: #000;
18
+ color: #ccc;
19
+ font-family: 'Lucida Grande', Helvetica, Arial, Verdana, sans-serif;
20
+ font-size: 12px;
21
+ line-height: 16px;
22
+ }
23
+
24
+ /* Экраны движка не показываются разом: каждый показывает свой модуль
25
+ (initIdList в main.js — инлайновым display, AuthView.show, StatView.show,
26
+ информеры). Правило целится по классу-маркеру каркаса (ensureGameShell): в
27
+ прод-сборке класс стоит на body, и правило тождественно `body > *` из
28
+ инлайна index.html; в standalone SDK — на контейнере игры, поэтому первый
29
+ уровень встраивающей страницы правило не задевает. Формы `body > *` здесь
30
+ быть не должно: она гасила бы и разметку самой страницы игры. */
31
+ .vimp-shell > * {
32
+ display: none;
33
+ }
34
+
35
+ body.hide-cursor {
36
+ cursor: none;
37
+ }
38
+
39
+ * {
40
+ user-select: none;
41
+ }
42
+
43
+ #panel {
44
+ align-items: center;
45
+ text-transform: uppercase;
46
+ background-color: #fff;
47
+ text-align: center;
48
+ font-size: 10px;
49
+ line-height: 12px;
50
+ position: absolute;
51
+ top: 0;
52
+ left: 0;
53
+ right: 0;
54
+ height: 24px;
55
+ z-index: 4;
56
+ overflow: hidden;
57
+ }
58
+
59
+ #logo {
60
+ flex: 0 1 auto;
61
+ max-width: 40%;
62
+ font-size: 16px;
63
+ line-height: 24px;
64
+ height: 24px;
65
+ margin: 0;
66
+ padding: 0 8px;
67
+ white-space: nowrap;
68
+ overflow: hidden;
69
+ text-overflow: ellipsis;
70
+ text-align: center;
71
+ text-transform: none;
72
+ color: #cccccc;
73
+ -webkit-text-fill-color: #cccccc;
74
+ -webkit-background-clip: text;
75
+ background-clip: text;
76
+
77
+ /* Градиент разделен на 3 зоны по 100% ширины логотипа каждая */
78
+ background-image: linear-gradient(
79
+ to right,
80
+ #cccccc 0%,
81
+ #cccccc 33.3%,
82
+ rgb(255, 50, 50) 38%,
83
+ rgb(255, 165, 0) 44%,
84
+ rgb(255, 255, 0) 50%,
85
+ rgb(50, 205, 50) 56%,
86
+ rgb(0, 220, 220) 62%,
87
+ #cccccc 66.6%,
88
+ #cccccc 100%
89
+ );
90
+
91
+ background-repeat: no-repeat;
92
+ /* Начинаем со 100% (видна только правая серая часть) */
93
+ background-position: 100% 0%;
94
+ /* Увеличиваем ширину фона до 300% */
95
+ background-size: 300% 100%;
96
+
97
+ display: inline-block;
98
+ }
99
+
100
+ @keyframes logo-wave {
101
+ 0% {
102
+ background-position: 100% 0%;
103
+ }
104
+ 100% {
105
+ /* Сдвигаем в 0% — цветная волна полностью уходит вправо за пределы логотипа */
106
+ background-position: 0% 0%;
107
+ }
108
+ }
109
+
110
+ #logo.logo-round-start {
111
+ animation: logo-wave 2s cubic-bezier(0.25, 1, 0.5, 1) forwards;
112
+ /* Делаем текст прозрачным во время работы анимации */
113
+ -webkit-text-fill-color: transparent;
114
+ color: transparent;
115
+ }
116
+
117
+ #panel table {
118
+ flex: 1 1 auto;
119
+ min-width: 0;
120
+ height: 100%;
121
+ }
122
+
123
+ #panel table td {
124
+ border: 1px solid #555;
125
+ background-color: rgba(0, 0, 0, 0.5);
126
+ text-shadow: 0 0 2px #333;
127
+ white-space: nowrap;
128
+ overflow: hidden;
129
+ width: 25%;
130
+ display: none;
131
+ }
132
+
133
+ #panel table td.active {
134
+ background-color: #333;
135
+ }
136
+
137
+ /* bar-поля панели (type: 'bar' в схеме игры) — классы нейтральные,
138
+ без привязки к игровым DOM-id */
139
+ #panel table td div.panel-bar-wrapper {
140
+ display: flex;
141
+ width: 100%;
142
+ height: 100%;
143
+ gap: 1px;
144
+ }
145
+
146
+ #panel table td div.panel-bar-block,
147
+ #panel table td div.panel-bar-block-empty {
148
+ flex: 1;
149
+ }
150
+
151
+ #panel table td div.panel-bar-blink {
152
+ animation: pulse-effect 2s infinite ease-out;
153
+ }
154
+
155
+ @keyframes pulse-effect {
156
+ 50% {
157
+ background-color: #888;
158
+ }
159
+ }
160
+
161
+ @media (prefers-reduced-motion: reduce) {
162
+ #logo.logo-round-start {
163
+ animation: none;
164
+ -webkit-text-fill-color: #cccccc;
165
+ color: #cccccc;
166
+ }
167
+
168
+ #panel table td div.panel-bar-blink {
169
+ animation: none;
170
+ }
171
+ }
172
+
173
+ /*stat*/
174
+
175
+ #stat {
176
+ position: absolute;
177
+ top: 30px;
178
+ bottom: 40px;
179
+ left: 0;
180
+ right: 0;
181
+ z-index: 7;
182
+ margin: auto;
183
+ background: rgba(25, 25, 25, 0.9);
184
+ border-radius: 5px;
185
+ border: 3px solid #fff;
186
+ overflow: hidden;
187
+ width: 90%;
188
+ max-width: 550px;
189
+ font-size: 10px;
190
+ line-height: 12px;
191
+ color: #fff;
192
+ }
193
+
194
+ #stat .stat-head {
195
+ display: flex;
196
+ width: 92%;
197
+ margin: 3% auto 1%;
198
+ text-transform: capitalize;
199
+ border-bottom: 1px solid #fff;
200
+ font-size: 1.2em;
201
+ line-height: 1.2em;
202
+ }
203
+
204
+ #stat .stat-tables {
205
+ width: 92%;
206
+ margin: 3% auto 1%;
207
+ }
208
+
209
+ #stat table {
210
+ width: 100%;
211
+ margin: 0 auto 3%;
212
+ border-spacing: 0;
213
+ table-layout: fixed;
214
+ font-size: 1em;
215
+ line-height: 1.2em;
216
+ border-collapse: collapse;
217
+ }
218
+
219
+ #stat table td {
220
+ overflow: hidden;
221
+ white-space: nowrap;
222
+ text-overflow: ellipsis;
223
+ padding: 1%;
224
+ }
225
+
226
+ #stat .stat-head span:nth-child(1),
227
+ #stat table td:nth-child(1) {
228
+ width: 40%;
229
+ }
230
+
231
+ #stat .stat-head span:nth-child(2),
232
+ #stat table td:nth-child(2) {
233
+ text-align: center;
234
+ width: 15%;
235
+ }
236
+
237
+ #stat .stat-head span:nth-child(3),
238
+ #stat table td:nth-child(3) {
239
+ width: 15%;
240
+ text-align: right;
241
+ }
242
+
243
+ #stat .stat-head span:nth-child(4),
244
+ #stat table td:nth-child(4) {
245
+ width: 15%;
246
+ text-align: right;
247
+ }
248
+
249
+ #stat .stat-head span:nth-child(5),
250
+ #stat table td:nth-child(5) {
251
+ width: 15%;
252
+ text-align: right;
253
+ }
254
+
255
+ /*vote*/
256
+
257
+ #vote {
258
+ display: block;
259
+ position: absolute;
260
+ top: 30px;
261
+ left: 5px;
262
+ margin: auto;
263
+ padding: 5px;
264
+ z-index: 5;
265
+ width: 250px;
266
+ color: #fff;
267
+ background: rgba(3, 100, 155, 0.5);
268
+ text-shadow: 0 0 1px #000;
269
+ white-space: nowrap;
270
+ border: 3px solid #fff;
271
+ border-radius: 5px;
272
+ }
273
+
274
+ #vote .vote-title {
275
+ font-size: 14px;
276
+ line-height: 16px;
277
+ font-weight: bold;
278
+ margin: 3px 3px 0;
279
+ white-space: normal;
280
+ }
281
+
282
+ #vote .vote-list {
283
+ margin: 0;
284
+ padding: 5px 0 0 5px;
285
+ list-style-position: inside;
286
+ font-size: 12px;
287
+ line-height: 17px;
288
+ }
289
+
290
+ #vote .vote-list li {
291
+ text-overflow: ellipsis;
292
+ overflow: hidden;
293
+ }
294
+
295
+ #vote .vote-nav {
296
+ padding: 0 0 0 5px;
297
+ margin-top: 10px;
298
+ }
299
+
300
+ #vote .vote-nav p {
301
+ display: none;
302
+ margin: 0;
303
+ }
304
+
305
+ #vote .vote-nav p.active {
306
+ display: block;
307
+ }
308
+
309
+ /*chat*/
310
+
311
+ #chat {
312
+ position: absolute;
313
+ bottom: 0;
314
+ left: 0;
315
+ right: 0;
316
+ z-index: 3;
317
+ }
318
+
319
+ #chat-box {
320
+ width: 92%;
321
+ font-size: 14px;
322
+ line-height: 18px;
323
+ margin: 0 auto 40px;
324
+ }
325
+
326
+ #chat-box .line,
327
+ #chat-box .line1,
328
+ #chat-box .line2,
329
+ #chat-box .line3 {
330
+ opacity: 1;
331
+ transition: all 2s;
332
+ word-wrap: break-word;
333
+ text-shadow: 1px 1px 1px #333;
334
+ padding: 2px 5px;
335
+ }
336
+
337
+ #chat-box .line:before,
338
+ #chat-box .line1:before,
339
+ #chat-box .line2:before,
340
+ #chat-box .line3:before {
341
+ content: attr(data-name);
342
+ margin-right: 5px;
343
+ font-weight: bold;
344
+ }
345
+
346
+ #chat-box .line:before {
347
+ color: #f00;
348
+ }
349
+
350
+ #chat-box .line1:before {
351
+ color: #f56;
352
+ }
353
+
354
+ #chat-box .line2:before {
355
+ color: #7ed;
356
+ }
357
+
358
+ #chat-box .line3:before {
359
+ color: #fe5;
360
+ }
361
+
362
+ #cmd {
363
+ display: none;
364
+ position: absolute;
365
+ left: 0;
366
+ right: 0;
367
+ bottom: 0;
368
+ width: 92%;
369
+ margin: 0 auto 5px;
370
+ font-size: 17px;
371
+ line-height: 23px;
372
+ outline: none;
373
+ border: 3px solid #099;
374
+ border-radius: 5px;
375
+ opacity: 0.9;
376
+ }
377
+
378
+ /*form*/
379
+
380
+ /* карточка форм: auth/lobby/lobby-auth используют один каркас */
381
+ .card {
382
+ width: 256px;
383
+ background-color: #333;
384
+ border: 1px solid #eee;
385
+ padding: 10px;
386
+ margin: 10px auto;
387
+ text-shadow: 1px 1px 1px #000;
388
+ font-size: 12px;
389
+ line-height: 15px;
390
+ }
391
+
392
+ .card h2 {
393
+ font-size: 21px;
394
+ line-height: 30px;
395
+ margin: 0 0 10px 0;
396
+ text-align: center;
397
+ }
398
+
399
+ /* кнопки: провайдеры логина, Create server, Start, Load more, Sign out —
400
+ нативный вид input[type=button], без декоративных стилей, только размер */
401
+ .lobby-auth-provider,
402
+ #lobby-auth-nick-submit,
403
+ #lobby-host,
404
+ #lobby-user-logout,
405
+ #lobby-more,
406
+ #auth-enter {
407
+ margin: 4px;
408
+ }
409
+
410
+ /* один вертикальный ритм для всех строк формы (room-форма и auth-форма) */
411
+ .form-row {
412
+ display: flex;
413
+ align-items: center;
414
+ justify-content: space-between;
415
+ gap: 8px;
416
+ margin: 8px 0;
417
+ }
418
+
419
+ .form-row input[type='text'] {
420
+ width: 50px;
421
+ text-align: center;
422
+ }
423
+
424
+ .form-label {
425
+ color: #aaa;
426
+ }
427
+
428
+ .lobby-divider {
429
+ border: 0;
430
+ border-top: 1px solid #3d3d3d;
431
+ margin: 12px 0;
432
+ }
433
+
434
+ .lobby-section-title {
435
+ color: #ccc;
436
+ font-size: 13px;
437
+ font-weight: bold;
438
+ letter-spacing: 0.5px;
439
+ margin: 0 0 8px;
440
+ }
441
+
442
+ /*auth*/
443
+
444
+ #auth {
445
+ background-color: #222;
446
+ position: absolute;
447
+ width: 100%;
448
+ min-height: 100%;
449
+ top: 0;
450
+ left: 0;
451
+ z-index: 8;
452
+ }
453
+
454
+ #auth-error {
455
+ color: #e00;
456
+ font-size: 12px;
457
+ line-height: 15px;
458
+ margin: 0 5px;
459
+ }
460
+
461
+ #auth-error:empty {
462
+ margin: 0;
463
+ }
464
+
465
+ #auth-enter {
466
+ display: block;
467
+ margin: 4px auto;
468
+ }
469
+
470
+ #auth-link {
471
+ font-size: 9px;
472
+ height: 15px;
473
+ margin: 0;
474
+ padding: 0 5px;
475
+ text-shadow: none;
476
+ display: flex;
477
+ justify-content: space-between;
478
+ }
479
+
480
+ #auth-link p {
481
+ margin: 0;
482
+ }
483
+
484
+ #auth-link p a {
485
+ color: #ff0;
486
+ }
487
+
488
+ #auth-link p:nth-child(1) {
489
+ text-align: left;
490
+ }
491
+
492
+ #auth-link p:nth-child(2) {
493
+ text-align: right;
494
+ }
495
+
496
+ .auth-inform {
497
+ background-color: #444;
498
+ padding: 7px 13px;
499
+ margin: 10px 0;
500
+ border-radius: 4px;
501
+ border: 1px solid #999;
502
+ }
503
+
504
+ .auth-inform h4 {
505
+ margin: 0 0 4px 0;
506
+ text-align: center;
507
+ }
508
+
509
+ .auth-inform p {
510
+ margin: 0;
511
+ padding: 1px 4px;
512
+ }
513
+
514
+ .auth-inform p.last {
515
+ margin-bottom: 10px;
516
+ }
517
+
518
+ /*informers*/
519
+
520
+ #tech-informer {
521
+ position: absolute;
522
+ top: 0;
523
+ left: 0;
524
+ z-index: 9;
525
+ width: 100%;
526
+ height: 100%;
527
+ background-color: #000;
528
+ color: #fff;
529
+ font-size: 20px;
530
+ line-height: 24px;
531
+ white-space: pre-line;
532
+ }
533
+
534
+ #game-informer {
535
+ position: absolute;
536
+ top: 0;
537
+ left: 0;
538
+ z-index: 6;
539
+ padding-top: 30vh;
540
+ width: 100%;
541
+ height: calc(100% - 30vh);
542
+ color: #fff;
543
+ font-size: clamp(24px, 16px + 4vw, 48px);
544
+ font-weight: bold;
545
+ background: transparent;
546
+ text-align: center;
547
+ text-transform: uppercase;
548
+ text-shadow:
549
+ -1px -1px 0 #000,
550
+ 1px -1px 0 #000,
551
+ -1px 1px 0 #000,
552
+ 1px 1px 0 #000,
553
+ 2px 2px 4px rgba(0, 0, 0, 0.7);
554
+ opacity: 0;
555
+ animation: appearFromCenter 0.8s ease-out forwards;
556
+ }
557
+
558
+ @keyframes appearFromCenter {
559
+ from {
560
+ opacity: 0;
561
+ transform: scale(0);
562
+ }
563
+ to {
564
+ opacity: 1;
565
+ transform: scale(1);
566
+ }
567
+ }
568
+
569
+ /*lobby*/
570
+
571
+ #lobby {
572
+ background-color: #222;
573
+ position: absolute;
574
+ width: 100%;
575
+ min-height: 100%;
576
+ top: 0;
577
+ left: 0;
578
+ z-index: 8;
579
+ display: flex;
580
+ align-items: flex-start;
581
+ justify-content: center;
582
+ padding: 10px 0 0;
583
+ box-sizing: border-box;
584
+ }
585
+
586
+ #lobby-create {
587
+ display: flex;
588
+ flex-direction: column;
589
+ gap: 8px;
590
+ margin-bottom: 12px;
591
+ padding: 8px;
592
+ }
593
+
594
+ /* поле имени сервера — строка формы, а не узкий числовой инпут
595
+ (.form-row input[type='text'] рассчитан на счётчики вроде max players) */
596
+ #lobby-name {
597
+ width: 200px;
598
+ text-align: left;
599
+ }
600
+
601
+ #lobby input[type='text'],
602
+ #lobby select {
603
+ box-sizing: border-box;
604
+ }
605
+
606
+ #lobby select {
607
+ width: 200px;
608
+ padding: 1px 2px;
609
+ appearance: auto;
610
+ }
611
+
612
+ #lobby input[type='checkbox'] {
613
+ width: 18px;
614
+ height: 18px;
615
+ margin: 0;
616
+ }
617
+
618
+ #lobby-host {
619
+ margin: 16px auto 0;
620
+ }
621
+
622
+ #lobby-search {
623
+ display: block;
624
+ width: 100%;
625
+ }
626
+
627
+ #lobby-list {
628
+ list-style: none;
629
+ margin: 0;
630
+ padding: 0;
631
+ flex: 0 1 auto;
632
+ min-height: 0;
633
+ overflow-y: auto;
634
+ }
635
+
636
+ .lobby-card {
637
+ display: grid;
638
+ grid-template-columns: 1fr auto auto;
639
+ column-gap: 8px;
640
+ row-gap: 2px;
641
+ padding: 10px 12px;
642
+ margin-bottom: 6px;
643
+ background-color: #363636;
644
+ border: 1px solid #444;
645
+ cursor: pointer;
646
+ align-items: center;
647
+ }
648
+
649
+ .lobby-card:hover {
650
+ background-color: #444;
651
+ }
652
+
653
+ .lobby-card-name {
654
+ grid-column: 1 / -1;
655
+ font-weight: bold;
656
+ overflow: hidden;
657
+ text-overflow: ellipsis;
658
+ white-space: nowrap;
659
+ }
660
+
661
+ .lobby-card-info {
662
+ grid-column: 1;
663
+ color: #aaa;
664
+ font-size: 10px;
665
+ overflow: hidden;
666
+ text-overflow: ellipsis;
667
+ white-space: nowrap;
668
+ }
669
+
670
+ .lobby-card-rating {
671
+ grid-column: 2;
672
+ text-align: right;
673
+ color: #4caf50;
674
+ font-size: 10px;
675
+ }
676
+
677
+ .lobby-card-rating.is-negative {
678
+ color: #e57373;
679
+ }
680
+
681
+ .lobby-card-latency {
682
+ grid-column: 3;
683
+ text-align: right;
684
+ color: #ff0;
685
+ font-size: 10px;
686
+ font-weight: bold;
687
+ }
688
+
689
+ #lobby-empty {
690
+ text-align: center;
691
+ color: #888;
692
+ margin: 16px 0;
693
+ }
694
+
695
+ #lobby-more {
696
+ display: block;
697
+ margin: 8px auto 0;
698
+ }
699
+
700
+ #lobby-user {
701
+ display: flex;
702
+ align-items: center;
703
+ justify-content: space-between;
704
+ gap: 8px;
705
+ margin-bottom: 12px;
706
+ color: #aaa;
707
+ }
708
+
709
+ /* два столбца лобби (lobby-page-plan): настройки/создание слева, вкладки
710
+ Active Servers / Leaderboard справа; собственная ширина панелей
711
+ переопределяет фиксированный .card { width:256px } (id сильнее класса);
712
+ align-items не задан — обе .card тянутся по умолчанию (stretch) до
713
+ высоты более высокой панели */
714
+ .lobby-grid {
715
+ display: flex;
716
+ gap: 20px;
717
+ justify-content: center;
718
+ }
719
+
720
+ #lobby-setup-panel {
721
+ width: 410px;
722
+ }
723
+
724
+ /* высоту правой .card задаёт форма слева: обе панели тянутся до высоты
725
+ более высокой через stretch у .lobby-grid, а список скроллится внутри и
726
+ не влияет на высоту. Чтобы контент правой панели не раздувал строку, он
727
+ вынесен в абсолютный слой .lobby-browser-inner (его высота не участвует в
728
+ расчёте высоты строки) */
729
+ #lobby-browser-panel {
730
+ width: 430px;
731
+ position: relative;
732
+ padding: 0;
733
+ }
734
+
735
+ .lobby-browser-inner {
736
+ position: absolute;
737
+ inset: 0;
738
+ display: flex;
739
+ flex-direction: column;
740
+ padding: 10px;
741
+ box-sizing: border-box;
742
+ }
743
+
744
+ /* активная вкладка (servers/leaderboard) заполняет слой по высоте и
745
+ является flex-колонкой, внутри которой скроллится только список */
746
+ #lobby-servers-content,
747
+ #lobby-leaderboard-content {
748
+ flex: 1;
749
+ min-height: 0;
750
+ display: flex;
751
+ flex-direction: column;
752
+ }
753
+
754
+ /* фиксированные элементы вкладок не сжимаются при переполнении списка;
755
+ это же держит #lobby-my-placement сразу за списком, а не внизу карточки */
756
+ .lobby-tabs-header,
757
+ #lobby-search,
758
+ #lobby-empty,
759
+ #lobby-more,
760
+ .lobby-leaderboard-header,
761
+ #lobby-my-placement {
762
+ flex-shrink: 0;
763
+ }
764
+
765
+ @media (max-width: 800px) {
766
+ .lobby-grid {
767
+ flex-direction: column;
768
+ align-items: stretch;
769
+ gap: unset;
770
+ }
771
+
772
+ /* в столбик высоту строкой никто не задаёт — возвращаем слой в поток,
773
+ иначе absolute-слой схлопнулся бы в 0 */
774
+ .lobby-browser-inner {
775
+ position: static;
776
+ }
777
+ }
778
+
779
+ .lobby-tabs-header {
780
+ display: flex;
781
+ gap: 12px;
782
+ border-bottom: 1px solid #444;
783
+ margin-bottom: 12px;
784
+ }
785
+
786
+ .lobby-tab-btn {
787
+ background: none;
788
+ border: none;
789
+ color: #888;
790
+ font-weight: bold;
791
+ padding: 6px 4px 8px;
792
+ margin: 0;
793
+ text-shadow: none;
794
+ }
795
+
796
+ .lobby-tab-btn.active {
797
+ color: #fff;
798
+ border-bottom: 2px solid #fff;
799
+ }
800
+
801
+ .lobby-leaderboard-header {
802
+ display: flex;
803
+ justify-content: space-between;
804
+ color: #ccc;
805
+ font-size: 12px;
806
+ margin-bottom: 8px;
807
+ }
808
+
809
+ .lobby-leaderboard-header span:last-child {
810
+ color: #888;
811
+ }
812
+
813
+ #lobby-leaderboard-list {
814
+ list-style: none;
815
+ margin: 0;
816
+ padding: 0;
817
+ flex: 0 1 auto;
818
+ min-height: 0;
819
+ overflow-y: auto;
820
+ }
821
+
822
+ #lobby-leaderboard-list li,
823
+ #lobby-my-placement {
824
+ display: flex;
825
+ justify-content: space-between;
826
+ background-color: #363636;
827
+ border: 1px solid #444;
828
+ color: #ccc;
829
+ font-size: 12px;
830
+ padding: 6px 10px;
831
+ margin-bottom: 4px;
832
+ }
833
+
834
+ #lobby-leaderboard-list li span:last-child,
835
+ #lobby-my-placement span:last-child {
836
+ color: #aaa;
837
+ }
838
+
839
+ #lobby-my-placement {
840
+ position: relative;
841
+ margin-top: 16px;
842
+ }
843
+
844
+ /* разделитель «…» перед позицией игрока, когда он вне топа (SVG-ориентир) */
845
+ #lobby-my-placement.lobby-placement-gap::before {
846
+ content: '…';
847
+ position: absolute;
848
+ top: -14px;
849
+ left: 50%;
850
+ transform: translateX(-50%);
851
+ color: #666;
852
+ font-weight: bold;
853
+ }
854
+
855
+ #lobby-my-placement:empty {
856
+ display: none;
857
+ }
858
+
859
+ /*lobby-auth (Этап B2: логин перед лобби)*/
860
+
861
+ #lobby-auth {
862
+ background-color: #222;
863
+ position: absolute;
864
+ width: 100%;
865
+ min-height: 100%;
866
+ top: 0;
867
+ left: 0;
868
+ z-index: 8;
869
+ }
870
+
871
+ #lobby-auth-login,
872
+ #lobby-auth-nick {
873
+ text-align: center;
874
+ }
875
+
876
+ #lobby-auth-login-buttons {
877
+ display: flex;
878
+ flex-direction: column;
879
+ gap: 8px;
880
+ }
881
+
882
+ .lobby-auth-provider,
883
+ #lobby-host,
884
+ #lobby-user-logout {
885
+ align-self: center;
886
+ }
887
+
888
+ #lobby-auth-nick-input {
889
+ display: block;
890
+ width: 100%;
891
+ margin-bottom: 10px;
892
+ }
893
+
894
+ #lobby-auth-login-error,
895
+ #lobby-auth-nick-error {
896
+ color: #f56;
897
+ margin: 8px 0;
898
+ }