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