anvilsign in

collin/mahjong

1@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;600;900&display=swap');
2
3:root {
4 /* Depth of each player's strip. `--h-len` is floored so a window too narrow
5 to hold two strips shrinks the tiles rather than going negative.
6
7 Shallower than it looks like it should be, on purpose: discards live in the
8 middle of the table now rather than in a row inside each strip, and the
9 depth that freed up was given to the centre instead of to the tiles. The
10 wall square and the pile inside it are what wanted the room. */
11 --depth: clamp(226px, 30vh, 307px);
12 --h-len: max(160px, calc(100vw - 2 * var(--depth)));
13 --v-len: 100vh;
14 /* Height the action buttons occupy, held whether or not any are showing.
15 The strip is built from its edge inwards, so a bar that grew when 打出
16 appeared would shove the melds and discards along with it — the reservation
17 is what keeps everything above it still. Sized for the tallest button: the
18 ones that name a tile (打出 五萬, 吃 三萬 四萬) carry a third line. */
19 --bar-h: 52px;
20 /* Largest tile a strip can hold. Edge inwards it has to fit the melds (a
21 full tile, 1.375 tall, since they are the same tiles as the ones in the
22 hand), the button bar, the hand (1.71 + 6px) and the nameplate (28px), plus
23 its own padding, gaps and margins — so 3.085 tiles plus 122px.
24
25 The melds used to be drawn at 0.62 of a tile and budgeted at 0.86, which is
26 where the old 2.57 came from; `--depth` grew by the difference rather than
27 the tiles shrinking, for the same reason it shrank when the discards moved
28 out to the middle. The strip is deeper, but the felt is *bigger* than it
29 was for any seat that has not claimed anything — see geometry.ts.
30
31 It used to be 3.45 tiles: the missing 0.88 is the row of discards, which is
32 in the middle of the table now. `--depth` came down by the same amount
33 rather than the tiles growing, so a tile is the size it always was and the
34 centre is bigger.
35
36 Tile size otherwise follows the strip's *length*, which is why a window
37 with less height than F11 kept full-size tiles and left the strip's
38 contents to be cut off. */
39 --tile-fit: calc((var(--depth) - 122px) / 3.085);
40
41 /* One size for every tile on the table — in a hand, in the wall, and lying in
42 the middle. They are the same tiles, so drawing them at different sizes was
43 always a lie, and a visible one: a tile that shrank as it was thrown read as
44 being dropped away from you rather than skidded across a flat table.
45
46 Taken from whichever way round is tighter, so all four hands match rather
47 than the side seats getting their own size off `--v-len`. */
48 --tile-w: min(
49 calc((var(--h-len) - 40px) / 18),
50 calc((var(--v-len) - 40px) / 18),
51 var(--tile-fit),
52 72px
53 );
54
55 --felt: #0f4534;
56 --felt-2: #0a3126;
57 --ivory: #f6f0e2;
58 --ivory-edge: #d9cfb6;
59 --ink: #21262b;
60 --gold: #d9b45c;
61 --red: #b3261e;
62 --green: #1c7a4b;
63 --blue: #1d5aa8;
64}
65
66* {
67 box-sizing: border-box;
68 -webkit-tap-highlight-color: transparent;
69}
70
71html,
72body,
73#root {
74 height: 100%;
75 margin: 0;
76 overflow: hidden;
77 overscroll-behavior: none;
78 touch-action: manipulation;
79 user-select: none;
80}
81
82/* Phone browsers hand out a `100%`/`100vh` viewport that includes the address
83 bar they are about to slide away over the top of it; `dvh` is the height the
84 table can actually have. */
85@supports (height: 100dvh) {
86 html,
87 body,
88 #root {
89 height: 100dvh;
90 }
91}
92
93body {
94 font-family: 'Noto Sans TC', system-ui, sans-serif;
95 color: var(--ivory);
96 background: var(--felt-2);
97}
98
99/* ---------- lobby ---------- */
100.lobby {
101 height: 100%;
102 display: flex;
103 flex-direction: column;
104 align-items: center;
105 justify-content: center;
106 gap: 0.5rem;
107 background: radial-gradient(ellipse at center, var(--felt) 0%, var(--felt-2) 75%);
108}
109.lobby h1 {
110 font-size: clamp(3rem, 9vw, 7rem);
111 font-weight: 900;
112 letter-spacing: 0.1em;
113 margin: 0 0 0.5rem;
114 color: var(--gold);
115}
116.lobby p {
117 margin: 0;
118 opacity: 0.75;
119 letter-spacing: 0.15em;
120}
121.lobby .hint {
122 opacity: 0.45;
123 font-size: 0.9rem;
124 margin-bottom: 1.5rem;
125}
126
127/* ---------- table ---------- */
128.table {
129 position: relative;
130 height: 100%;
131 width: 100%;
132 background: radial-gradient(ellipse at center, var(--felt) 0%, var(--felt-2) 80%);
133}
134
135.slot {
136 position: absolute;
137 overflow: hidden;
138}
139.slot-bottom {
140 bottom: 0;
141 left: var(--depth);
142 right: var(--depth);
143 height: var(--depth);
144}
145.slot-top {
146 top: 0;
147 left: var(--depth);
148 right: var(--depth);
149 height: var(--depth);
150}
151.slot-left {
152 left: 0;
153 top: 0;
154 width: var(--depth);
155 height: 100%;
156}
157.slot-right {
158 right: 0;
159 top: 0;
160 width: var(--depth);
161 height: 100%;
162}
163
164.seat {
165 position: absolute;
166 top: 50%;
167 left: 50%;
168 height: var(--depth);
169 display: flex;
170 flex-direction: column;
171 justify-content: flex-end;
172 align-items: center;
173 gap: 2px;
174 padding: 2px 8px 4px;
175 border-radius: 10px;
176 overflow: hidden;
177 transition: background 0.2s;
178}
179/* `--tile-w` is the table's, set once in :root — a seat only says how long its
180 own strip is. */
181.slot-bottom .seat,
182.slot-top .seat {
183 width: var(--h-len);
184}
185.slot-left .seat,
186.slot-right .seat {
187 width: var(--v-len);
188}
189.seat.active {
190 background: linear-gradient(to top, rgba(217, 180, 92, 0.22), transparent 70%);
191}
192.seat.claiming {
193 background: linear-gradient(to top, rgba(179, 38, 30, 0.25), transparent 70%);
194 animation: pulse 1.1s ease-in-out infinite;
195}
196@keyframes pulse {
197 50% {
198 background: linear-gradient(to top, rgba(179, 38, 30, 0.45), transparent 70%);
199 }
200}
201
202/* ---------- tiles ---------- */
203/* Faces come from a 43x2 sprite sheet; the second row is the highlighted
204 variant, which we use for the tile a player has lifted to discard. */
205.tile {
206 position: relative;
207 flex: none;
208 --tw: var(--tile-w);
209 width: var(--tw);
210 height: calc(var(--tw) * 1.375);
211 border-radius: calc(var(--tw) * 0.1);
212 transition: transform 0.12s ease, filter 0.12s ease;
213}
214/* A melded tile is the same tile it was in the hand it came out of, so it is
215 the same size. It used to be drawn at 0.62 and read as a different, smaller
216 set of tiles sitting beside the real ones. */
217.tile.t-meld {
218 --tw: var(--tile-w);
219}
220.tile.t-small {
221 --tw: calc(var(--tile-w) * 0.64);
222}
223.tile.face {
224 background-image: url('./assets/tiles.svg');
225 background-repeat: no-repeat;
226 background-size: calc(var(--tw) * 43) calc(var(--tw) * 2.75);
227 background-position: calc(var(--col) * var(--tw) * -1) 0;
228 filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.45));
229}
230.tile.back {
231 background-image: url('./assets/back.png');
232 background-size: 100% 100%;
233 filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.45));
234}
235.tile.tappable {
236 cursor: pointer;
237}
238.tile.raised {
239 transform: translateY(calc(var(--tw) * -0.28));
240 filter: drop-shadow(0 10px 12px rgba(0, 0, 0, 0.55));
241}
242/* Lifted tiles switch to the sheet's highlighted row. */
243.tile.face.raised {
244 background-position: calc(var(--col) * var(--tw) * -1) calc(var(--tw) * -1.375);
245}
246.tile.hl {
247 outline: 2px solid var(--gold);
248 outline-offset: 1px;
249 border-radius: calc(var(--tw) * 0.1);
250}
251.tile.dim {
252 opacity: 0.45;
253}
254
255/* ---------- seat sections ---------- */
256.hand {
257 display: flex;
258 gap: 2px;
259 /* the extra top room is where the 摸 tag sits */
260 height: calc(var(--tile-w) * 1.71 + 6px);
261 padding-top: calc(var(--tile-w) * 0.34);
262 align-items: flex-end;
263 flex: none;
264}
265/* Somebody has thrown a tile hard enough to come off the pool and into this
266 row. It takes the knock and rocks back — the sound is `barge` in game/sound,
267 and what decides it happened is the physics, in ui/Pool. Inside the seat's
268 own rotation, so it is always knocked *away from* the middle of the table. */
269.hand.shoved {
270 animation: shoved 0.34s ease-out;
271}
272@keyframes shoved {
273 0% {
274 transform: none;
275 }
276 22% {
277 transform: translateY(5px) rotate(0.5deg);
278 }
279 55% {
280 transform: translateY(-2px) rotate(-0.3deg);
281 }
282 80% {
283 transform: translateY(1px);
284 }
285 100% {
286 transform: none;
287 }
288}
289/* Every hand tile sits in a draggable cell. */
290.hand-cell {
291 position: relative;
292 display: flex;
293 align-items: flex-end;
294 touch-action: none;
295 cursor: grab;
296}
297.hand-cell.dragging {
298 z-index: 5;
299 cursor: grabbing;
300}
301.hand-cell.dragging .tile {
302 transform: translateY(calc(var(--tile-w) * -0.2)) scale(1.06);
303 filter: drop-shadow(0 14px 18px rgba(0, 0, 0, 0.65));
304}
305/* Lifted out of the row and being carried: the canvas is drawing it now, so the
306 element steps aside. Its slot stays, so the row does not shuffle around under
307 the tile you are holding and you can still drop it back where it came from. */
308.hand-cell.carrying .tile {
309 visibility: hidden;
310}
311.drawn-slot {
312 margin-left: calc(var(--tile-w) * 0.45);
313}
314.drawn-slot .tile {
315 outline: 2px solid var(--gold);
316 outline-offset: 1px;
317 border-radius: calc(var(--tw) * 0.1);
318}
319.drawn-tag {
320 position: absolute;
321 top: calc(var(--tile-w) * -0.3);
322 left: 0;
323 right: 0;
324 text-align: center;
325 font-size: calc(var(--tile-w) * 0.24);
326 font-weight: 600;
327 letter-spacing: 0.1em;
328 color: var(--gold);
329 white-space: nowrap;
330 pointer-events: none;
331}
332
333/* No reservation. The strip is packed towards its outer edge and this is the
334 innermost thing in it, so a meld appearing grows inwards and moves nothing —
335 the height was only ever holding open a band of empty green between the seat
336 and the middle, which table/geometry.ts then had to hand back to the felt.
337 Room for one is still budgeted in `--tile-fit`, so a claim cannot overflow
338 the strip. */
339.melds {
340 display: flex;
341 gap: 10px;
342 flex-wrap: wrap;
343 justify-content: center;
344 flex: none;
345}
346.meld {
347 display: flex;
348 gap: 1px;
349}
350.meld.flowers {
351 margin-left: 12px;
352 padding-left: 12px;
353 border-left: 1px dashed rgba(246, 240, 226, 0.3);
354}
355/* Only the phone layout has these now — at full size a discard is out in the
356 middle with the others. Kept as the shared base the `.compact` rules below
357 build on, not as something the four strips render. */
358.discards {
359 display: flex;
360 flex-wrap: wrap;
361 gap: 2px;
362 justify-content: center;
363 align-content: flex-start;
364 max-width: 94%;
365 min-height: 0;
366 margin-bottom: 2px;
367 flex: 1 1 0;
368 overflow-y: auto;
369 scrollbar-width: none;
370}
371
372.nameplate {
373 display: flex;
374 align-items: center;
375 gap: 8px;
376 font-size: 13px;
377 letter-spacing: 0.08em;
378 opacity: 0.9;
379 padding-top: 2px;
380 flex: none;
381}
382.nameplate .wind {
383 display: flex;
384 flex-direction: column;
385 align-items: center;
386 line-height: 1;
387 font-size: 18px;
388 font-weight: 900;
389 color: var(--gold);
390}
391.nameplate .wind em {
392 font-style: normal;
393 font-size: 8px;
394 font-weight: 600;
395 letter-spacing: 0.1em;
396 text-transform: uppercase;
397 opacity: 0.6;
398}
399.nameplate .wind.dealer {
400 color: #ff7a68;
401}
402.nameplate .tag {
403 font-size: 11px;
404 padding: 1px 6px;
405 border-radius: 999px;
406 background: rgba(217, 180, 92, 0.2);
407 border: 1px solid rgba(217, 180, 92, 0.5);
408}
409/* Why a tile lobs over the square instead of sliding into it. */
410.nameplate .tag.walled {
411 background: rgba(246, 240, 226, 0.08);
412 border-color: rgba(246, 240, 226, 0.28);
413 opacity: 0.75;
414}
415.nameplate .tag.ting {
416 background: rgba(46, 170, 120, 0.2);
417 border-color: rgba(46, 170, 120, 0.6);
418}
419.nameplate .tag.sacred {
420 background: rgba(179, 38, 30, 0.22);
421 border-color: rgba(224, 108, 96, 0.6);
422 color: #ffc9c2;
423}
424.nameplate .chips {
425 margin-left: 4px;
426 font-weight: 900;
427 font-size: 16px;
428 color: var(--gold);
429}
430
431/* The tiles and the buttons that act on them, on one row. The hand stays in the
432 middle of the strip where it has always been, and the bar is taken out of the
433 flow and hung on the right-hand end — so a button arriving moves nothing at
434 all, neither the tiles under it nor the sets and flowers above. */
435.hand-row {
436 position: relative;
437 display: flex;
438 justify-content: center;
439 align-items: flex-end;
440 align-self: stretch;
441 flex: none;
442}
443.actionbar {
444 position: absolute;
445 right: 0;
446 bottom: 0;
447 height: 100%;
448 display: flex;
449 gap: 8px;
450 align-items: center;
451 justify-content: flex-end;
452 padding-right: calc(var(--tile-w) * 0.4);
453 flex-wrap: wrap;
454 flex: none;
455}
456/* A phone has no room beside a hand. There the wrapper steps out of the way
457 entirely and the bar is a row of its own again, where the grid and the
458 `order`s further down put it. */
459.compact .hand-row {
460 display: contents;
461}
462.compact .actionbar {
463 position: static;
464 height: auto;
465 min-height: var(--bar-h);
466 align-self: stretch;
467}
468/* Every button on the bar is the same height, so the row is the same height
469 whichever ones are up: without this a 打出 五萬 stands taller than a bare 過
470 and the strip shuffles itself every time you lift a tile. */
471.actionbar .btn {
472 height: var(--bar-h);
473 min-height: 0;
474 padding-top: 4px;
475 padding-bottom: 4px;
476}
477.waiting {
478 font-size: 12px;
479 opacity: 0.5;
480}
481.delta {
482 font-size: 20px;
483 font-weight: 900;
484}
485.delta.up {
486 color: #5fd39a;
487}
488.delta.down {
489 color: #ff8a80;
490}
491
492/* ---------- buttons ---------- */
493.btn {
494 font-family: inherit;
495 font-size: 17px;
496 font-weight: 400;
497 color: #1a1a1a;
498 background: linear-gradient(180deg, #f3e6c4, #d9b45c);
499 border: none;
500 border-radius: 8px;
501 padding: 6px 15px;
502 min-height: 46px;
503 box-shadow: 0 2px 0 #9a7d38;
504 cursor: pointer;
505 display: inline-flex;
506 align-items: center;
507 gap: 6px;
508}
509.btn:active {
510 transform: translateY(2px);
511 box-shadow: none;
512}
513.btn-sub {
514 font-size: 12px;
515 opacity: 0.7;
516}
517
518/* Bilingual label: the called term above, English cue below. */
519.bi {
520 display: flex;
521 flex-direction: column;
522 align-items: center;
523 justify-content: center;
524 line-height: 1.05;
525 gap: 1px;
526}
527.bi .zh {
528 font-size: 19px;
529 font-weight: 400;
530 letter-spacing: 0.06em;
531}
532.bi .en {
533 font-size: 9.5px;
534 font-weight: 600;
535 letter-spacing: 0.11em;
536 text-transform: uppercase;
537 opacity: 0.68;
538 white-space: nowrap;
539}
540.bi .bi-sub {
541 font-size: 11px;
542 opacity: 0.72;
543 white-space: nowrap;
544}
545.big-btn .bi .zh {
546 font-size: 24px;
547}
548.big-btn .bi .en {
549 font-size: 11px;
550}
551.btn.claim.hu {
552 background: linear-gradient(180deg, #ff8a7a, #c0392b);
553 color: #fff;
554 box-shadow: 0 2px 0 #7c231a;
555}
556.btn.claim.kong,
557.btn.claim.pung {
558 background: linear-gradient(180deg, #8fd3ff, #2b7ec0);
559 color: #fff;
560 box-shadow: 0 2px 0 #1c527d;
561}
562.btn.claim.chow {
563 background: linear-gradient(180deg, #9be3b7, #2e9e63);
564 color: #fff;
565 box-shadow: 0 2px 0 #1d6640;
566}
567.btn.sort {
568 min-height: 20px;
569 padding: 1px 8px;
570 font-size: 11px;
571 letter-spacing: 0.06em;
572 border-radius: 999px;
573}
574.btn.proceed {
575 background: linear-gradient(180deg, #f3e6c4, #d9b45c);
576}
577.btn.pass,
578.btn.ghost {
579 background: transparent;
580 color: rgba(246, 240, 226, 0.75);
581 border: 1px solid rgba(246, 240, 226, 0.35);
582 box-shadow: none;
583}
584.btn.discard {
585 background: linear-gradient(180deg, #fff2cf, #e0c070);
586}
587.big-btn {
588 font-size: 22px;
589 padding: 12px 34px;
590 min-height: 56px;
591}
592
593/* ---------- the tiles in the middle ---------- */
594/* One canvas over the whole table, drawn by ui/Pool.tsx. It has to span the
595 table and not just the centre: the strips clip their own contents, so a tile
596 thrown out of a hand would be cut off at the edge of its seat if it were an
597 element. On the canvas there is nothing to clip it.
598
599 Layering: above .wall-ring (0), since the tiles land inside the square and in
600 front of it; level with .facing, which sits later in the DOM and so keeps the
601 centre readout on top; below the panels, which raise themselves. */
602.tile-pool {
603 position: absolute;
604 inset: 0;
605 width: 100%;
606 height: 100%;
607 pointer-events: none;
608 z-index: 1;
609}
610/* The pile is handled directly: a discard is picked up, carried over the top of
611 the others and set down or flicked away. The tiles are on a canvas with no
612 elements to hit, so the hit test is against the physics and the listener is a
613 capturing one on the table itself — see `dragging` in ui/Pool.tsx. Nothing
614 here needs a box; `touch-action` is what stops a drag scrolling the page. */
615.table {
616 touch-action: none;
617}
618
619/* ---------- center ---------- */
620.center {
621 position: absolute;
622 inset: var(--depth);
623 display: flex;
624 flex-direction: column;
625 align-items: center;
626 justify-content: center;
627 gap: 10px;
628 pointer-events: none;
629}
630.center > * {
631 pointer-events: auto;
632}
633/* Everything in the centre reads on top of the wall square. */
634.info,
635.last-discard,
636.prompt,
637.result,
638.info {
639 display: flex;
640 flex-direction: column;
641 align-items: center;
642 gap: 2px;
643 opacity: 0.9;
644 text-align: center;
645}
646.info .round,
647.info .wall {
648 display: flex;
649 flex-direction: column;
650 align-items: center;
651 line-height: 1.05;
652}
653.info .wall {
654 margin-top: 10px;
655 padding-top: 10px;
656 border-top: 1px solid rgba(246, 240, 226, 0.14);
657}
658.info .big {
659 font-size: 40px;
660 font-weight: 400;
661 color: var(--gold);
662}
663.info .lbl {
664 font-size: 11px;
665 letter-spacing: 0.06em;
666 opacity: 0.6;
667}
668.info .hand-no {
669 margin-top: 8px;
670 font-size: 11px;
671 opacity: 0.5;
672 letter-spacing: 0.1em;
673}
674/* The wall, built the way it is built on a table: four walls laid out like a #
675 rather than mitred into a closed square, each one carrying on past the corner
676 of the next so no corner is a joint anybody had to cut.
677
678 Nothing here is turned. A square wants no rotation to describe it: each wall
679 is pinned to its own edge of the opening, the two flat ones run their stacks
680 across and the two upright ones run them down, and that is the whole
681 difference between them. How big the square comes out is `wallSquare` in
682 `game/wall.ts`, measured and applied by `ui/WallRing`; the size below is only
683 of one stack.
684
685 `.wall-ring` is the opening, not the outside: the walls hang off its edges,
686 so the square encloses the pool the way it does on a table. */
687.wall-ring {
688 /* A stack's short side. Solved in ui/WallRing off the tight way across the
689 *table* — its height — so that eighteen stacks fit it, rather than off the
690 room the four strips leave in the middle, which a real wall never fitted
691 and which had the tile down to under half a hand's. The fallback here is
692 only for the frame before the first measure. */
693 --ws: var(--tile-w);
694 --wd: calc(var(--ws) * 1.375);
695 /* A wall, end to end: eighteen stacks, set from ui/WallRing. It is longer
696 than the edge of the opening by one wall-depth, and that overhang is what
697 covers the corner. */
698 --wall-len: 0px;
699 position: absolute;
700 top: 50%;
701 left: 50%;
702 width: 0;
703 height: 0;
704 transform: translate(-50%, -50%);
705 pointer-events: none;
706 z-index: 0;
707}
708/* Never drawn. It is here to be measured: how big a stack came out is CSS's
709 answer, and the layout needs it in stacks before it has placed any. */
710.wall-probe {
711 position: absolute;
712 visibility: hidden;
713 width: var(--ws);
714 height: var(--wd);
715}
716/* Each wall sits outside its own edge of the opening, flush with the corner it
717 is built from and running one wall-depth past the far one — the same way
718 round each time, which is what makes four straight walls a pinwheel. Every
719 corner of the square is covered by exactly one of them, and no two overlap.
720
721 Every place in the wall is here whether or not there is still a stack on it —
722 see `ringLayout` — so the row never closes up and the gap stays where the
723 drawing actually got to. */
724.wall-side {
725 position: absolute;
726 display: flex;
727 /* Where it has been pushed to, in the table's own axes — a square wall goes
728 where the finger goes. */
729 transform: translate(var(--dx, 0px), var(--dy, 0px));
730 /* The ring lets everything through; a wall is the one thing you can take
731 hold of. */
732 pointer-events: auto;
733 cursor: grab;
734}
735.wall-side.top,
736.wall-side.bottom {
737 width: var(--wall-len);
738 height: var(--wd);
739}
740.wall-side.left,
741.wall-side.right {
742 width: var(--wd);
743 height: var(--wall-len);
744}
745/* Built from the top left corner, running right over the head of the right. */
746.wall-side.top {
747 left: 0;
748 bottom: 100%;
749}
750/* From the top right, running down. */
751.wall-side.right {
752 left: 100%;
753 top: 0;
754 flex-direction: column;
755}
756/* From the bottom right, running left. */
757.wall-side.bottom {
758 right: 0;
759 top: 100%;
760 flex-direction: row-reverse;
761}
762/* From the bottom left, running up. */
763.wall-side.left {
764 right: 100%;
765 bottom: 0;
766 flex-direction: column-reverse;
767}
768.wall-stack {
769 position: relative;
770 flex: none;
771 width: var(--ws);
772 height: var(--wd);
773}
774/* Lying the other way about, which is how the two upright walls are built. */
775.wall-side.left > .wall-stack,
776.wall-side.right > .wall-stack {
777 width: var(--wd);
778 height: var(--ws);
779}
780/* The tile itself fills its stack, whichever way round the stack is lying — so
781 the two upright walls need nothing said about them here. */
782.wall-stack i {
783 position: absolute;
784 inset: 0;
785 background-image: url('./assets/back.png');
786 background-size: 100% 100%;
787 /* A box-shadow, not a drop-shadow. The square is seventy-two stacks now
788 rather than the twenty-odd that were left after a deal: `filter` gives each
789 its own layer to rasterise, and seventy-two of those being redrawn while a
790 wall is pushed about is enough to make the page stop answering. The tile is
791 a rectangle, so the cheap shadow is the same picture. */
792 box-shadow: 0 1px 1px rgba(0, 0, 0, 0.45);
793}
794/* One tile left in the stack. */
795.wall-stack.s1 i {
796 opacity: 0.8;
797 transform: scale(0.86);
798}
799.wall-stack.next::after {
800 content: '';
801 position: absolute;
802 inset: -2px;
803 border: 2px solid var(--gold);
804 border-radius: 3px;
805 box-shadow: 0 0 12px rgba(217, 180, 92, 0.7);
806}
807
808/* Text only, and it sits over the pool — so it must not swallow a finger that
809 is reaching in to wash the tiles about. */
810.facing {
811 pointer-events: none;
812 position: relative;
813 z-index: 1;
814 display: flex;
815 flex-direction: column;
816 align-items: center;
817 gap: 8px;
818 transition: transform 0.35s ease;
819}
820
821.last-discard {
822 display: flex;
823 flex-direction: column;
824 align-items: center;
825 gap: 6px;
826 --tile-w: 64px;
827}
828.ld-label {
829 font-size: 12px;
830 opacity: 0.6;
831}
832.prompt {
833 font-size: 15px;
834 letter-spacing: 0.1em;
835 opacity: 0.8;
836}
837.prompt.warn {
838 color: #ffb4a2;
839 font-weight: 600;
840}
841.result {
842 /* Over the pile in the middle, not under it. */
843 z-index: 2;
844 background: rgba(10, 30, 24, 0.94);
845 border: 1px solid rgba(217, 180, 92, 0.45);
846 border-radius: 14px;
847 padding: 18px 26px;
848 display: flex;
849 flex-direction: column;
850 align-items: center;
851 gap: 10px;
852 max-height: 90%;
853 overflow: auto;
854}
855.result h2 {
856 margin: 0;
857 font-size: 24px;
858 color: var(--gold);
859}
860.result .wt {
861 color: var(--ivory);
862 opacity: 0.8;
863}
864.tai-list {
865 display: grid;
866 grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
867 gap: 2px 18px;
868 font-size: 14px;
869 width: 100%;
870}
871.tai-item {
872 display: flex;
873 align-items: baseline;
874 justify-content: space-between;
875 gap: 12px;
876 padding: 2px 0;
877 border-bottom: 1px dotted rgba(246, 240, 226, 0.2);
878}
879.tai-name {
880 display: flex;
881 flex-direction: column;
882 line-height: 1.15;
883}
884.tai-name em {
885 font-style: normal;
886 font-size: 10px;
887 letter-spacing: 0.08em;
888 text-transform: uppercase;
889 opacity: 0.55;
890}
891.tai-val {
892 font-weight: 700;
893 color: var(--gold);
894}
895/* 胡牌 — the arrow that lands on the winner's edge of the table. Sits over the
896 strip rather than in it, so the hand underneath keeps the height it had and
897 nothing shifts when it appears. */
898.win-arrow {
899 position: absolute;
900 top: 0;
901 left: 50%;
902 font-size: 52px;
903 line-height: 0.9;
904 color: var(--gold);
905 text-shadow: 0 0 6px #0a3126, 0 0 20px rgba(217, 180, 92, 0.9);
906 pointer-events: none;
907 z-index: 4;
908 animation: arrow-nudge 0.9s ease-in-out infinite;
909}
910@keyframes arrow-nudge {
911 0%,
912 100% {
913 transform: translate(-50%, 0);
914 }
915 50% {
916 transform: translate(-50%, 9px);
917 }
918}
919
920/* 流局 — who was there and what they were sitting on. */
921.ready-list {
922 display: flex;
923 flex-direction: column;
924 gap: 6px;
925 margin-top: 2px;
926}
927.ready-row {
928 display: flex;
929 align-items: center;
930 gap: 8px;
931 font-size: 14px;
932}
933.ready-row.none {
934 opacity: 0.45;
935}
936.ready-name {
937 min-width: 6.5em;
938 text-align: right;
939}
940.ready-row .tag.ting {
941 font-size: 11px;
942 padding: 1px 6px;
943 border-radius: 999px;
944 background: rgba(46, 170, 120, 0.2);
945 border: 1px solid rgba(46, 170, 120, 0.6);
946 white-space: nowrap;
947}
948.ready-tiles {
949 display: flex;
950 flex-wrap: wrap;
951 gap: 2px;
952 /* Tiles size themselves off their strip's --tile-w, and the centre is not in
953 one — without a size of its own here they come out 0 x 0 and vanish. */
954 --tile-w: 46px;
955}
956.ready-no {
957 font-size: 12px;
958 letter-spacing: 0.04em;
959}
960
961.deltas {
962 display: flex;
963 gap: 16px;
964 font-size: 14px;
965 opacity: 0.9;
966}
967.deltas .up {
968 color: #5fd39a;
969}
970.deltas .down {
971 color: #ff8a80;
972}
973
974/* ---------- help panel ---------- */
975.helpbtn.gear {
976 font-size: 13px;
977}
978.helpbtn {
979 margin-left: 2px;
980 width: 22px;
981 height: 22px;
982 border-radius: 50%;
983 border: 1px solid rgba(246, 240, 226, 0.35);
984 background: transparent;
985 color: rgba(246, 240, 226, 0.75);
986 font-family: inherit;
987 font-size: 12px;
988 line-height: 1;
989 cursor: pointer;
990}
991/* The rules panel gets its own deeper slot so it is comfortable to read. */
992.help-slot {
993 position: absolute;
994 inset: 0;
995 z-index: 30;
996 background: rgba(4, 18, 14, 0.55);
997}
998.help-slot.slot-bottom,
999.help-slot.slot-top,
1000.help-slot.slot-left,
1001.help-slot.slot-right {
1002 inset: 0;
1003 width: auto;
1004 height: auto;
1005}
1006.help {
1007 position: absolute;
1008 top: 50%;
1009 left: 50%;
1010 height: var(--hd);
1011 z-index: 31;
1012 display: flex;
1013 flex-direction: column;
1014 background: rgba(8, 32, 25, 0.985);
1015 border: 1px solid rgba(217, 180, 92, 0.4);
1016 border-radius: 12px;
1017 padding: 10px 18px 14px;
1018 box-shadow: 0 18px 50px rgba(0, 0, 0, 0.6);
1019}
1020/* Anchored to the asking player's own edge, and rotated to face them. The
1021 leading translate is applied last, so it shifts the box in screen space. */
1022.help-slot.slot-bottom .help,
1023.help-slot.slot-top .help {
1024 width: calc(100vw - 60px);
1025 --hd: min(52vh, 560px);
1026 --off: calc(50vh - var(--hd) / 2 - 14px);
1027}
1028.help-slot.slot-left .help,
1029.help-slot.slot-right .help {
1030 width: calc(100vh - 60px);
1031 --hd: min(52vw, 560px);
1032 --off: calc(50vw - var(--hd) / 2 - 14px);
1033}
1034.help-slot.slot-bottom .help {
1035 translate: 0 var(--off);
1036}
1037.help-slot.slot-top .help {
1038 translate: 0 calc(var(--off) * -1);
1039}
1040.help-slot.slot-left .help {
1041 translate: calc(var(--off) * -1) 0;
1042}
1043.help-slot.slot-right .help {
1044 translate: var(--off) 0;
1045}
1046.help-head {
1047 display: flex;
1048 align-items: center;
1049 justify-content: space-between;
1050 gap: 16px;
1051 flex: none;
1052}
1053.help-head h3 {
1054 margin: 0;
1055 font-size: 15px;
1056 font-weight: 400;
1057 letter-spacing: 0.08em;
1058 color: var(--gold);
1059}
1060.help-head .btn {
1061 min-height: 30px;
1062 padding: 2px 12px;
1063 font-size: 13px;
1064}
1065.help-body {
1066 flex: 1 1 0;
1067 min-height: 0;
1068 overflow-y: auto;
1069 columns: 4;
1070 column-gap: 26px;
1071 column-fill: auto;
1072 padding-top: 8px;
1073 overflow-x: auto;
1074 scrollbar-width: thin;
1075}
1076.help-body section {
1077 break-inside: avoid;
1078 margin-bottom: 10px;
1079}
1080.help-body h4 {
1081 margin: 0 0 2px;
1082 font-size: 13px;
1083 font-weight: 400;
1084 color: var(--gold);
1085 letter-spacing: 0.06em;
1086}
1087.help-body h4 em {
1088 font-style: normal;
1089 font-size: 9px;
1090 letter-spacing: 0.12em;
1091 text-transform: uppercase;
1092 opacity: 0.55;
1093 margin-left: 4px;
1094}
1095.help-body p {
1096 margin: 0 0 4px;
1097 display: flex;
1098 flex-direction: column;
1099 line-height: 1.35;
1100 font-size: 11.5px;
1101}
1102.help-body p em {
1103 font-style: normal;
1104 font-size: 10.5px;
1105 opacity: 0.6;
1106}
1107.help-body .credit {
1108 opacity: 0.6;
1109}
1110
1111/* ---------- lobby resume ---------- */
1112.lobby .sub {
1113 font-size: 0.95rem;
1114}
1115.resume {
1116 margin-top: 1.4rem;
1117 padding: 12px 26px;
1118 border: 1px solid rgba(217, 180, 92, 0.35);
1119 border-radius: 12px;
1120 background: rgba(0, 0, 0, 0.2);
1121 text-align: center;
1122 display: flex;
1123 flex-direction: column;
1124 gap: 6px;
1125}
1126.resume-line {
1127 font-size: 14px;
1128 color: var(--gold);
1129 letter-spacing: 0.06em;
1130}
1131.resume-scores {
1132 display: flex;
1133 gap: 18px;
1134 justify-content: center;
1135 font-size: 13px;
1136 opacity: 0.8;
1137}
1138.resume-scores b {
1139 color: var(--gold);
1140 font-weight: 600;
1141}
1142.resume-when {
1143 font-size: 11px;
1144 opacity: 0.45;
1145 letter-spacing: 0.08em;
1146}
1147.lobby-buttons {
1148 display: flex;
1149 flex-direction: column;
1150 gap: 12px;
1151 margin-top: 1.6rem;
1152 align-items: stretch;
1153}
1154/* The two ways to play carry a line of explanation under the name. */
1155.lobby-buttons .big-btn {
1156 display: flex;
1157 flex-direction: column;
1158 gap: 3px;
1159 min-width: 360px;
1160 padding: 12px 28px;
1161}
1162.lobby-buttons .big-btn em {
1163 font-style: normal;
1164 font-size: 12px;
1165 font-weight: 400;
1166 letter-spacing: 0.03em;
1167 opacity: 0.72;
1168}
1169
1170/* ---------- sound ---------- */
1171.lobby-extras {
1172 margin-top: 1.4rem;
1173 display: flex;
1174 gap: 12px;
1175 align-items: center;
1176}
1177/* Small rounded button for the things that sit under the main choices. */
1178.btn.pill {
1179 min-height: 34px;
1180 padding: 2px 14px;
1181 font-size: 13px;
1182 letter-spacing: 0.06em;
1183 border-radius: 999px;
1184 gap: 8px;
1185}
1186.sound-toggle.on {
1187 border-color: rgba(217, 180, 92, 0.7);
1188 color: var(--gold);
1189}
1190.sound-toggle.compact {
1191 min-height: 28px;
1192 padding: 2px 9px;
1193 font-size: 15px;
1194}
1195.sound-toggle .spk {
1196 font-size: 15px;
1197 line-height: 1;
1198}
1199
1200/* ---------- language ---------- */
1201/* Sized to stand beside the sound and fullscreen pills in both their forms. */
1202.lang-toggle {
1203 min-height: 34px;
1204 padding: 2px 14px;
1205}
1206.lang-toggle.compact {
1207 min-height: 28px;
1208 padding: 2px 9px;
1209}
1210/* A boxed glyph, so 雙 / 中 / A all take the same room and the row does not
1211 shuffle sideways as the mode steps round. */
1212.lang-toggle .lang-icon {
1213 display: inline-grid;
1214 place-items: center;
1215 width: 18px;
1216 height: 18px;
1217 flex: none;
1218 border: 1px solid rgba(230, 226, 214, 0.45);
1219 border-radius: 4px;
1220 font-size: 12px;
1221 line-height: 1;
1222}
1223
1224/* ---------- shared table controls ---------- */
1225/* Bottom-right of the centre square, opposite the log. Unrotated on purpose:
1226 it belongs to the table, not to a seat. */
1227/* Out of the way of a throw. The undo button is a wide target lying on the
1228 playing surface at the bottom-right of the middle, which is exactly where a
1229 tile leaves seat 0's hand — see the `carrying` class in ui/Pool.tsx. */
1230.table.carrying .table-controls {
1231 pointer-events: none;
1232}
1233.table-controls {
1234 position: absolute;
1235 /* Over the pile, same as .result. */
1236 z-index: 2;
1237 right: 10px;
1238 bottom: 8px;
1239 display: flex;
1240 align-items: center;
1241 gap: 8px;
1242}
1243.btn.undo {
1244 min-height: 34px;
1245 padding: 3px 14px;
1246 border-radius: 999px;
1247 gap: 9px;
1248 max-width: 40vh;
1249}
1250.undo-arrow {
1251 font-size: 17px;
1252 line-height: 1;
1253 color: var(--gold);
1254}
1255.undo-what {
1256 display: flex;
1257 flex-direction: column;
1258 align-items: flex-start;
1259 line-height: 1.15;
1260 min-width: 0;
1261}
1262.undo-what .zh {
1263 font-size: 13px;
1264 letter-spacing: 0.05em;
1265}
1266.undo-what .en {
1267 font-size: 9px;
1268 letter-spacing: 0.09em;
1269 text-transform: uppercase;
1270 opacity: 0.55;
1271 white-space: nowrap;
1272 overflow: hidden;
1273 text-overflow: ellipsis;
1274 max-width: 100%;
1275}
1276
1277/* ---------- result: one block per winner ---------- */
1278.result .win {
1279 display: flex;
1280 flex-direction: column;
1281 align-items: center;
1282 gap: 8px;
1283 width: 100%;
1284}
1285.result .win + .win {
1286 padding-top: 10px;
1287 border-top: 1px solid rgba(217, 180, 92, 0.3);
1288}
1289.result .liable {
1290 font-size: 12px;
1291 letter-spacing: 0.08em;
1292 color: #ffb4a2;
1293}
1294.result .multi {
1295 font-size: 12px;
1296 letter-spacing: 0.1em;
1297 color: var(--gold);
1298 opacity: 0.85;
1299}
1300
1301/* ---------- settings ---------- */
1302.over-buttons {
1303 display: flex;
1304 gap: 12px;
1305 align-items: center;
1306}
1307.settings-scrim {
1308 position: absolute;
1309 inset: 0;
1310 z-index: 40;
1311 background: rgba(4, 18, 14, 0.72);
1312 display: flex;
1313 align-items: center;
1314 justify-content: center;
1315 padding: 20px;
1316}
1317.settings {
1318 width: min(1180px, 100%);
1319 max-height: 100%;
1320 display: flex;
1321 flex-direction: column;
1322 background: rgba(8, 32, 25, 0.99);
1323 border: 1px solid rgba(217, 180, 92, 0.4);
1324 border-radius: 14px;
1325 box-shadow: 0 18px 50px rgba(0, 0, 0, 0.6);
1326 padding: 16px 28px 22px;
1327}
1328.settings-head {
1329 display: flex;
1330 align-items: center;
1331 justify-content: space-between;
1332 gap: 16px;
1333 flex: none;
1334 padding-bottom: 12px;
1335 border-bottom: 1px solid rgba(246, 240, 226, 0.12);
1336}
1337.settings-head h3 {
1338 margin: 0;
1339 font-size: 22px;
1340 font-weight: 400;
1341 letter-spacing: 0.08em;
1342 color: var(--gold);
1343}
1344.settings-head-btns {
1345 display: flex;
1346 gap: 10px;
1347}
1348.settings-head .btn {
1349 min-height: 46px;
1350 padding: 2px 22px;
1351 font-size: 17px;
1352}
1353.settings-body {
1354 /* `1 1 auto`, not `1 1 0`: the panel's own height comes from this content,
1355 so a zero basis collapses the pair of them to nothing. `max-height` on the
1356 panel is what brings the scrollbar back at the other end. */
1357 flex: 1 1 auto;
1358 min-height: 0;
1359 overflow-y: auto;
1360 scrollbar-width: thin;
1361 padding-top: 14px;
1362 /* Columns rather than a grid: grid rows would size to the tallest section
1363 and leave the shorter one padded with air, which is what pushed the last
1364 house rule off the bottom. */
1365 columns: 2;
1366 column-gap: 44px;
1367}
1368.settings-body section {
1369 break-inside: avoid;
1370 margin-bottom: 22px;
1371}
1372.settings-body h4 {
1373 margin: 0 0 10px;
1374 font-size: 16px;
1375 font-weight: 400;
1376 color: var(--gold);
1377 letter-spacing: 0.06em;
1378}
1379.settings-body h4 em {
1380 font-style: normal;
1381 font-size: 11px;
1382 letter-spacing: 0.12em;
1383 text-transform: uppercase;
1384 opacity: 0.55;
1385 margin-left: 5px;
1386}
1387.settings .field {
1388 display: flex;
1389 align-items: center;
1390 gap: 14px;
1391 padding: 5px 0;
1392}
1393.settings .f-label {
1394 display: flex;
1395 align-items: baseline;
1396 gap: 6px;
1397 font-size: 16px;
1398 /* Wide enough for 起始籌碼 STARTING CHIPS, so every input in a section
1399 starts at the same place. */
1400 min-width: 196px;
1401 opacity: 0.85;
1402}
1403/* ...except the player rows, which are labelled 1–4 and need no such room. */
1404.settings .names .f-label {
1405 min-width: 26px;
1406}
1407.settings .f-label em {
1408 font-style: normal;
1409 font-size: 11px;
1410 letter-spacing: 0.1em;
1411 text-transform: uppercase;
1412 opacity: 0.55;
1413}
1414.settings input[type='text'],
1415.settings input:not([type]),
1416.settings input[type='number'] {
1417 flex: 1 1 0;
1418 min-width: 0;
1419 font-family: inherit;
1420 font-size: 18px;
1421 color: var(--ivory);
1422 background: rgba(0, 0, 0, 0.28);
1423 border: 1px solid rgba(246, 240, 226, 0.25);
1424 border-radius: 8px;
1425 padding: 11px 14px;
1426 min-height: 48px;
1427 user-select: text;
1428}
1429.settings input:focus {
1430 outline: none;
1431 border-color: rgba(217, 180, 92, 0.8);
1432}
1433.settings input[type='number'] {
1434 max-width: 150px;
1435}
1436.settings input[type='range'] {
1437 flex: 1 1 0;
1438 height: 34px;
1439 accent-color: var(--gold);
1440}
1441.settings input:disabled {
1442 opacity: 0.4;
1443}
1444.settings .switch {
1445 display: grid;
1446 grid-template-columns: auto 1fr;
1447 gap: 3px 14px;
1448 align-items: center;
1449 padding: 9px 0;
1450 cursor: pointer;
1451}
1452.settings .switch input {
1453 width: 26px;
1454 height: 26px;
1455 accent-color: var(--gold);
1456 cursor: pointer;
1457}
1458.settings .sw-name {
1459 display: flex;
1460 align-items: baseline;
1461 gap: 8px;
1462 font-size: 17px;
1463}
1464.settings .sw-name em {
1465 font-style: normal;
1466 font-size: 11px;
1467 letter-spacing: 0.1em;
1468 text-transform: uppercase;
1469 opacity: 0.55;
1470}
1471.settings .note {
1472 grid-column: 2;
1473 font-size: 13px;
1474 line-height: 1.4;
1475 opacity: 0.55;
1476}
1477.settings .switch.dead {
1478 opacity: 0.4;
1479 cursor: default;
1480}
1481.settings .note.lock {
1482 grid-column: auto;
1483 margin: 4px 0 0;
1484 color: #ffb4a2;
1485 opacity: 0.75;
1486}
1487
1488/* ============================================================================
1489 compact — a phone
1490
1491 The table layout is four strips of --depth arranged around a centre square,
1492 which wants about 700px each way; a phone has that in neither, and in
1493 landscape the top and bottom strips simply overlap. It also assumes four
1494 people sitting around a screen lying flat, which is not what a phone is.
1495
1496 Compact throws both assumptions out. Nothing is rotated — every label reads
1497 upright for the one person holding the phone — the table becomes a grid, the
1498 three other seats shrink to cards showing what is public about them, and the
1499 depth that frees up goes to the hand, which wraps when it has to.
1500
1501 The `compact` class is put on <html> by useCompact(); the breakpoint itself
1502 lives in ui/compact.ts, so it is written down once.
1503 ============================================================================ */
1504
1505.compact .table {
1506 display: grid;
1507 gap: 2px;
1508 /* Landscape is where a notch eats into the sides. */
1509 padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom)
1510 env(safe-area-inset-left);
1511 grid-template-columns: 1fr 1fr 1fr;
1512 grid-template-rows: minmax(0, 1fr) auto auto;
1513 grid-template-areas:
1514 'left across right'
1515 'bar bar bar'
1516 'mine mine mine';
1517}
1518.compact .slot {
1519 position: relative;
1520 inset: auto;
1521 width: auto;
1522 height: auto;
1523 min-width: 0;
1524 min-height: 0;
1525}
1526/* `.slot` and not the edge class alone: the rules sheet carries an edge class
1527 of its own, and would otherwise be parked in the grid area named here. */
1528.compact .slot.slot-left {
1529 grid-area: left;
1530}
1531.compact .slot.slot-top {
1532 grid-area: across;
1533}
1534.compact .slot.slot-right {
1535 grid-area: right;
1536}
1537.compact .slot.slot-bottom {
1538 grid-area: mine;
1539}
1540.compact .center {
1541 grid-area: bar;
1542}
1543/* The strip no longer floats in the middle of a slot — it is the slot. */
1544.compact .seat {
1545 position: relative;
1546 top: auto;
1547 left: auto;
1548 width: auto;
1549 height: 100%;
1550 padding: 2px 4px 3px;
1551}
1552/* Off to one side rather than dead centre: a card is narrow enough that the
1553 arrow would otherwise land on the name it is pointing out. */
1554.compact .win-arrow {
1555 font-size: 24px;
1556 left: 14px;
1557}
1558
1559/* ---------- the other three seats ---------- */
1560/* Read top-down: who they are, how many tiles they hold, what they have laid
1561 down, what they have thrown. The hand is the least interesting of those and
1562 goes smallest; the discards get whatever room is left, since reading them is
1563 the whole of playing against somebody. */
1564.compact .opp .seat {
1565 --tile-w: clamp(13px, calc((100vw / 3 - 20px) / 8.6), 32px);
1566 justify-content: flex-start;
1567 gap: 2px;
1568 border: 1px solid rgba(246, 240, 226, 0.09);
1569 border-radius: 8px;
1570 background: rgba(0, 0, 0, 0.14);
1571}
1572.compact .opp .nameplate {
1573 order: 0;
1574}
1575.compact .opp .hand {
1576 order: 1;
1577}
1578.compact .opp .melds {
1579 order: 2;
1580}
1581.compact .opp .discards {
1582 order: 3;
1583}
1584.compact .opp .actionbar {
1585 order: 4;
1586}
1587
1588.compact .opp .nameplate {
1589 flex-wrap: wrap;
1590 justify-content: center;
1591 gap: 3px 5px;
1592 font-size: 10px;
1593 padding-top: 0;
1594 row-gap: 1px;
1595}
1596.compact .opp .nameplate .wind {
1597 font-size: 13px;
1598}
1599.compact .opp .nameplate .wind em,
1600.compact .opp .nameplate .helpbtn,
1601.compact .opp .nameplate .sort {
1602 display: none;
1603}
1604.compact .opp .nameplate .chips {
1605 font-size: 13px;
1606 margin-left: 0;
1607}
1608.compact .opp .nameplate .tag {
1609 font-size: 9px;
1610 padding: 0 4px;
1611}
1612/* Face-down backs: a count you can see at a glance, nothing more. */
1613.compact .opp .hand {
1614 flex-wrap: wrap;
1615 justify-content: center;
1616 height: auto;
1617 padding-top: 0;
1618 gap: 1px;
1619 max-width: 100%;
1620}
1621.compact .opp .hand .tile {
1622 --tw: calc(var(--tile-w) * 0.52);
1623}
1624.compact .opp .melds {
1625 gap: 5px;
1626 min-height: 0;
1627 max-width: 100%;
1628}
1629/* Same size as the hand they were laid down from — which on an opponent's card
1630 is that card's own smaller tile, not the table's. */
1631.compact .opp .melds .tile {
1632 --tw: calc(var(--tile-w) * 0.52);
1633}
1634.compact .opp .meld.flowers {
1635 margin-left: 5px;
1636 padding-left: 5px;
1637}
1638.compact .opp .discards {
1639 flex: 1 1 auto;
1640 max-width: 100%;
1641 min-height: 0;
1642 overflow-y: auto;
1643 align-content: flex-start;
1644}
1645/* No height is reserved here: an opponent's bar is empty except in hotseat,
1646 and a card that stays still matters less than the room it costs. */
1647.compact .opp .actionbar {
1648 min-height: 0;
1649 padding-right: 0;
1650 justify-content: center;
1651 gap: 4px;
1652}
1653.compact .opp .actionbar .btn {
1654 height: auto;
1655 min-height: 34px;
1656 padding: 2px 8px;
1657}
1658.compact .opp .actionbar .bi .zh {
1659 font-size: 14px;
1660}
1661.compact .opp .delta {
1662 font-size: 15px;
1663}
1664
1665/* ---------- your own seat ---------- */
1666/* A grid rather than the stack the strips use, so the melds and the discards
1667 share one line and the hand gets the depth that saves. */
1668.compact .mine .seat {
1669 --tile-w: min(calc((100vw - 24px) / 18.3), 54px);
1670 display: grid;
1671 /* `minmax(0, auto)`, so a full set of melds wraps inside its column instead
1672 of shouldering the discards off the end of the strip. */
1673 grid-template-columns: minmax(0, auto) minmax(0, 1fr);
1674 grid-template-areas:
1675 'melds discards'
1676 'bar bar'
1677 'hand hand'
1678 'name name';
1679 align-content: end;
1680 align-items: end;
1681 justify-items: center;
1682 gap: 2px 8px;
1683 padding: 2px 6px 3px;
1684}
1685.compact .mine .melds {
1686 grid-area: melds;
1687 justify-content: flex-start;
1688 min-height: 0;
1689 gap: 6px;
1690}
1691/* Full size, like the hand under it — the discards beside them stay at half,
1692 which is now the whole of what tells the two rows apart. */
1693.compact .mine .melds .tile {
1694 --tw: calc(var(--tile-w) * 0.72);
1695}
1696.compact .mine .discards {
1697 grid-area: discards;
1698 /* One line that scrolls, kept pinned to its end by Seat.tsx — wrapping would
1699 cost a second row of depth the hand wants more. Reserved even while empty,
1700 since this row and the melds beside it share it: without that the strip
1701 would jump the first time either one appeared. */
1702 flex: none;
1703 flex-wrap: nowrap;
1704 justify-content: flex-start;
1705 align-self: end;
1706 max-width: 100%;
1707 width: 100%;
1708 min-height: calc(var(--tile-w) * 0.9);
1709 margin-bottom: 0;
1710 overflow-x: auto;
1711 overflow-y: hidden;
1712 /* Melds and discards end up shoulder to shoulder here, and both are rows of
1713 tiles — the rule and the smaller face are what tell them apart. */
1714 border-left: 1px solid rgba(246, 240, 226, 0.16);
1715 padding-left: 8px;
1716 opacity: 0.85;
1717}
1718.compact .mine .discards .tile {
1719 --tw: calc(var(--tile-w) * 0.5);
1720}
1721.compact .mine .actionbar {
1722 grid-area: bar;
1723 --bar-h: 44px;
1724 justify-content: center;
1725 padding-right: 0;
1726 gap: 6px;
1727}
1728.compact .mine .hand {
1729 grid-area: hand;
1730 flex-wrap: wrap;
1731 align-content: flex-end;
1732 justify-content: center;
1733 height: auto;
1734 max-width: 100%;
1735 /* The row gap matches the padding for the same reason: the 摸 tag hangs above
1736 its tile, and on a wrapped hand that is somebody else's row. */
1737 gap: calc(var(--tile-w) * 0.34) 2px;
1738 padding-top: calc(var(--tile-w) * 0.34);
1739}
1740.compact .mine .nameplate {
1741 grid-area: name;
1742 flex-wrap: wrap;
1743 justify-content: center;
1744 gap: 3px 7px;
1745 font-size: 11px;
1746 padding-top: 0;
1747}
1748.compact .mine .nameplate .wind {
1749 font-size: 15px;
1750}
1751.compact .mine .nameplate .wind em {
1752 display: none;
1753}
1754.compact .mine .nameplate .chips {
1755 font-size: 14px;
1756}
1757.compact .mine .nameplate .btn.sort {
1758 min-height: 26px;
1759 padding: 1px 9px;
1760 font-size: 12px;
1761}
1762.compact .mine .nameplate .helpbtn {
1763 width: 26px;
1764 height: 26px;
1765 font-size: 13px;
1766}
1767
1768/* ---------- the middle ---------- */
1769/* One bar across the table rather than a square in the middle of it. */
1770.compact .center {
1771 position: relative;
1772 inset: auto;
1773 flex-direction: row;
1774 align-items: center;
1775 justify-content: space-between;
1776 gap: 8px;
1777 padding: 1px 6px;
1778 min-height: 44px;
1779}
1780.compact .facing {
1781 flex: 1 1 auto;
1782 flex-direction: row;
1783 align-items: center;
1784 justify-content: center;
1785 gap: 10px;
1786 min-width: 0;
1787}
1788.compact .last-discard {
1789 --tile-w: 30px;
1790 flex-direction: row;
1791 align-items: center;
1792 gap: 6px;
1793}
1794.compact .ld-label {
1795 font-size: 10px;
1796}
1797.compact .prompt {
1798 font-size: 12px;
1799 text-align: center;
1800}
1801.compact .table-controls {
1802 position: static;
1803 flex: none;
1804}
1805.compact .btn.undo {
1806 max-width: 34vw;
1807 min-height: 30px;
1808 padding: 2px 10px;
1809}
1810.compact .undo-what .zh {
1811 font-size: 11px;
1812}
1813.compact .undo-what .en {
1814 display: none;
1815}
1816/* What the wall square was telling you, in the room a bar has. */
1817.wall-pill {
1818 display: flex;
1819 align-items: baseline;
1820 gap: 5px;
1821 flex: none;
1822 letter-spacing: 0.06em;
1823 opacity: 0.75;
1824}
1825.wall-pill b {
1826 font-size: 18px;
1827 font-weight: 900;
1828 color: var(--gold);
1829}
1830.wall-pill em {
1831 font-style: normal;
1832 font-size: 9px;
1833 text-transform: uppercase;
1834 opacity: 0.7;
1835}
1836
1837/* The hand result covers the table instead of sitting in a square that is no
1838 longer there. */
1839.compact .result {
1840 position: fixed;
1841 top: 50%;
1842 left: 50%;
1843 transform: translate(-50%, -50%);
1844 z-index: 25;
1845 max-width: min(560px, 94vw);
1846 max-height: 92dvh;
1847 padding: 12px 16px;
1848 gap: 8px;
1849}
1850.compact .result h2 {
1851 font-size: 18px;
1852}
1853.compact .tai-list {
1854 grid-template-columns: repeat(auto-fit, minmax(92px, 1fr));
1855 gap: 1px 12px;
1856 font-size: 12px;
1857}
1858.compact .ready-row {
1859 font-size: 12px;
1860}
1861.compact .ready-name {
1862 min-width: 4.5em;
1863}
1864.compact .ready-tiles {
1865 --tile-w: 26px;
1866}
1867.compact .deltas {
1868 flex-wrap: wrap;
1869 justify-content: center;
1870 gap: 6px 14px;
1871 font-size: 12px;
1872}
1873.compact .big-btn {
1874 font-size: 18px;
1875 padding: 8px 22px;
1876 min-height: 44px;
1877}
1878
1879/* ---------- portrait ---------- */
1880/* Held upright there is no room for three cards side by side, so they stack in
1881 the order play reaches them, and the hand wraps to two rows. */
1882@media (orientation: portrait) {
1883 .compact .table {
1884 grid-template-columns: 1fr;
1885 grid-template-rows: repeat(3, minmax(0, 1fr)) auto auto;
1886 grid-template-areas:
1887 'right'
1888 'across'
1889 'left'
1890 'bar'
1891 'mine';
1892 }
1893 /* A card gets the whole width here, so the tiles can be read rather than
1894 merely counted; the discards wrap to a second line when they need it. */
1895 .compact .opp .seat {
1896 --tile-w: clamp(18px, calc((100vw - 26px) / 13), 34px);
1897 }
1898 .compact .mine .seat {
1899 --tile-w: min(calc((100vw - 26px) / 9.4), 54px);
1900 }
1901 /* A phone's width will not hold the wall count, who discarded what, the claim
1902 prompt and the table's own buttons on one line. The readout — the part that
1903 changes — takes a line of its own underneath. The room comes out of the
1904 other seats, which are the flexible rows, so the hand never moves. */
1905 .compact .center {
1906 flex-wrap: wrap;
1907 row-gap: 2px;
1908 }
1909 .compact .wall-pill {
1910 order: 0;
1911 }
1912 .compact .table-controls {
1913 order: 1;
1914 }
1915 .compact .facing {
1916 order: 2;
1917 flex-basis: 100%;
1918 }
1919}
1920
1921/* ---------- the panels ---------- */
1922.compact .lobby {
1923 justify-content: flex-start;
1924 overflow-y: auto;
1925 gap: 0.25rem;
1926 padding: 12px 14px calc(14px + env(safe-area-inset-bottom));
1927}
1928.compact .lobby > * {
1929 flex: none;
1930}
1931/* Sized off both axes: a phone held sideways has width to spare and no height
1932 at all, and the title was the first thing to push the buttons off. */
1933.compact .lobby h1 {
1934 font-size: clamp(1.9rem, min(9vw, 11vh), 3.2rem);
1935 margin-bottom: 0.2rem;
1936}
1937.compact .lobby .sub {
1938 font-size: 0.78rem;
1939}
1940/* The screen dividers this asks for are a four-round-a-table idea, and nobody
1941 props cards against a phone. */
1942.compact .lobby .hint {
1943 display: none;
1944}
1945.compact .resume {
1946 margin-top: 0.8rem;
1947 padding: 8px 16px;
1948}
1949.compact .resume-scores {
1950 flex-wrap: wrap;
1951 gap: 4px 12px;
1952 font-size: 12px;
1953}
1954.compact .lobby-buttons {
1955 margin-top: 0.9rem;
1956 gap: 8px;
1957 width: min(460px, 100%);
1958}
1959.compact .lobby-buttons .big-btn {
1960 min-width: 0;
1961 width: 100%;
1962 padding: 8px 16px;
1963}
1964.compact .lobby-extras {
1965 margin-top: 0.9rem;
1966}
1967/* Sideways there is barely 390px of height for a title, a saved game, three
1968 ways to start and the settings link. Everything gives a little so the last of
1969 them lands above the fold, rather than behind a scroll nobody looks for. */
1970@media (orientation: landscape) {
1971 .compact .lobby h1 {
1972 font-size: clamp(1.6rem, 9vh, 2.6rem);
1973 }
1974 .compact .lobby .sub,
1975 .compact .resume-when {
1976 display: none;
1977 }
1978 .compact .resume {
1979 margin-top: 0.4rem;
1980 padding: 6px 14px;
1981 }
1982 .compact .resume-line {
1983 font-size: 12px;
1984 }
1985 .compact .lobby-buttons {
1986 margin-top: 0.5rem;
1987 }
1988 .compact .lobby-buttons .big-btn {
1989 min-height: 40px;
1990 padding: 5px 16px;
1991 font-size: 19px;
1992 }
1993 .compact .lobby-extras {
1994 margin-top: 0.5rem;
1995 }
1996}
1997
1998.compact .help-slot .help {
1999 width: calc(100vw - 16px);
2000 --hd: calc(100dvh - 16px);
2001 translate: none;
2002 padding: 8px 12px 10px;
2003}
2004.compact .help-head h3 {
2005 font-size: 13px;
2006}
2007/* The strip version fills four columns and scrolls sideways, which reads fine
2008 across a laptop's width and not at all on a phone. One column, scrolled the
2009 way every other sheet on the device is. */
2010.compact .help-body {
2011 columns: auto;
2012 column-gap: normal;
2013 overflow-x: hidden;
2014}
2015
2016.compact .settings-scrim {
2017 padding: 6px;
2018}
2019.compact .settings {
2020 padding: 10px 14px 14px;
2021}
2022.compact .settings-head h3 {
2023 font-size: 16px;
2024}
2025.compact .settings-head .btn {
2026 min-height: 38px;
2027 padding: 2px 14px;
2028 font-size: 14px;
2029}
2030/* Two columns sideways, where there is width and no height; one held upright,
2031 where it is the other way round. */
2032.compact .settings-body {
2033 columns: 2;
2034 column-gap: 26px;
2035 padding-top: 10px;
2036}
2037@media (orientation: portrait) {
2038 .compact .settings-body {
2039 columns: 1;
2040 }
2041}
2042.compact .settings-body section {
2043 margin-bottom: 14px;
2044}
2045.compact .settings-body h4 {
2046 margin-bottom: 6px;
2047 font-size: 14px;
2048}
2049.compact .settings .f-label {
2050 min-width: 116px;
2051 font-size: 14px;
2052}
2053.compact .settings input[type='text'],
2054.compact .settings input:not([type]),
2055.compact .settings input[type='number'] {
2056 font-size: 15px;
2057 min-height: 40px;
2058 padding: 8px 10px;
2059}
2060.compact .settings .sw-name {
2061 font-size: 14px;
2062}
2063.compact .settings .note {
2064 font-size: 11px;
2065}
2066
2067/* ---------- fullscreen ---------- */
2068/* Sized to match the sound toggle it stands beside, in both its forms. */
2069.fs-toggle {
2070 min-height: 34px;
2071 padding: 2px 14px;
2072}
2073.fs-toggle.compact {
2074 min-height: 28px;
2075 padding: 3px 9px;
2076}
2077.fs-toggle.on {
2078 border-color: rgba(217, 180, 92, 0.7);
2079 color: var(--gold);
2080}
2081.fs-toggle .fs-icon {
2082 display: block;
2083 flex: none;
2084}
2085
2086/* ---------- the party table's QR ---------- */
2087/* One join code for the whole table, lying in the middle of the felt like a
2088 tile someone left face up. It may cover the pool while it shows — it steps
2089 aside the moment every hand is held, and comes back while a 📱 tag is held
2090 down. Below the result panel (z-index 2, later in the DOM), above the pool
2091 and the centre readout (1): scannable while it matters, never over the
2092 scores. */
2093.qr-dock {
2094 position: absolute;
2095 left: 50%;
2096 top: 50%;
2097 transform: translate(-50%, -50%);
2098 z-index: 2;
2099 display: flex;
2100 flex-direction: column;
2101 align-items: center;
2102 gap: 8px;
2103 /* Purely something to look at: taps fall through to the table under it. */
2104 pointer-events: none;
2105}
2106/* The QR wears the face of a giant tile: ivory, softly bevelled, the same
2107 rounded corners and resting shadow the sprite gives the real ones, with the
2108 code engraved in the deep green the dragons are cut in. */
2109.seat-qr {
2110 --qw: clamp(96px, calc(var(--depth) * 0.56), 168px);
2111 width: var(--qw);
2112 height: calc(var(--qw) * 1.2);
2113 display: grid;
2114 place-items: center;
2115 padding: calc(var(--qw) * 0.09);
2116 border-radius: calc(var(--qw) * 0.1);
2117 background: linear-gradient(165deg, #fffdf3, #f4edd9 58%, #e6dcbf);
2118 box-shadow:
2119 inset 0 2px 1px rgba(255, 255, 255, 0.85),
2120 inset 0 calc(var(--qw) * -0.05) calc(var(--qw) * 0.04) rgba(148, 130, 90, 0.4),
2121 inset calc(var(--qw) * -0.02) 0 calc(var(--qw) * 0.03) rgba(148, 130, 90, 0.22),
2122 0 6px 12px rgba(0, 0, 0, 0.5);
2123}
2124.seat-qr svg {
2125 display: block;
2126 width: 100%;
2127 height: auto;
2128 aspect-ratio: 1;
2129}
2130.qr-hint {
2131 font-size: 11px;
2132 line-height: 1.35;
2133 color: var(--ivory);
2134 text-align: center;
2135 max-width: 190px;
2136 padding: 5px 12px;
2137 border-radius: 999px;
2138 background: rgba(10, 49, 38, 0.88);
2139 border: 1px solid rgba(217, 180, 92, 0.4);
2140}
2141.qr-hint em {
2142 display: block;
2143 font-style: normal;
2144 opacity: 0.6;
2145 font-size: 9px;
2146}
2147.nameplate .tag.phone {
2148 background: rgba(29, 90, 168, 0.35);
2149 border-color: rgba(29, 90, 168, 0.7);
2150 cursor: pointer;
2151 user-select: none;
2152 touch-action: none;
2153}
2154
2155/* ---------- the phone controller ---------- */
2156/* A hand held in landscape: status, buttons, tiles, in that order down the
2157 screen, with the common screen doing everything else. Tiles get the room —
2158 they are the only thing this screen is for. */
2159.ctl {
2160 /* The whole hand in one row: 17 tiles and their gaps must fit the width. */
2161 --tile-w: clamp(30px, calc((100vw - 56px) / 17.6), 76px);
2162 position: fixed;
2163 inset: 0;
2164 display: flex;
2165 flex-direction: column;
2166 background: radial-gradient(ellipse at center, var(--felt) 0%, var(--felt-2) 80%);
2167 color: var(--ivory);
2168 overflow: hidden;
2169 touch-action: none;
2170}
2171.ctl-top {
2172 display: flex;
2173 align-items: center;
2174 gap: 8px;
2175 padding: 8px 12px 4px;
2176 flex-wrap: wrap;
2177 font-size: 13px;
2178}
2179.ctl-top .wind {
2180 font-weight: 900;
2181 font-size: 18px;
2182 color: var(--gold);
2183}
2184.ctl-top .wind em {
2185 font-style: normal;
2186 font-size: 10px;
2187 margin-left: 2px;
2188 opacity: 0.7;
2189}
2190.ctl-top .pname {
2191 font-weight: 600;
2192}
2193.ctl-top .tag {
2194 border: 1px solid rgba(246, 240, 226, 0.4);
2195 border-radius: 20px;
2196 padding: 1px 8px;
2197 font-size: 11px;
2198}
2199.ctl-top .tag.ting {
2200 border-color: var(--gold);
2201 color: var(--gold);
2202}
2203.ctl-status {
2204 opacity: 0.75;
2205}
2206.ctl-status em {
2207 font-style: normal;
2208 opacity: 0.6;
2209 margin-left: 4px;
2210}
2211.ctl-status.hot {
2212 opacity: 1;
2213 color: var(--gold);
2214 font-weight: 600;
2215}
2216.ctl-last {
2217 display: inline-flex;
2218 align-items: center;
2219 gap: 4px;
2220 margin-left: auto;
2221}
2222.ctl-top .chips {
2223 font-weight: 600;
2224 color: var(--gold);
2225}
2226.ctl-actions {
2227 display: flex;
2228 align-items: center;
2229 justify-content: center;
2230 gap: 8px;
2231 flex-wrap: wrap;
2232 min-height: var(--bar-h);
2233 padding: 2px 10px;
2234}
2235.ctl-hint {
2236 font-size: 11px;
2237 opacity: 0.55;
2238}
2239.ctl-hand {
2240 margin-top: auto;
2241 display: flex;
2242 justify-content: center;
2243 padding: 0 8px calc(10px + env(safe-area-inset-bottom, 0px));
2244}
2245.ctl-hand .hand {
2246 flex-wrap: nowrap;
2247}
2248/* Held upright, seventeen tiles will never make one row. Break into two and
2249 size the tiles off half the count instead of shrinking them past reading.
2250 The row gap leaves room for the 摸 tag over a tile in the lower row. */
2251@media (orientation: portrait) {
2252 .ctl {
2253 --tile-w: clamp(34px, calc((100vw - 44px) / 9.4), 76px);
2254 }
2255 .ctl-hand .hand {
2256 flex-wrap: wrap;
2257 justify-content: center;
2258 /* Bounded, or it stays one long row: a `flex: none` item is happy to
2259 overflow its container rather than wrap inside it. */
2260 width: 100%;
2261 height: auto;
2262 row-gap: calc(var(--tile-w) * 0.4);
2263 }
2264}
2265/* The tile in the fingers once it has left the row, and its exit: flung, it
2266 keeps going up off the screen — the common screen takes it from here. */
2267.ctl-held {
2268 position: fixed;
2269 z-index: 5;
2270 pointer-events: none;
2271 transform: translate(-50%, -50%);
2272}
2273.ctl-held .tile {
2274 width: 100%;
2275 height: 100%;
2276}
2277.ctl-held.flung {
2278 animation: fling-up 0.3s ease-in forwards;
2279}
2280@keyframes fling-up {
2281 to {
2282 transform: translate(-50%, -140vh) rotate(18deg);
2283 opacity: 0.9;
2284 }
2285}
2286.ctl-result {
2287 position: fixed;
2288 left: 50%;
2289 top: 44%;
2290 transform: translate(-50%, -50%);
2291 z-index: 6;
2292 background: rgba(10, 49, 38, 0.96);
2293 border: 1px solid rgba(217, 180, 92, 0.5);
2294 border-radius: 14px;
2295 padding: 16px 22px;
2296 text-align: center;
2297 max-width: min(92vw, 480px);
2298}
2299.ctl-result h2 {
2300 margin: 0 0 8px;
2301 font-size: 18px;
2302}
2303.ctl-result .deltas {
2304 display: flex;
2305 gap: 12px;
2306 flex-wrap: wrap;
2307 justify-content: center;
2308 margin-bottom: 12px;
2309 font-size: 13px;
2310}
2311.ctl-result .deltas .up {
2312 color: var(--gold);
2313}
2314.ctl-result .deltas .down {
2315 color: #e08a84;
2316}
2317
2318/* Seat picking, on the phone and in the online lobby. */
2319.ctl-pick {
2320 align-items: center;
2321 justify-content: center;
2322 text-align: center;
2323 overflow-y: auto;
2324 touch-action: pan-y;
2325}
2326.pick-grid {
2327 display: grid;
2328 grid-template-columns: repeat(2, minmax(150px, 220px));
2329 gap: 10px;
2330 justify-content: center;
2331 margin: 12px 0;
2332}
2333.pick-grid .btn em {
2334 display: block;
2335 font-style: normal;
2336 font-size: 11px;
2337 opacity: 0.65;
2338 margin-top: 2px;
2339}
2340
2341/* The dice tool. Dev builds only — `import.meta.env.DEV` in ui/Center — so it
2342 is styled for being useful rather than for being lived with: bottom left, out
2343 of the way of the undo button and of every seat's own strip. */
2344.dice-debug {
2345 position: absolute;
2346 z-index: 2;
2347 left: 10px;
2348 bottom: 8px;
2349 display: flex;
2350 flex-direction: column;
2351 align-items: center;
2352 gap: 4px;
2353 padding: 6px 10px 5px;
2354 border-radius: 10px;
2355 background: rgba(0, 0, 0, 0.32);
2356 border: 1px solid rgba(217, 180, 92, 0.25);
2357 font-size: 11px;
2358 /* A wall gets pushed about through this corner, and the panel is only here to
2359 be read — so the finger goes through it, and only the buttons take it. */
2360 pointer-events: none;
2361}
2362.dice-debug .btn {
2363 pointer-events: auto;
2364}
2365.table.carrying .dice-debug {
2366 pointer-events: none;
2367}
2368.dice-faces {
2369 display: flex;
2370 align-items: baseline;
2371 gap: 4px;
2372 line-height: 1;
2373}
2374.dice-faces b {
2375 font-size: 26px;
2376 font-weight: 400;
2377 color: var(--gold);
2378}
2379.dice-faces em {
2380 font-style: normal;
2381 font-size: 13px;
2382 font-weight: 700;
2383 opacity: 0.75;
2384 margin-left: 4px;
2385}
2386.dice-row {
2387 display: flex;
2388 gap: 4px;
2389}
2390.dice-row .btn {
2391 font-size: 12px;
2392 min-height: 0;
2393 padding: 3px 9px;
2394}
2395.dice-row .btn.on {
2396 border-color: var(--gold);
2397 color: var(--gold);
2398}
2399.dice-says {
2400 opacity: 0.6;
2401 letter-spacing: 0.02em;
2402 white-space: nowrap;
2403}
2404
2405/* The gear's panel. Same shell as the rules sheet — anchored in the player's
2406 own strip and turned to face it — but it is four buttons, not a book, so it
2407 is only as tall as they are. */
2408/* `--hd` is how deep it stands, and `--off` — how far out of the middle it is
2409 pushed towards its own edge — is worked out from it, so the menu sets the
2410 depth rather than letting its content decide it. Four buttons' worth. */
2411.help-slot.slot-bottom .help.menu,
2412.help-slot.slot-top .help.menu,
2413.help-slot.slot-left .help.menu,
2414.help-slot.slot-right .help.menu {
2415 width: min(420px, calc(100vw - 60px));
2416 --hd: 268px;
2417}
2418.help.menu {
2419 padding: 10px 18px 16px;
2420}
2421.menu-body {
2422 display: flex;
2423 flex-direction: column;
2424 gap: 8px;
2425 margin-top: 10px;
2426}
2427.menu-body .btn {
2428 justify-content: flex-start;
2429 width: 100%;
2430}
2431.menu-body .btn:disabled {
2432 opacity: 0.4;
2433 cursor: default;
2434}