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