anvilsign in

collin/mahjong

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