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