anvilsign in

collin/mahjong

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