anvilsign in

collin/mahjong

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