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