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