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