anvilsign in

collin/mahjong

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