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