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/* ---------- language ---------- */
1149/* Sized to stand beside the sound and fullscreen pills in both their forms. */
1150.lang-toggle {
1151 min-height: 34px;
1152 padding: 2px 14px;
1153}
1154.lang-toggle.compact {
1155 min-height: 28px;
1156 padding: 2px 9px;
1157}
1158/* A boxed glyph, so 雙 / 中 / A all take the same room and the row does not
1159 shuffle sideways as the mode steps round. */
1160.lang-toggle .lang-icon {
1161 display: inline-grid;
1162 place-items: center;
1163 width: 18px;
1164 height: 18px;
1165 flex: none;
1166 border: 1px solid rgba(230, 226, 214, 0.45);
1167 border-radius: 4px;
1168 font-size: 12px;
1169 line-height: 1;
1170}
1171
1172/* ---------- shared table controls ---------- */
1173/* Bottom-right of the centre square, opposite the log. Unrotated on purpose:
1174 it belongs to the table, not to a seat. */
1175.table-controls {
1176 position: absolute;
1177 /* Over the pile, same as .result. */
1178 z-index: 2;
1179 right: 10px;
1180 bottom: 8px;
1181 display: flex;
1182 align-items: center;
1183 gap: 8px;
1184}
1185.btn.undo {
1186 min-height: 34px;
1187 padding: 3px 14px;
1188 border-radius: 999px;
1189 gap: 9px;
1190 max-width: 40vh;
1191}
1192.undo-arrow {
1193 font-size: 17px;
1194 line-height: 1;
1195 color: var(--gold);
1196}
1197.undo-what {
1198 display: flex;
1199 flex-direction: column;
1200 align-items: flex-start;
1201 line-height: 1.15;
1202 min-width: 0;
1203}
1204.undo-what .zh {
1205 font-size: 13px;
1206 letter-spacing: 0.05em;
1207}
1208.undo-what .en {
1209 font-size: 9px;
1210 letter-spacing: 0.09em;
1211 text-transform: uppercase;
1212 opacity: 0.55;
1213 white-space: nowrap;
1214 overflow: hidden;
1215 text-overflow: ellipsis;
1216 max-width: 100%;
1217}
1218
1219/* ---------- result: one block per winner ---------- */
1220.result .win {
1221 display: flex;
1222 flex-direction: column;
1223 align-items: center;
1224 gap: 8px;
1225 width: 100%;
1226}
1227.result .win + .win {
1228 padding-top: 10px;
1229 border-top: 1px solid rgba(217, 180, 92, 0.3);
1230}
1231.result .liable {
1232 font-size: 12px;
1233 letter-spacing: 0.08em;
1234 color: #ffb4a2;
1235}
1236.result .multi {
1237 font-size: 12px;
1238 letter-spacing: 0.1em;
1239 color: var(--gold);
1240 opacity: 0.85;
1241}
1242
1243/* ---------- settings ---------- */
1244.over-buttons {
1245 display: flex;
1246 gap: 12px;
1247 align-items: center;
1248}
1249.settings-scrim {
1250 position: absolute;
1251 inset: 0;
1252 z-index: 40;
1253 background: rgba(4, 18, 14, 0.72);
1254 display: flex;
1255 align-items: center;
1256 justify-content: center;
1257 padding: 20px;
1258}
1259.settings {
1260 width: min(1180px, 100%);
1261 max-height: 100%;
1262 display: flex;
1263 flex-direction: column;
1264 background: rgba(8, 32, 25, 0.99);
1265 border: 1px solid rgba(217, 180, 92, 0.4);
1266 border-radius: 14px;
1267 box-shadow: 0 18px 50px rgba(0, 0, 0, 0.6);
1268 padding: 16px 28px 22px;
1269}
1270.settings-head {
1271 display: flex;
1272 align-items: center;
1273 justify-content: space-between;
1274 gap: 16px;
1275 flex: none;
1276 padding-bottom: 12px;
1277 border-bottom: 1px solid rgba(246, 240, 226, 0.12);
1278}
1279.settings-head h3 {
1280 margin: 0;
1281 font-size: 22px;
1282 font-weight: 400;
1283 letter-spacing: 0.08em;
1284 color: var(--gold);
1285}
1286.settings-head-btns {
1287 display: flex;
1288 gap: 10px;
1289}
1290.settings-head .btn {
1291 min-height: 46px;
1292 padding: 2px 22px;
1293 font-size: 17px;
1294}
1295.settings-body {
1296 /* `1 1 auto`, not `1 1 0`: the panel's own height comes from this content,
1297 so a zero basis collapses the pair of them to nothing. `max-height` on the
1298 panel is what brings the scrollbar back at the other end. */
1299 flex: 1 1 auto;
1300 min-height: 0;
1301 overflow-y: auto;
1302 scrollbar-width: thin;
1303 padding-top: 14px;
1304 /* Columns rather than a grid: grid rows would size to the tallest section
1305 and leave the shorter one padded with air, which is what pushed the last
1306 house rule off the bottom. */
1307 columns: 2;
1308 column-gap: 44px;
1309}
1310.settings-body section {
1311 break-inside: avoid;
1312 margin-bottom: 22px;
1313}
1314.settings-body h4 {
1315 margin: 0 0 10px;
1316 font-size: 16px;
1317 font-weight: 400;
1318 color: var(--gold);
1319 letter-spacing: 0.06em;
1320}
1321.settings-body h4 em {
1322 font-style: normal;
1323 font-size: 11px;
1324 letter-spacing: 0.12em;
1325 text-transform: uppercase;
1326 opacity: 0.55;
1327 margin-left: 5px;
1328}
1329.settings .field {
1330 display: flex;
1331 align-items: center;
1332 gap: 14px;
1333 padding: 5px 0;
1334}
1335.settings .f-label {
1336 display: flex;
1337 align-items: baseline;
1338 gap: 6px;
1339 font-size: 16px;
1340 /* Wide enough for 起始籌碼 STARTING CHIPS, so every input in a section
1341 starts at the same place. */
1342 min-width: 196px;
1343 opacity: 0.85;
1344}
1345/* ...except the player rows, which are labelled 1–4 and need no such room. */
1346.settings .names .f-label {
1347 min-width: 26px;
1348}
1349.settings .f-label em {
1350 font-style: normal;
1351 font-size: 11px;
1352 letter-spacing: 0.1em;
1353 text-transform: uppercase;
1354 opacity: 0.55;
1355}
1356.settings input[type='text'],
1357.settings input:not([type]),
1358.settings input[type='number'] {
1359 flex: 1 1 0;
1360 min-width: 0;
1361 font-family: inherit;
1362 font-size: 18px;
1363 color: var(--ivory);
1364 background: rgba(0, 0, 0, 0.28);
1365 border: 1px solid rgba(246, 240, 226, 0.25);
1366 border-radius: 8px;
1367 padding: 11px 14px;
1368 min-height: 48px;
1369 user-select: text;
1370}
1371.settings input:focus {
1372 outline: none;
1373 border-color: rgba(217, 180, 92, 0.8);
1374}
1375.settings input[type='number'] {
1376 max-width: 150px;
1377}
1378.settings input[type='range'] {
1379 flex: 1 1 0;
1380 height: 34px;
1381 accent-color: var(--gold);
1382}
1383.settings input:disabled {
1384 opacity: 0.4;
1385}
1386.settings .switch {
1387 display: grid;
1388 grid-template-columns: auto 1fr;
1389 gap: 3px 14px;
1390 align-items: center;
1391 padding: 9px 0;
1392 cursor: pointer;
1393}
1394.settings .switch input {
1395 width: 26px;
1396 height: 26px;
1397 accent-color: var(--gold);
1398 cursor: pointer;
1399}
1400.settings .sw-name {
1401 display: flex;
1402 align-items: baseline;
1403 gap: 8px;
1404 font-size: 17px;
1405}
1406.settings .sw-name em {
1407 font-style: normal;
1408 font-size: 11px;
1409 letter-spacing: 0.1em;
1410 text-transform: uppercase;
1411 opacity: 0.55;
1412}
1413.settings .note {
1414 grid-column: 2;
1415 font-size: 13px;
1416 line-height: 1.4;
1417 opacity: 0.55;
1418}
1419.settings .switch.dead {
1420 opacity: 0.4;
1421 cursor: default;
1422}
1423.settings .note.lock {
1424 grid-column: auto;
1425 margin: 4px 0 0;
1426 color: #ffb4a2;
1427 opacity: 0.75;
1428}
1429
1430/* ============================================================================
1431 compact — a phone
1432
1433 The table layout is four strips of --depth arranged around a centre square,
1434 which wants about 700px each way; a phone has that in neither, and in
1435 landscape the top and bottom strips simply overlap. It also assumes four
1436 people sitting around a screen lying flat, which is not what a phone is.
1437
1438 Compact throws both assumptions out. Nothing is rotated — every label reads
1439 upright for the one person holding the phone — the table becomes a grid, the
1440 three other seats shrink to cards showing what is public about them, and the
1441 depth that frees up goes to the hand, which wraps when it has to.
1442
1443 The `compact` class is put on <html> by useCompact(); the breakpoint itself
1444 lives in ui/compact.ts, so it is written down once.
1445 ============================================================================ */
1446
1447.compact .table {
1448 display: grid;
1449 gap: 2px;
1450 /* Landscape is where a notch eats into the sides. */
1451 padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom)
1452 env(safe-area-inset-left);
1453 grid-template-columns: 1fr 1fr 1fr;
1454 grid-template-rows: minmax(0, 1fr) auto auto;
1455 grid-template-areas:
1456 'left across right'
1457 'bar bar bar'
1458 'mine mine mine';
1459}
1460.compact .slot {
1461 position: relative;
1462 inset: auto;
1463 width: auto;
1464 height: auto;
1465 min-width: 0;
1466 min-height: 0;
1467}
1468/* `.slot` and not the edge class alone: the rules sheet carries an edge class
1469 of its own, and would otherwise be parked in the grid area named here. */
1470.compact .slot.slot-left {
1471 grid-area: left;
1472}
1473.compact .slot.slot-top {
1474 grid-area: across;
1475}
1476.compact .slot.slot-right {
1477 grid-area: right;
1478}
1479.compact .slot.slot-bottom {
1480 grid-area: mine;
1481}
1482.compact .center {
1483 grid-area: bar;
1484}
1485/* The strip no longer floats in the middle of a slot — it is the slot. */
1486.compact .seat {
1487 position: relative;
1488 top: auto;
1489 left: auto;
1490 width: auto;
1491 height: 100%;
1492 padding: 2px 4px 3px;
1493}
1494/* Off to one side rather than dead centre: a card is narrow enough that the
1495 arrow would otherwise land on the name it is pointing out. */
1496.compact .win-arrow {
1497 font-size: 24px;
1498 left: 14px;
1499}
1500
1501/* ---------- the other three seats ---------- */
1502/* Read top-down: who they are, how many tiles they hold, what they have laid
1503 down, what they have thrown. The hand is the least interesting of those and
1504 goes smallest; the discards get whatever room is left, since reading them is
1505 the whole of playing against somebody. */
1506.compact .opp .seat {
1507 --tile-w: clamp(13px, calc((100vw / 3 - 20px) / 8.6), 32px);
1508 justify-content: flex-start;
1509 gap: 2px;
1510 border: 1px solid rgba(246, 240, 226, 0.09);
1511 border-radius: 8px;
1512 background: rgba(0, 0, 0, 0.14);
1513}
1514.compact .opp .nameplate {
1515 order: 0;
1516}
1517.compact .opp .hand {
1518 order: 1;
1519}
1520.compact .opp .melds {
1521 order: 2;
1522}
1523.compact .opp .discards {
1524 order: 3;
1525}
1526.compact .opp .actionbar {
1527 order: 4;
1528}
1529
1530.compact .opp .nameplate {
1531 flex-wrap: wrap;
1532 justify-content: center;
1533 gap: 3px 5px;
1534 font-size: 10px;
1535 padding-top: 0;
1536 row-gap: 1px;
1537}
1538.compact .opp .nameplate .wind {
1539 font-size: 13px;
1540}
1541.compact .opp .nameplate .wind em,
1542.compact .opp .nameplate .helpbtn,
1543.compact .opp .nameplate .sort {
1544 display: none;
1545}
1546.compact .opp .nameplate .chips {
1547 font-size: 13px;
1548 margin-left: 0;
1549}
1550.compact .opp .nameplate .tag {
1551 font-size: 9px;
1552 padding: 0 4px;
1553}
1554/* Face-down backs: a count you can see at a glance, nothing more. */
1555.compact .opp .hand {
1556 flex-wrap: wrap;
1557 justify-content: center;
1558 height: auto;
1559 padding-top: 0;
1560 gap: 1px;
1561 max-width: 100%;
1562}
1563.compact .opp .hand .tile {
1564 --tw: calc(var(--tile-w) * 0.52);
1565}
1566.compact .opp .melds {
1567 gap: 5px;
1568 min-height: 0;
1569 max-width: 100%;
1570}
1571.compact .opp .meld.flowers {
1572 margin-left: 5px;
1573 padding-left: 5px;
1574}
1575.compact .opp .discards {
1576 flex: 1 1 auto;
1577 max-width: 100%;
1578 min-height: 0;
1579 overflow-y: auto;
1580 align-content: flex-start;
1581}
1582/* No height is reserved here: an opponent's bar is empty except in hotseat,
1583 and a card that stays still matters less than the room it costs. */
1584.compact .opp .actionbar {
1585 min-height: 0;
1586 padding-right: 0;
1587 justify-content: center;
1588 gap: 4px;
1589}
1590.compact .opp .actionbar .btn {
1591 height: auto;
1592 min-height: 34px;
1593 padding: 2px 8px;
1594}
1595.compact .opp .actionbar .bi .zh {
1596 font-size: 14px;
1597}
1598.compact .opp .delta {
1599 font-size: 15px;
1600}
1601
1602/* ---------- your own seat ---------- */
1603/* A grid rather than the stack the strips use, so the melds and the discards
1604 share one line and the hand gets the depth that saves. */
1605.compact .mine .seat {
1606 --tile-w: min(calc((100vw - 24px) / 18.3), 54px);
1607 display: grid;
1608 /* `minmax(0, auto)`, so a full set of melds wraps inside its column instead
1609 of shouldering the discards off the end of the strip. */
1610 grid-template-columns: minmax(0, auto) minmax(0, 1fr);
1611 grid-template-areas:
1612 'melds discards'
1613 'bar bar'
1614 'hand hand'
1615 'name name';
1616 align-content: end;
1617 align-items: end;
1618 justify-items: center;
1619 gap: 2px 8px;
1620 padding: 2px 6px 3px;
1621}
1622.compact .mine .melds {
1623 grid-area: melds;
1624 justify-content: flex-start;
1625 min-height: 0;
1626 gap: 6px;
1627}
1628.compact .mine .discards {
1629 grid-area: discards;
1630 /* One line that scrolls, kept pinned to its end by Seat.tsx — wrapping would
1631 cost a second row of depth the hand wants more. Reserved even while empty,
1632 since this row and the melds beside it share it: without that the strip
1633 would jump the first time either one appeared. */
1634 flex: none;
1635 flex-wrap: nowrap;
1636 justify-content: flex-start;
1637 align-self: end;
1638 max-width: 100%;
1639 width: 100%;
1640 min-height: calc(var(--tile-w) * 0.9);
1641 margin-bottom: 0;
1642 overflow-x: auto;
1643 overflow-y: hidden;
1644 /* Melds and discards end up shoulder to shoulder here, and both are rows of
1645 tiles — the rule and the smaller face are what tell them apart. */
1646 border-left: 1px solid rgba(246, 240, 226, 0.16);
1647 padding-left: 8px;
1648 opacity: 0.85;
1649}
1650.compact .mine .discards .tile {
1651 --tw: calc(var(--tile-w) * 0.5);
1652}
1653.compact .mine .actionbar {
1654 grid-area: bar;
1655 --bar-h: 44px;
1656 justify-content: center;
1657 padding-right: 0;
1658 gap: 6px;
1659}
1660.compact .mine .hand {
1661 grid-area: hand;
1662 flex-wrap: wrap;
1663 align-content: flex-end;
1664 justify-content: center;
1665 height: auto;
1666 max-width: 100%;
1667 /* The row gap matches the padding for the same reason: the 摸 tag hangs above
1668 its tile, and on a wrapped hand that is somebody else's row. */
1669 gap: calc(var(--tile-w) * 0.34) 2px;
1670 padding-top: calc(var(--tile-w) * 0.34);
1671}
1672.compact .mine .nameplate {
1673 grid-area: name;
1674 flex-wrap: wrap;
1675 justify-content: center;
1676 gap: 3px 7px;
1677 font-size: 11px;
1678 padding-top: 0;
1679}
1680.compact .mine .nameplate .wind {
1681 font-size: 15px;
1682}
1683.compact .mine .nameplate .wind em {
1684 display: none;
1685}
1686.compact .mine .nameplate .chips {
1687 font-size: 14px;
1688}
1689.compact .mine .nameplate .btn.sort {
1690 min-height: 26px;
1691 padding: 1px 9px;
1692 font-size: 12px;
1693}
1694.compact .mine .nameplate .helpbtn {
1695 width: 26px;
1696 height: 26px;
1697 font-size: 13px;
1698}
1699
1700/* ---------- the middle ---------- */
1701/* One bar across the table rather than a square in the middle of it. */
1702.compact .center {
1703 position: relative;
1704 inset: auto;
1705 flex-direction: row;
1706 align-items: center;
1707 justify-content: space-between;
1708 gap: 8px;
1709 padding: 1px 6px;
1710 min-height: 44px;
1711}
1712.compact .facing {
1713 flex: 1 1 auto;
1714 flex-direction: row;
1715 align-items: center;
1716 justify-content: center;
1717 gap: 10px;
1718 min-width: 0;
1719}
1720.compact .last-discard {
1721 --tile-w: 30px;
1722 flex-direction: row;
1723 align-items: center;
1724 gap: 6px;
1725}
1726.compact .ld-label {
1727 font-size: 10px;
1728}
1729.compact .prompt {
1730 font-size: 12px;
1731 text-align: center;
1732}
1733.compact .table-controls {
1734 position: static;
1735 flex: none;
1736}
1737.compact .btn.undo {
1738 max-width: 34vw;
1739 min-height: 30px;
1740 padding: 2px 10px;
1741}
1742.compact .undo-what .zh {
1743 font-size: 11px;
1744}
1745.compact .undo-what .en {
1746 display: none;
1747}
1748/* What the wall square was telling you, in the room a bar has. */
1749.wall-pill {
1750 display: flex;
1751 align-items: baseline;
1752 gap: 5px;
1753 flex: none;
1754 letter-spacing: 0.06em;
1755 opacity: 0.75;
1756}
1757.wall-pill b {
1758 font-size: 18px;
1759 font-weight: 900;
1760 color: var(--gold);
1761}
1762.wall-pill em {
1763 font-style: normal;
1764 font-size: 9px;
1765 text-transform: uppercase;
1766 opacity: 0.7;
1767}
1768
1769/* The hand result covers the table instead of sitting in a square that is no
1770 longer there. */
1771.compact .result {
1772 position: fixed;
1773 top: 50%;
1774 left: 50%;
1775 transform: translate(-50%, -50%);
1776 z-index: 25;
1777 max-width: min(560px, 94vw);
1778 max-height: 92dvh;
1779 padding: 12px 16px;
1780 gap: 8px;
1781}
1782.compact .result h2 {
1783 font-size: 18px;
1784}
1785.compact .tai-list {
1786 grid-template-columns: repeat(auto-fit, minmax(92px, 1fr));
1787 gap: 1px 12px;
1788 font-size: 12px;
1789}
1790.compact .ready-row {
1791 font-size: 12px;
1792}
1793.compact .ready-name {
1794 min-width: 4.5em;
1795}
1796.compact .ready-tiles {
1797 --tile-w: 26px;
1798}
1799.compact .deltas {
1800 flex-wrap: wrap;
1801 justify-content: center;
1802 gap: 6px 14px;
1803 font-size: 12px;
1804}
1805.compact .big-btn {
1806 font-size: 18px;
1807 padding: 8px 22px;
1808 min-height: 44px;
1809}
1810
1811/* ---------- portrait ---------- */
1812/* Held upright there is no room for three cards side by side, so they stack in
1813 the order play reaches them, and the hand wraps to two rows. */
1814@media (orientation: portrait) {
1815 .compact .table {
1816 grid-template-columns: 1fr;
1817 grid-template-rows: repeat(3, minmax(0, 1fr)) auto auto;
1818 grid-template-areas:
1819 'right'
1820 'across'
1821 'left'
1822 'bar'
1823 'mine';
1824 }
1825 /* A card gets the whole width here, so the tiles can be read rather than
1826 merely counted; the discards wrap to a second line when they need it. */
1827 .compact .opp .seat {
1828 --tile-w: clamp(18px, calc((100vw - 26px) / 13), 34px);
1829 }
1830 .compact .mine .seat {
1831 --tile-w: min(calc((100vw - 26px) / 9.4), 54px);
1832 }
1833 /* A phone's width will not hold the wall count, who discarded what, the claim
1834 prompt and the table's own buttons on one line. The readout — the part that
1835 changes — takes a line of its own underneath. The room comes out of the
1836 other seats, which are the flexible rows, so the hand never moves. */
1837 .compact .center {
1838 flex-wrap: wrap;
1839 row-gap: 2px;
1840 }
1841 .compact .wall-pill {
1842 order: 0;
1843 }
1844 .compact .table-controls {
1845 order: 1;
1846 }
1847 .compact .facing {
1848 order: 2;
1849 flex-basis: 100%;
1850 }
1851}
1852
1853/* ---------- the panels ---------- */
1854.compact .lobby {
1855 justify-content: flex-start;
1856 overflow-y: auto;
1857 gap: 0.25rem;
1858 padding: 12px 14px calc(14px + env(safe-area-inset-bottom));
1859}
1860.compact .lobby > * {
1861 flex: none;
1862}
1863/* Sized off both axes: a phone held sideways has width to spare and no height
1864 at all, and the title was the first thing to push the buttons off. */
1865.compact .lobby h1 {
1866 font-size: clamp(1.9rem, min(9vw, 11vh), 3.2rem);
1867 margin-bottom: 0.2rem;
1868}
1869.compact .lobby .sub {
1870 font-size: 0.78rem;
1871}
1872/* The screen dividers this asks for are a four-round-a-table idea, and nobody
1873 props cards against a phone. */
1874.compact .lobby .hint {
1875 display: none;
1876}
1877.compact .resume {
1878 margin-top: 0.8rem;
1879 padding: 8px 16px;
1880}
1881.compact .resume-scores {
1882 flex-wrap: wrap;
1883 gap: 4px 12px;
1884 font-size: 12px;
1885}
1886.compact .lobby-buttons {
1887 margin-top: 0.9rem;
1888 gap: 8px;
1889 width: min(460px, 100%);
1890}
1891.compact .lobby-buttons .big-btn {
1892 min-width: 0;
1893 width: 100%;
1894 padding: 8px 16px;
1895}
1896.compact .lobby-extras {
1897 margin-top: 0.9rem;
1898}
1899/* Sideways there is barely 390px of height for a title, a saved game, three
1900 ways to start and the settings link. Everything gives a little so the last of
1901 them lands above the fold, rather than behind a scroll nobody looks for. */
1902@media (orientation: landscape) {
1903 .compact .lobby h1 {
1904 font-size: clamp(1.6rem, 9vh, 2.6rem);
1905 }
1906 .compact .lobby .sub,
1907 .compact .resume-when {
1908 display: none;
1909 }
1910 .compact .resume {
1911 margin-top: 0.4rem;
1912 padding: 6px 14px;
1913 }
1914 .compact .resume-line {
1915 font-size: 12px;
1916 }
1917 .compact .lobby-buttons {
1918 margin-top: 0.5rem;
1919 }
1920 .compact .lobby-buttons .big-btn {
1921 min-height: 40px;
1922 padding: 5px 16px;
1923 font-size: 19px;
1924 }
1925 .compact .lobby-extras {
1926 margin-top: 0.5rem;
1927 }
1928}
1929
1930.compact .help-slot .help {
1931 width: calc(100vw - 16px);
1932 --hd: calc(100dvh - 16px);
1933 translate: none;
1934 padding: 8px 12px 10px;
1935}
1936.compact .help-head h3 {
1937 font-size: 13px;
1938}
1939/* The strip version fills four columns and scrolls sideways, which reads fine
1940 across a laptop's width and not at all on a phone. One column, scrolled the
1941 way every other sheet on the device is. */
1942.compact .help-body {
1943 columns: auto;
1944 column-gap: normal;
1945 overflow-x: hidden;
1946}
1947
1948.compact .settings-scrim {
1949 padding: 6px;
1950}
1951.compact .settings {
1952 padding: 10px 14px 14px;
1953}
1954.compact .settings-head h3 {
1955 font-size: 16px;
1956}
1957.compact .settings-head .btn {
1958 min-height: 38px;
1959 padding: 2px 14px;
1960 font-size: 14px;
1961}
1962/* Two columns sideways, where there is width and no height; one held upright,
1963 where it is the other way round. */
1964.compact .settings-body {
1965 columns: 2;
1966 column-gap: 26px;
1967 padding-top: 10px;
1968}
1969@media (orientation: portrait) {
1970 .compact .settings-body {
1971 columns: 1;
1972 }
1973}
1974.compact .settings-body section {
1975 margin-bottom: 14px;
1976}
1977.compact .settings-body h4 {
1978 margin-bottom: 6px;
1979 font-size: 14px;
1980}
1981.compact .settings .f-label {
1982 min-width: 116px;
1983 font-size: 14px;
1984}
1985.compact .settings input[type='text'],
1986.compact .settings input:not([type]),
1987.compact .settings input[type='number'] {
1988 font-size: 15px;
1989 min-height: 40px;
1990 padding: 8px 10px;
1991}
1992.compact .settings .sw-name {
1993 font-size: 14px;
1994}
1995.compact .settings .note {
1996 font-size: 11px;
1997}
1998
1999/* ---------- fullscreen ---------- */
2000/* Sized to match the sound toggle it stands beside, in both its forms. */
2001.fs-toggle {
2002 min-height: 34px;
2003 padding: 2px 14px;
2004}
2005.fs-toggle.compact {
2006 min-height: 28px;
2007 padding: 3px 9px;
2008}
2009.fs-toggle.on {
2010 border-color: rgba(217, 180, 92, 0.7);
2011 color: var(--gold);
2012}
2013.fs-toggle .fs-icon {
2014 display: block;
2015 flex: none;
2016}
2017
2018/* ---------- the party table's QR ---------- */
2019/* One join code for the whole table, lying in the middle of the felt like a
2020 tile someone left face up. It may cover the pool while it shows — it steps
2021 aside the moment every hand is held, and comes back while a 📱 tag is held
2022 down. Below the result panel (z-index 2, later in the DOM), above the pool
2023 and the centre readout (1): scannable while it matters, never over the
2024 scores. */
2025.qr-dock {
2026 position: absolute;
2027 left: 50%;
2028 top: 50%;
2029 transform: translate(-50%, -50%);
2030 z-index: 2;
2031 display: flex;
2032 flex-direction: column;
2033 align-items: center;
2034 gap: 8px;
2035 /* Purely something to look at: taps fall through to the table under it. */
2036 pointer-events: none;
2037}
2038/* The QR wears the face of a giant tile: ivory, softly bevelled, the same
2039 rounded corners and resting shadow the sprite gives the real ones, with the
2040 code engraved in the deep green the dragons are cut in. */
2041.seat-qr {
2042 --qw: clamp(96px, calc(var(--depth) * 0.56), 168px);
2043 width: var(--qw);
2044 height: calc(var(--qw) * 1.2);
2045 display: grid;
2046 place-items: center;
2047 padding: calc(var(--qw) * 0.09);
2048 border-radius: calc(var(--qw) * 0.1);
2049 background: linear-gradient(165deg, #fffdf3, #f4edd9 58%, #e6dcbf);
2050 box-shadow:
2051 inset 0 2px 1px rgba(255, 255, 255, 0.85),
2052 inset 0 calc(var(--qw) * -0.05) calc(var(--qw) * 0.04) rgba(148, 130, 90, 0.4),
2053 inset calc(var(--qw) * -0.02) 0 calc(var(--qw) * 0.03) rgba(148, 130, 90, 0.22),
2054 0 6px 12px rgba(0, 0, 0, 0.5);
2055}
2056.seat-qr svg {
2057 display: block;
2058 width: 100%;
2059 height: auto;
2060 aspect-ratio: 1;
2061}
2062.qr-hint {
2063 font-size: 11px;
2064 line-height: 1.35;
2065 color: var(--ivory);
2066 text-align: center;
2067 max-width: 190px;
2068 padding: 5px 12px;
2069 border-radius: 999px;
2070 background: rgba(10, 49, 38, 0.88);
2071 border: 1px solid rgba(217, 180, 92, 0.4);
2072}
2073.qr-hint em {
2074 display: block;
2075 font-style: normal;
2076 opacity: 0.6;
2077 font-size: 9px;
2078}
2079.nameplate .tag.phone {
2080 background: rgba(29, 90, 168, 0.35);
2081 border-color: rgba(29, 90, 168, 0.7);
2082 cursor: pointer;
2083 user-select: none;
2084 touch-action: none;
2085}
2086
2087/* ---------- the phone controller ---------- */
2088/* A hand held in landscape: status, buttons, tiles, in that order down the
2089 screen, with the common screen doing everything else. Tiles get the room —
2090 they are the only thing this screen is for. */
2091.ctl {
2092 /* The whole hand in one row: 17 tiles and their gaps must fit the width. */
2093 --tile-w: clamp(30px, calc((100vw - 56px) / 17.6), 76px);
2094 position: fixed;
2095 inset: 0;
2096 display: flex;
2097 flex-direction: column;
2098 background: radial-gradient(ellipse at center, var(--felt) 0%, var(--felt-2) 80%);
2099 color: var(--ivory);
2100 overflow: hidden;
2101 touch-action: none;
2102}
2103.ctl-top {
2104 display: flex;
2105 align-items: center;
2106 gap: 8px;
2107 padding: 8px 12px 4px;
2108 flex-wrap: wrap;
2109 font-size: 13px;
2110}
2111.ctl-top .wind {
2112 font-weight: 900;
2113 font-size: 18px;
2114 color: var(--gold);
2115}
2116.ctl-top .wind em {
2117 font-style: normal;
2118 font-size: 10px;
2119 margin-left: 2px;
2120 opacity: 0.7;
2121}
2122.ctl-top .pname {
2123 font-weight: 600;
2124}
2125.ctl-top .tag {
2126 border: 1px solid rgba(246, 240, 226, 0.4);
2127 border-radius: 20px;
2128 padding: 1px 8px;
2129 font-size: 11px;
2130}
2131.ctl-top .tag.ting {
2132 border-color: var(--gold);
2133 color: var(--gold);
2134}
2135.ctl-status {
2136 opacity: 0.75;
2137}
2138.ctl-status em {
2139 font-style: normal;
2140 opacity: 0.6;
2141 margin-left: 4px;
2142}
2143.ctl-status.hot {
2144 opacity: 1;
2145 color: var(--gold);
2146 font-weight: 600;
2147}
2148.ctl-last {
2149 display: inline-flex;
2150 align-items: center;
2151 gap: 4px;
2152 margin-left: auto;
2153}
2154.ctl-top .chips {
2155 font-weight: 600;
2156 color: var(--gold);
2157}
2158.ctl-actions {
2159 display: flex;
2160 align-items: center;
2161 justify-content: center;
2162 gap: 8px;
2163 flex-wrap: wrap;
2164 min-height: var(--bar-h);
2165 padding: 2px 10px;
2166}
2167.ctl-hint {
2168 font-size: 11px;
2169 opacity: 0.55;
2170}
2171.ctl-hand {
2172 margin-top: auto;
2173 display: flex;
2174 justify-content: center;
2175 padding: 0 8px calc(10px + env(safe-area-inset-bottom, 0px));
2176}
2177.ctl-hand .hand {
2178 flex-wrap: nowrap;
2179}
2180/* Held upright, seventeen tiles will never make one row. Break into two and
2181 size the tiles off half the count instead of shrinking them past reading.
2182 The row gap leaves room for the 摸 tag over a tile in the lower row. */
2183@media (orientation: portrait) {
2184 .ctl {
2185 --tile-w: clamp(34px, calc((100vw - 44px) / 9.4), 76px);
2186 }
2187 .ctl-hand .hand {
2188 flex-wrap: wrap;
2189 justify-content: center;
2190 /* Bounded, or it stays one long row: a `flex: none` item is happy to
2191 overflow its container rather than wrap inside it. */
2192 width: 100%;
2193 height: auto;
2194 row-gap: calc(var(--tile-w) * 0.4);
2195 }
2196}
2197/* The tile in the fingers once it has left the row, and its exit: flung, it
2198 keeps going up off the screen — the common screen takes it from here. */
2199.ctl-held {
2200 position: fixed;
2201 z-index: 5;
2202 pointer-events: none;
2203 transform: translate(-50%, -50%);
2204}
2205.ctl-held .tile {
2206 width: 100%;
2207 height: 100%;
2208}
2209.ctl-held.flung {
2210 animation: fling-up 0.3s ease-in forwards;
2211}
2212@keyframes fling-up {
2213 to {
2214 transform: translate(-50%, -140vh) rotate(18deg);
2215 opacity: 0.9;
2216 }
2217}
2218.ctl-result {
2219 position: fixed;
2220 left: 50%;
2221 top: 44%;
2222 transform: translate(-50%, -50%);
2223 z-index: 6;
2224 background: rgba(10, 49, 38, 0.96);
2225 border: 1px solid rgba(217, 180, 92, 0.5);
2226 border-radius: 14px;
2227 padding: 16px 22px;
2228 text-align: center;
2229 max-width: min(92vw, 480px);
2230}
2231.ctl-result h2 {
2232 margin: 0 0 8px;
2233 font-size: 18px;
2234}
2235.ctl-result .deltas {
2236 display: flex;
2237 gap: 12px;
2238 flex-wrap: wrap;
2239 justify-content: center;
2240 margin-bottom: 12px;
2241 font-size: 13px;
2242}
2243.ctl-result .deltas .up {
2244 color: var(--gold);
2245}
2246.ctl-result .deltas .down {
2247 color: #e08a84;
2248}
2249
2250/* Seat picking, on the phone and in the online lobby. */
2251.ctl-pick {
2252 align-items: center;
2253 justify-content: center;
2254 text-align: center;
2255 overflow-y: auto;
2256 touch-action: pan-y;
2257}
2258.pick-grid {
2259 display: grid;
2260 grid-template-columns: repeat(2, minmax(150px, 220px));
2261 gap: 10px;
2262 justify-content: center;
2263 margin: 12px 0;
2264}
2265.pick-grid .btn em {
2266 display: block;
2267 font-style: normal;
2268 font-size: 11px;
2269 opacity: 0.65;
2270 margin-top: 2px;
2271}