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