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