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