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