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