anvilsign in

collin/mahjong

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