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