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