/* ============================================================================
   The port layer.

   This is the only stylesheet under `app/client/` that is not the captain's
   file, and it exists so that the other six can stay byte-identical: the
   design's identity stays auditable against `frontend-tak`, line for line,
   while the port gets the elements `GameAPIv1` needs and the imported design
   has no home for.

   It is loaded last, it is purely additive, and it introduces no colour, no
   type family and no radius of its own: every value below is a token from
   `design/tak/tokens.css` or a step of `game.css`'s own vertical rhythm
   (--k1..--k6). Where a token is not in scope on a page - the lobby does not
   load `game.css` - the fallback in `var(..., x)` is that token's own value.

   Each numbered section says what the design does, what `GameAPIv1` needs on
   top of it, and why. The numbers are the ones `lobby.js` and `game/game.js`
   cite next to the element they build.
   ========================================================================= */

/* Both pages budget the visible browser viewport. Dynamic units follow browser
   bars opening/closing; safe-area padding is deducted by each page once. */
:root { --app-vh: 100dvh; --app-vw: 100dvw; }

/* --- 0 · `hidden` means hidden ---------------------------------------------
   Every element this layer adds is shown and hidden with the `hidden`
   attribute, and every one of them is given a `display` here - which wins over
   the user-agent's `[hidden]{display:none}` on specificity alone. Stating it
   once is the whole fix; the alternative is a class per element that says the
   same thing twice. */

.pt-bar[hidden],
.pt-wait[hidden],
.pt-say[hidden],
.pt-exposed[hidden],
.pt-ct-mult[hidden],
.pt-notice[hidden] { display: none; }

/* --- 1 · The lobby's notice line -------------------------------------------
   The imported lobby never called a server, so it has no element for a
   transport failure or one of the nine typed refusals. Creating a room can
   fail; the player has to be told which way, in the panel they pressed. */

#lobbyNotice.pt-notice {
  margin: calc(9px + 2 * var(--g, 0px)) 0 0;
  padding: var(--k2, 8px) var(--k3, 12px);
  border-radius: var(--r-btn-sm, 13px);
  background: rgba(var(--sh-rgb, 4, 22, 13), .28);
  box-shadow: inset 0 0 0 1px rgba(240, 138, 128, .34);
  color: var(--tx2, rgba(242, 236, 221, .8));
  font-family: var(--fu);
  font-size: .82rem;
  line-height: 1.5;
  text-align: center;
}

/* --- 2 · The table's notice bar --------------------------------------------
   The same gap on the table page, plus the one thing the table can do about
   it: ask the authority again. It sits under the top bar rather than over the
   mat, because the mat is where the player is looking and a bar that covers a
   card is worse than the fault it reports. */

.pt-bar {
  position: fixed;
  inset-block-start: calc(var(--sa-t, 0px) + 52px);
  inset-inline: var(--k3, 12px);
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--k3, 12px);
  padding: var(--k2, 8px) var(--k3, 12px);
  border-radius: var(--r-btn-sm, 13px);
  background: linear-gradient(180deg, var(--s-panel-hi, #424a5b), var(--s-panel-lo, #363c4d));
  box-shadow: var(--sh3, 0 2px 4px rgba(4, 22, 13, .48));
  color: var(--tx, #f2ecdd);
  font-family: var(--fu);
  font-size: .79rem;
  line-height: 1.45;
}

.pt-bar[data-tone="bad"] {
  box-shadow: var(--sh3, 0 2px 4px rgba(4, 22, 13, .48)), inset 0 0 0 1px rgba(240, 138, 128, .5);
}

.pt-bar-copy { flex: 1 1 auto; text-align: start; }

.pt-bar-act {
  flex: 0 0 auto;
  /* Apple's 44pt floor; the bar is the one control that appears when the rest
     of the page has stopped answering, so it is the one that must be hittable
     on the first try. */
  min-block-size: 44px;
  padding: 0 var(--k3, 12px);
  border: 0;
  border-radius: var(--r-btn-sm, 13px);
  background: linear-gradient(180deg, var(--a-hi, #fbd549), var(--a-lo, #d1a416));
  color: var(--ink, #1b140c);
  font-family: var(--fu);
  font-size: .79rem;
  font-weight: var(--w-control, 800);
  cursor: pointer;
}

/* --- 3 · The room a partner joins ------------------------------------------
   A duo room is dealt and closed until its partner sits down, and in
   `GameAPIv1` the room code *is* the invitation. The imported design has no
   room-code panel on either page - its own backend put two humans together
   some other way - so the port shows it on the table, which is where the
   player who pressed **أنت والخوي** already is. The panel takes a third grid
   row across the whole table rather than a cell, because a code that is
   ellipsised is a code that cannot be read aloud. */

.pt-wait {
  grid-column: 1 / -1;
  grid-row: 3;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--k3, 12px);
  padding: var(--k5, 24px) var(--k4, 16px);
  text-align: center;
}

/* The two agent seats and the mat are hidden while the room is held, and so is
   the partner's fan: the server deals a duo room when it creates it, so those
   cards are real, but a fan above a seat nobody is sitting in reads as a hand
   already being played. The seat's own name stays, because it is who the panel
   below is waiting for. */
body.is-waiting .gm-trick,
body.is-waiting .gm-backs,
body.is-waiting .gm-seat-right,
body.is-waiting .gm-seat-left { visibility: hidden; }

.pt-wait-title {
  margin: 0;
  font-family: var(--fu);
  font-size: 1.266rem;
  font-weight: var(--w-display, 700);
  color: var(--tx, #f2ecdd);
}

.pt-wait-status {
  margin: 0;
  font-family: var(--fu);
  font-size: .79rem;
  color: var(--tx3, rgba(242, 236, 221, .78));
}

.pt-wait-code {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--k1, 4px);
  padding: var(--k3, 12px) var(--k5, 24px);
  border-radius: var(--r-plate, 20px);
  background: linear-gradient(180deg, var(--s-well-hi, #141622), var(--s-well-lo, #0e0f18));
  box-shadow: var(--well, inset 0 2px 5px rgba(4, 22, 13, .55));
}

.pt-wait-code-k {
  font-family: var(--fu);
  font-size: .702rem;
  color: var(--tx3, rgba(242, 236, 221, .78));
}

.pt-wait-code-v {
  font-family: var(--fu);
  font-size: 1.9rem;
  font-weight: var(--w-lead, 900);
  letter-spacing: .18em;
  color: var(--a, #ecc330);
}

/* --- 4 · The doubling rung -------------------------------------------------
   `contract.*` names the play and `doubling.*` names the rung *and* its
   openness, and a raised contract rendered as its base is the exact legacy
   defect the engine substitution removes. The design's contract line has two
   slots (the play, and its suit); this is the third, and it appears only when
   the rung is above base. */

.pt-ct-mult {
  margin-inline-start: var(--k1, 4px);
  font-family: var(--fu);
  font-size: .702rem;
  font-weight: var(--w-control, 800);
  font-style: normal;
  color: var(--a, #ecc330);
  white-space: nowrap;
}

/* --- 5 · What a seat said --------------------------------------------------
   SaikalCore's auction is a ladder - hokom, sun, ashkal, qablik, the doubling
   rungs - and a project is announced before it is shown. The imported design's
   backend had three bid buttons and no projects, so no seat there ever spoke.
   The bubble is the Unified Base's behaviour: what a seat said stays above it
   until the next thing happens there.

   It grows *downward* from the seat on the top row and upward on the sides,
   because the top seat sits just under the top bar and a bubble that grows up
   from it is drawn through that bar. */

.pt-say {
  position: absolute;
  z-index: 6;
  inset-inline-start: 50%;
  transform: translateX(-50%);
  /* `max-content`, not shrink-to-fit: a seat is only as wide as the fan it
     holds - 58px on the sides - so an auto-width absolute box inside it wraps
     "طلب حكم ♠" into three lines. The bubble is allowed to be wider than the
     seat it belongs to; that is what makes it readable. */
  inline-size: max-content;
  max-inline-size: min(62vw, 260px);
  padding: var(--k1, 4px) var(--k2, 8px);
  border-radius: var(--r-btn-sm, 13px);
  background: linear-gradient(180deg, var(--s-lift-hi, #565f6d), var(--s-lift-lo, #4f5766));
  box-shadow: var(--sh2, 0 1px 2px rgba(4, 22, 13, .46));
  color: var(--tx, #f2ecdd);
  font-family: var(--fu);
  font-size: .702rem;
  font-weight: var(--w-control, 800);
  line-height: 1.4;
  text-align: center;
  pointer-events: none;
}

.gm-seat { position: relative; }
.gm-seat-top .pt-say { inset-block-start: calc(100% + var(--k1, 4px)); }

/* The two side seats sit against the edges of the screen, so their bubbles grow
   *inward* rather than from their own centre - centred on a 58px seat, a 180px
   bubble would hang half off the glass. In RTL `.gm-seat-right` is the column
   on the right, so pinning its inline-end edge sends it toward the mat. */
.gm-seat-right .pt-say,
.gm-seat-left .pt-say {
  inset-block-end: calc(100% + var(--k1, 4px));
  transform: none;
  max-inline-size: min(42vw, 180px);
}

.gm-seat-right .pt-say { inset-inline-start: auto; inset-inline-end: 0; }
.gm-seat-left .pt-say { inset-inline-start: 0; inset-inline-end: auto; }

.pt-say-cards {
  display: flex;
  justify-content: center;
  gap: 2px;
  margin-block-start: var(--k1, 4px);
}

.pt-say-cards > img {
  inline-size: calc(var(--card-w, 60px) * .34);
  border-radius: 3px;
}

/* --- 6 · المكشوف -----------------------------------------------------------
   The exposed card is a rule, not an ornament: the whole first auction is a
   decision about it. The design's trick grid is 3x3 with the four slots on the
   arms, so its centre cell is free and is exactly where the exposed card
   belongs - inside the mat, between the four seats that are bidding on it. */

.pt-exposed {
  grid-column: 2;
  grid-row: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}

.pt-exposed-k {
  font-family: var(--fu);
  font-size: .58rem;
  color: var(--tx3, rgba(242, 236, 221, .78));
}

.pt-exposed > .card {
  inline-size: calc(var(--card-w, 60px) * .56);
  block-size: calc(var(--card-h, 84px) * .56);
}

/* --- 7 · A rail wider than four -------------------------------------------
   The design sizes the action rail for at most four buttons, because its
   backend never advertised more. SaikalCore does: the doubling ladder and the
   project window both routinely offer five or six candidates, and every one of
   them is a decision a player is entitled to take. The rail wraps rather than
   scrolls, and it stays above the 44pt floor at every count. */

.gm-act { flex-wrap: wrap; row-gap: var(--k2, 8px); }
/* صكاتي's way in on a match result (docs/majlis/my-games.md): «شوف الصكة»
   beside «صكة جديدة» makes three buttons, which a narrow phone cannot set in one
   row without wrapping their words; there the new match keeps the first row. */
@media (max-width: 389.98px) {
  body.gm.has-outcome .gm-act:has(> .pt-history) > .btn-1 { flex: 1 0 100%; }
}
.gm-act[data-count="5"] > .btn,
.gm-act[data-count="6"] > .btn,
.gm-act[data-count="7"] > .btn,
.gm-act[data-count="8"] > .btn {
  min-inline-size: 0;
  flex: 1 1 calc(33% - var(--k2, 8px));
  padding: 0 var(--k2, 8px);
  font-size: .889rem;
}

/* A refusal is a rung, not an error: it takes the design's own quiet body with
   the suit red on its edge, never a red fill. */
.pt-btn-danger { box-shadow: inset 0 0 0 1px rgba(240, 138, 128, .46), 0 1px 2px rgba(0, 0, 0, .4); }

/* --- 8 · The end of a hand -------------------------------------------------
   The design's own `#gmOver` cluster, kept exactly where its note puts it: a
   third grid row across the whole table, so the two numbers are never squeezed
   into the trick's centre column. */

.pt-over {
  grid-column: 1 / -1;
  grid-row: 3;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--k2, 8px);
  text-align: center;
  white-space: nowrap;
}

.pt-over > .gm-hint,
.pt-over > .gm-turn { margin: 0; }
.pt-over > .gm-turn { font-size: 1.052rem; letter-spacing: .02em; }

.pt-over .gm-over {
  margin: 0;
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: var(--k4, 16px);
}

.pt-over .gm-sc-v {
  display: inline-block;
  font-size: 1.9rem;
}

.pt-over .gm-sc-v.is-dim { color: var(--tx3, rgba(242, 236, 221, .78)); }

/* When the outcome cluster is up, the empty hand box is not worth a fifth of
   the screen - the design's own note measures it at 222.3px. `game.js` shrinks
   it; this keeps the rail from sliding under the home indicator while it is. */
body.has-outcome .gm-mine { padding-block-end: 0; }

/* --- 9 · A word that does not need a surface ------------------------------- */

.pt-toast {
  position: fixed;
  inset-inline: 0;
  inset-block-end: calc(var(--sa-b, 0px) + 76px);
  z-index: 50;
  margin-inline: auto;
  inline-size: fit-content;
  max-inline-size: 86vw;
  padding: var(--k2, 8px) var(--k4, 16px);
  border-radius: var(--r-pill, 999px);
  background: linear-gradient(180deg, var(--s-panel-hi, #424a5b), var(--s-panel-lo, #363c4d));
  box-shadow: var(--sh3, 0 2px 4px rgba(4, 22, 13, .48));
  color: var(--tx, #f2ecdd);
  font-family: var(--fu);
  font-size: .79rem;
  text-align: center;
  pointer-events: none;
}

/* --- 10 · Who deals, and who has gone quiet --------------------------------
   Two facts the API publishes per seat that the imported design has no mark
   for. Presence is advisory and changes no rule - a partner who has gone quiet
   keeps the seat, the turn and the hand - so it dims the disc rather than
   labelling it, and the dealer takes the same node the design already draws. */

.gm-seat[data-dealer="1"] .gm-node {
  box-shadow: 0 0 0 2px rgba(var(--a-rgb, 224, 168, 60), .55);
}

.gm-seat[data-away="1"] .gm-who { opacity: .55; }

/* The violet is the agent's mark alone. A seat a human holds carries no dot -
   the design's own note calls a violet ground "a killed signal", and a dot on
   every seat is the same mistake spread thinner. */
.gm-seat[data-role="human"] .gm-node { display: none; }

/* --- 11 · The lobby's wait -------------------------------------------------
   `createRoom` deals a hand and drives three automated seats before it
   answers, so on a phone the press is followed by seconds of nothing. The
   pressed button says so in its own subtitle - `lobby.js` swaps the text - and
   this is what makes the swap read as work rather than as a different label. */

.tkb.is-busy .tkb-s {
  background: linear-gradient(90deg,
    var(--tx3, rgba(242, 236, 221, .78)) 0%,
    var(--tx, #f2ecdd) 50%,
    var(--tx3, rgba(242, 236, 221, .78)) 100%) 0 0 / 220% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

@media (prefers-reduced-motion: no-preference) {
  .tkb.is-busy .tkb-s { animation: pt-sweep 1.4s linear infinite; }
}

@keyframes pt-sweep {
  from { background-position: 220% 0; }
  to { background-position: -220% 0; }
}

.tkb[disabled] { cursor: progress; }

/* --- 12 · Behaving like an app --------------------------------------------
   The four things iOS Safari does that an app must not: rubber-band the table
   under the player's thumb, select the Arabic labels on a long press, offer to
   save a card face because the card is an image, and flash a tap highlight on
   a control that already has a pressed state. The design's own `viewport-fit`
   and `env(safe-area-inset-*)` budget is untouched: it is the single opinion
   about the notch, and this adds no second one. */

html, body {
  overscroll-behavior: none;
  -webkit-tap-highlight-color: transparent;
}

body.gm {
  /* A drag on the board must not peel the page off the top of the screen. That
     is `overscroll-behavior` above, and it is the whole fix: the table is *not*
     `overflow:hidden`, because forbidding the scroll does not make the content
     fit - it makes the overflow land on top of the top bar instead. Where the
     board genuinely does not fit, §13 lets it scroll, which is the captain's
     own ruling for the lobby in the same position: "a shorter scroll is not
     zero, and zero here cannot be bought without breaking a law". */
  touch-action: manipulation;
}

.gm-seat, .gm-who, .gm-turn, .gm-hint, .btn, .gm-sc, .gm-deal, .pt-say, .pt-wait {
  -webkit-user-select: none;
  user-select: none;
}

.card > .face, .gm-backs > i, .pt-say-cards > img {
  -webkit-touch-callout: none;
  pointer-events: none;
}

/* A card is the primary control of the play phase, so it is the one target
   that must never fall under the floor. `--card-w` bottoms out at 52px, which
   clears 44 on its own; this is the hit area around it, which the fan's
   negative tracks otherwise eat. */
.gm-hand > .card[data-ok="1"] { touch-action: manipulation; }

/* --- 13 · One design on every viewport -------------------------------------
   The desktop is not this layer's business, and that is deliberate: the
   captain's own `game.css` already answers a wide viewport at `min-width:520px`
   - the table becomes a phone-width column that centres, with the room spread
   around it - and its note says why ("the game is phone-first by intent, but
   opening it on a big screen has to read as a choice, not a defect"). One
   design everywhere is that rule, not a second layout, so nothing here widens
   it.

   What a phone turned sideways does is §36. This section's own answer - "what
   does not fit is scrolled to" - was this repository's reading of the captain's
   lobby ruling, and the captain replaced it on 2026-08-20. Nothing is left
   here, because there is nothing left to say: a wide viewport is already
   answered by a captain file. */

/* ============================================================================
   ROUND 1 · what three read-only audits found, fixed here
   ============================================================================
   Round 1 of the captain's design iteration. Three auditors scored the live
   build - visual fidelity 4.8/4.5, ergonomics 6.8, smoothness 6.7, loading
   6.5, asset weight 6.8 - and every numbered item below closes one of their
   ranked defects. The two calls they registered as the captain's are *not*
   here and are not worked around: replacing the card-face and mat art
   (visual D1/D2's art half) and what landscape should do (ergonomics D1).

   Still additive, still no captain file edited.
   ========================================================================= */

/* --- 14 · The mat holds its cards ------------------------------------------
   Visual D1, D7 · ergonomics D3.

   Measured: a 178x223 ornate frame holding four 46x61 cards, so 70% of it is
   empty with a full trick down and ~95% during the auction - "the single
   ugliest element in the app". And المكشوف, the card the whole first auction
   is a decision about, rendered 37x41 (27x25 on an SE) among cards of 66x92.

   The art is the captain's and is not touched. What is wrong is the *ratio*
   between the frame and what sits in it, and that is arithmetic: the played
   cards go from 0.64 of a card to 0.86, the exposed card from 0.56 to 0.92,
   and the frame's own padding drops from 0.30 of a card width to 0.12. The
   frame ends up holding cards instead of framing a hole. */

.gm-trick { padding: calc(var(--card-w) * .12); }
.gm-slot > .card {
  inline-size: calc(var(--card-w) * .86);
  block-size: calc(var(--card-h) * .86);
}
/* One aspect ratio for every card on the table. The audit measured three
   (0.714 hand, 0.753 played, 0.780 backs) - the last two are rounding at small
   sizes, and the cure for rounding is size. */
/* Scoped away from `.gm-backs-v`: the side seats hold their cards lying down,
   so their backs are the same rectangle turned, and an unscoped rule here stood
   them upright and ran the two columns off the mat. */
.gm-backs:not(.gm-backs-v) > i { block-size: calc(var(--back-w) / 0.714); }

/* The centre cell is one fixed grid row, so a card taller than the row was
   being compressed to a 60x28 sliver. It centres in the cell and keeps its own
   size instead - which is the whole point: this is the card the auction is
   about, and it now overflows the row the way a card laid on a mat would. */
.pt-exposed { align-self: center; justify-self: center; }
.pt-exposed > .card {
  inline-size: calc(var(--card-w) * .92);
  block-size: calc(var(--card-h) * .92);
  min-inline-size: calc(var(--card-w) * .92);
  min-block-size: calc(var(--card-h) * .92);
}
.pt-exposed-k {
  font-size: .702rem;
  color: var(--tx2, rgba(242, 236, 221, .8));
  letter-spacing: .04em;
}

/* --- 15 · The trick grid stops reserving room for nothing -------------------
   The 3x3 grid sized its columns and rows at a fixed fraction of a card, so
   the frame was the same size whether it held four cards or none. It now
   tracks what is in it. */
.gm-trick {
  grid-template-columns: repeat(3, calc(var(--card-w) * .62));
  grid-template-rows: repeat(3, calc(var(--card-w) * .80));
}

/* --- 16 · The table stops jumping 180px at every hand boundary --------------
   Ergonomics D2, the worst smoothness defect: 415 layout-shift entries in one
   match, a worst 1s burst of 0.200, because `.gm-mine` collapses 225px -> 24px
   between hands and `.gm-act` toggles 31 <-> 72px, moving the whole table.

   Both boxes now hold their height. The rail keeps its own minimum whether or
   not it has buttons, and the hand keeps a floor the size of one card, so the
   only thing that changes between hands is what is drawn inside them. */
.gm-act.is-empty {
  min-block-size: calc(56px + 9 * var(--g));
  padding: var(--k3, 12px);
}
.gm-mine { min-block-size: calc(var(--card-h) + var(--k5, 24px)); }
/* `game.js` collapses `.gm-mine` while a result is up, which is the design's
   own note about the 222px hole under an empty hand. Keeping the floor there
   would put the hole back, so the outcome panel is what fills it instead. */
body.has-outcome .gm-mine { min-block-size: 0; }

/* --- 17 · The result is a panel, not text on wallpaper ---------------------
   Visual D3, the third blocker: "bare centred text floating on the wallpaper,
   no panel, no elevation, no container", and a match win that "looks identical
   to a routine hand interstitial".

   It takes the lobby's own card language - the one screen the captain approved
   - which is `--s-panel` between `--r-panel` corners over `--sh4`. A completed
   match additionally gets the gold edge the lobby gives its primary action,
   and its numbers step up a size. Nothing here is a new colour. */
.pt-over {
  /* The whole table, centred in it - not a third row pinned under an empty
     mat. With the seats and the mat receded (below) the result is the only lit
     thing on the screen, which is what makes it read as a stop. */
  grid-row: 1 / -1;
  align-self: center;
  justify-self: stretch;
  gap: var(--k3, 12px);
  padding: var(--k5, 24px) var(--k4, 16px) var(--k4, 16px);
  margin-inline: var(--k4, 16px);
  border-radius: var(--r-panel, 22px);
  background: linear-gradient(180deg, var(--s-panel-hi, #424a5b), var(--s-panel-lo, #363c4d));
  box-shadow: var(--sh4, 0 3px 6px rgba(4, 22, 13, .52)), var(--rim, none);
}
.pt-over[data-kind="match"] {
  box-shadow: var(--sh4, 0 3px 6px rgba(4, 22, 13, .52)),
              inset 0 0 0 1px rgba(var(--a-rgb, 224, 168, 60), .55),
              0 0 34px rgba(var(--a-rgb, 224, 168, 60), .14);
}
.pt-over > .gm-hint {
  font-size: .79rem;
  color: var(--tx3, rgba(242, 236, 221, .78));
  letter-spacing: .06em;
}
.pt-over > .gm-turn { font-size: 1.266rem; font-weight: var(--w-lead, 900); }
.pt-over[data-kind="match"] > .gm-turn {
  font-size: 1.6rem;
  color: var(--a, #ecc330);
}
.pt-over[data-kind="match"] .gm-sc-v { font-size: 2.4rem; }

/* The seats and their empty fans are not part of a result. */
body.has-outcome .gm-backs,
body.has-outcome .gm-trick,
body.has-outcome .gm-seat .gm-who { visibility: hidden; }

/* --- 18 · The waiting room is not 55% empty wallpaper ----------------------
   Visual D4, the fourth blocker. The panel now sits in the middle of the
   table's own grid rather than in a third row under it, and it says the room
   is doing something: the code plate breathes on the same slow pulse the
   design uses for a live seat. */
.pt-wait {
  grid-row: 1 / -1;
  align-self: center;
  gap: var(--k4, 16px);
  padding: var(--k5, 24px) var(--k4, 16px);
  margin-inline: var(--k4, 16px);
  border-radius: var(--r-panel, 22px);
  background: linear-gradient(180deg, var(--s-panel-hi, #424a5b), var(--s-panel-lo, #363c4d));
  box-shadow: var(--sh4, 0 3px 6px rgba(4, 22, 13, .52));
}
body.is-waiting .gm-seat-top { visibility: hidden; }
.pt-wait-code { position: relative; }
@media (prefers-reduced-motion: no-preference) {
  .pt-wait-code::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    box-shadow: 0 0 0 1px rgba(var(--a-rgb, 224, 168, 60), .45);
    animation: pt-breathe 2.4s var(--ease-out, ease) infinite;
    pointer-events: none;
  }
}
@keyframes pt-breathe {
  0%, 100% { opacity: .25; transform: scale(1); }
  50% { opacity: .9; transform: scale(1.02); }
}

/* --- 19 · The header survives three digits ---------------------------------
   Visual D6 · the two-line `الشراء الأول` colliding with the score chips, and
   `لنا 151 / لهم 61` with a `دبل ×2` chip running into one strip.

   The two score chips stop being flexible and the contract block takes what is
   left, one line, ellipsised rather than wrapped. The multiplier gets its own
   chip body so it reads as a thing rather than as more text. */
.gm-top { column-gap: var(--k2, 8px); }
.gm-sc { flex: 0 0 auto; }
.gm-deal { flex: 1 1 auto; min-inline-size: 0; }
/* The collision the audit measured was the score chips being *pushed*, so the
   chips are what stops flexing; the contract keeps its two lines rather than
   being truncated to `اخترا…`, which is worse than the wrap it replaced. Only
   the buyer's line - a name, and the one unbounded string here - ellipsises. */
.gm-deal-1 { line-height: 1.15; }
.gm-deal-2 {
  display: block;
  max-inline-size: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.pt-ct-mult {
  padding: 1px var(--k1, 4px);
  border-radius: 999px;
  background: rgba(var(--a-rgb, 224, 168, 60), .16);
  box-shadow: inset 0 0 0 1px rgba(var(--a-rgb, 224, 168, 60), .38);
}

/* --- 20 · An unplayable card looks unplayable ------------------------------
   Visual D11: the design dims an illegal card by `brightness(.93)` - a 7%
   change the audit called imperceptible, so "unplayable cards look fully
   playable". It is a card face on a lit table; it needs to leave the light. */
/* Only in the phase where playing a card is the move. During the auction and
   the project window the whole hand is inert by design - it is not a hand full
   of illegal cards, it is a hand nobody is being asked to play from - and
   dimming all eight there just makes the screen look dead.

   ROUND 2 narrowed it further, for the same reason it was scoped in the first
   place: `data-ok` is `0` on *every* card while another seat is thinking, so
   inside `PLAY_TRICKS` this dimmed the whole hand for the three beats out of
   four that are not yours - which is the screen looking dead again, only for
   longer. The comparison a dim is for exists only while you are choosing, so
   that is when it is drawn. */
body[data-phase="PLAY_TRICKS"].is-your-turn .gm-hand > .card[data-ok="0"] {
  filter: brightness(.62) saturate(.55);
  opacity: .78;
}

/* --- 21 · The two header controls are controls -----------------------------
   Visual D22: exit and mute are "bare glyphs with no button surface, no
   hit-target chrome, and no hover/press state". They keep their 44pt box and
   gain the design's own quiet body. */
.gm-exit, .gm-mute {
  border-radius: var(--r-btn-sm, 13px);
  background: linear-gradient(180deg, var(--s-rise-hi, #4f5766), var(--s-rise-lo, #48505f));
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .07), 0 1px 2px rgba(0, 0, 0, .35);
  transition: transform var(--t-down, 80ms) var(--ease-out, ease),
              filter var(--t-tint, 200ms) linear;
}
.gm-exit:active, .gm-mute:active { transform: translateY(1px); filter: brightness(1.12); }

/* --- 22 · The confirm hint is a thing you can see --------------------------
   Visual D21: `اضغط مرة ثانية للعب` is "small grey text on the extreme bottom
   edge with no button treatment - easy to miss entirely". It is the one
   instruction between a player and their move. */
.gm-act.is-empty .gm-hint.pt-confirm {
  color: var(--ink, #1b140c);
  background: linear-gradient(180deg, var(--a-hi, #fbd549), var(--a-lo, #d1a416));
  border-radius: 999px;
  padding: var(--k1, 4px) var(--k4, 16px);
  font-size: .79rem;
  font-weight: var(--w-control, 800);
  min-block-size: 0;
}

/* --- 23 · The hand is inset like everything else ---------------------------
   Visual D14: the fan spans x=2 to x=388 of a 390px viewport - "effectively
   zero gutter, while every other element on the page is inset". */
.gm-hand { padding-inline: var(--k3, 12px); }

/* --- 24 · The smallest labels come up off the floor ------------------------
   Ergonomics D8: لنا / لهم, the seat names, the dealer line and the result's
   own label are all 12.6px Arabic over a woven ground. Contrast is fine
   (9-14:1); size is not. One step up the design's own type scale. */
.gm-sc-k, .gm-hint, .gm-deal-2 { font-size: .79rem; }
.gm-seat-top .gm-name, .gm-seat-top .gm-role { font-size: .79rem; }
/* The side seats are capped at 58px by the design because every pixel they take
   they take from the mat. §14 gave the mat its pixels back by tightening the
   frame instead, so the two names can be read without being cut to `سيك…`. */
.gm-seat-right, .gm-seat-left { max-inline-size: 78px; }
.gm-seat-right .gm-who, .gm-seat-left .gm-who {
  font-size: .702rem;
  max-inline-size: 78px;
}

/* --- 25 · The trick's winner is shown on the trick -------------------------
   Visual D20: the winner is announced only as a text line below the mat, so
   "the eye never learns who took the trick". The four cards lift toward the
   seat that took them, which is the thing that already happens a beat later. */
.gm-slot[data-won="1"] > .card {
  box-shadow: 0 0 0 2px var(--a, #ecc330), 0 6px 18px rgba(0, 0, 0, .5);
  transition: box-shadow var(--t-tint, 200ms) linear;
}

/* --- 26 · An outage does not make the dead rail look primary ---------------
   Ergonomics D6: while the server is unreachable the rail is disabled but
   still drawn with the gold outline, so it reads as *more* prominent than
   usual. And the notice covered the top bar outright. */
#act .btn[disabled] { filter: grayscale(.7) brightness(.7); box-shadow: none; }
.pt-bar { inset-block-start: calc(var(--sa-t, 0px) + 4px); }

/* --- 27 · The design file's example values never reach a screen -------------
   Visual D19. `game/index.html` is an inert import and ships a played hand in
   its markup: `لنا 84`, `لهم 62`, contract `حكم ♥`, buyer `سيكل 1`. `game.js`
   clears them, but only once the module has run - which on a cold cellular
   open is seconds of a first-time player looking at somebody else's score.

   CSS gets there first: this stylesheet is already render-blocking, so the
   values are hidden from the very first paint and revealed by the class the
   binding sets when it has real ones. `visibility`, not `display`, so nothing
   moves when they arrive. */
body.gm:not(.pt-live) .gm-sc-v,
body.gm:not(.pt-live) .gm-deal,
body.gm:not(.pt-live) .gm-turn { visibility: hidden; }

/* --- 28 · Leaving asks first ------------------------------------------------
   Ergonomics D4 / visual D12. The question takes the action rail, where the
   thumb already is. `اترك` carries the suit red on its edge - the design's own
   danger tone - and `أكمل` is the primary, so the safe answer is the gold one. */
.gm-act .pt-ask {
  flex: 1 0 100%;
  margin: 0 0 var(--k1, 4px);
  font-size: .79rem;
  color: var(--tx, #f2ecdd);
}

/* --- 29 · The lobby's small finish -----------------------------------------
   Visual D10, D23, D24, D27 - each small, each the kind of thing a first-time
   player notices once and a returning player stops seeing.

   D10: the notice was a plain grey rectangle with no error colour, no icon and
   none of the panel's language, for a message that was also wrong.
   D23: `ادخل` was near-white, a value that appears nowhere else in the palette.
   D24: the empty ledger printed three literal em-dashes.
   D27: the name field was a borderless dark rectangle with no affordance. */

#lobbyNotice.pt-notice {
  display: flex;
  align-items: flex-start;
  gap: var(--k2, 8px);
  text-align: start;
  background: linear-gradient(180deg, rgba(240, 138, 128, .14), rgba(240, 138, 128, .07));
  box-shadow: inset 0 0 0 1px rgba(240, 138, 128, .45), var(--sh1, 0 1px 2px rgba(4, 22, 13, .45));
  color: var(--tx, #f2ecdd);
}
#lobbyNotice.pt-notice::before {
  content: "!";
  flex: 0 0 auto;
  inline-size: 20px;
  block-size: 20px;
  border-radius: 999px;
  background: var(--suit-dark, #f08a80);
  color: var(--s-void, #0a0b12);
  font-weight: var(--w-lead, 900);
  font-size: .79rem;
  line-height: 20px;
  text-align: center;
}

/* The quiet button takes the design's own quiet body instead of near-white. */
.tkb.tkb-q, .roomfold > summary.tkb-q {
  background: linear-gradient(180deg, var(--s-rise-hi, #4f5766), var(--s-rise-lo, #48505f));
  color: var(--tx, #f2ecdd);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .07), 0 1px 2px rgba(0, 0, 0, .4);
}

/* An empty ledger says it is empty. Three em-dashes read as a rendering fault. */
.lg-cell .n[data-empty="1"] {
  color: var(--tx4, rgba(242, 236, 221, .46));
  font-size: .79rem;
  letter-spacing: .04em;
}

/* The name field is a field before it is focused. */
.tkfield-in {
  box-shadow: var(--well, inset 0 2px 5px rgba(4, 22, 13, .55)),
              inset 0 0 0 1px rgba(255, 255, 255, .09);
}
.tkfield-in:focus-visible { box-shadow: var(--well, none), inset 0 0 0 2px var(--a, #ecc330); }

/* --- 30 · A card on the smallest phone has room to be missed ---------------
   Ergonomics D9: with eight legal cards at 375 px the narrowest clickable strip
   measured 44.0 px - on Apple's floor with zero headroom, and only survivable
   because play is two taps. The fan opens a little at the narrowest viewport,
   which costs a few pixels of overlap and buys back the margin. */
@media (max-width: 380px) {
  body.gm { --card-w: clamp(50px, max(13.4vw, 7.4svh), 62px); }
  .gm-hand { padding-inline: var(--k2, 8px); }
}

/* --- 31 · An empty mat stops demanding the eye -----------------------------
   Visual D1's other half. Sizing the frame to what it holds (§14) fixes the
   full trick; for most of a hand the mat holds nothing at all, and a lit gold
   frame around nothing is exactly what the audit called "the single ugliest
   element in the app".

   It is not resized when it empties - that would move the whole table twice a
   trick, which is the defect §16 exists to remove. It recedes instead: the
   frame settles back toward the felt while there is nothing on it, and comes
   back up the moment a card lands. The art is untouched; only its presence
   changes, on the design's own tint timing.

   ROUND 2: **`data-empty` counts المكشوف**. §6 puts the exposed card in the
   free centre cell of this same grid, and round 1 computed emptiness from the
   four played slots alone - so for the whole of every auction the one card the
   auction is a decision about was drawn at .42 through a receded frame. The mat
   is not empty when it is holding something; `game/game.js:exposedIsShown` is
   the single predicate both this and the element itself read. */
.gm-trick {
  transition: opacity var(--t-tint, 200ms) linear,
              filter var(--t-tint, 200ms) linear;
}
.gm-trick[data-empty="1"] { opacity: .42; filter: saturate(.7); }

/* --- 32 · The support and privacy line --------------------------------------
   The App Store needs reachable support and privacy surfaces, so the lobby
   carries links to `support/` and `privacy/`. The captain's lobby has no legal
   surface - an imported design with no such surface has nothing to link - so
   `lobby.js` creates them and this is their whole appearance.

   Discreet is the requirement and also the design's own manner: they sit under
   everything, in the muted text colour the page already defines, at the size
   the design uses for a caption. They remain underlined links with independent
   44pt-tall targets rather than decoration that has to be guessed at. */

.pt-foot {
  /* No block margin of its own: the lobby column is exactly one screen tall and
     the `.grow` spacer above it absorbs the difference, so anything this line
     adds beyond the targets below is a scrollbar on a page designed not to have
     one. The links' own padding is the whole spacing. */
  margin: 0;
  display: flex;
  justify-content: center;
  text-align: center;
  font-size: .78rem;
  line-height: 1;
}

.pt-foot > a {
  display: inline-block;
  padding: 1rem .75rem;
  color: var(--tx2, #a9a294);
  text-decoration: underline;
  text-underline-offset: .25em;
  opacity: .75;
}

.pt-foot > a:hover,
.pt-foot > a:focus-visible { opacity: 1; }

/* The lobby column is exactly one screen on a modern iPhone and the `.grow`
   spacer absorbs this line there - measured at 390x844: the page does not
   scroll with the link on it. On the shortest supported phone (375x667) that
   spacer is already at zero before this line exists, so the lobby gains a few
   pixels of scroll rather than losing the link or covering the ledger with it.
   The target stays at 44px there; only the air around it goes. */
@media (max-height: 720px) {
  .pt-foot > a {
    padding-block: 0;
    min-block-size: 44px;
    display: inline-flex;
    align-items: center;
  }
}

/* ============================================================================
   ROUND 2 · the second pass of the captain's design iteration
   ============================================================================
   Round 1 closed the three audits' ranked lists and left four calls with the
   captain. All four were ruled on, durably, on 2026-08-20, and §33-§36 below
   are this repository's half of them plus what a fresh audit of every non-lobby
   view found on the merged build. The rulings themselves are in
   `../README.md` ("All three were ruled on") and in `AGENTS.md`.

   Still additive, still no captain file edited, still no colour, type family or
   radius that is not the design's own.
   ========================================================================= */

/* --- 33 · A card has an edge ------------------------------------------------
   The faces are frozen (ruling 1) and nothing here touches one. What is not
   frozen is everything around a face, and around a face there was nothing: the
   design gives `.card` a white body, a 3% corner and a shadow that is invisible
   against navy, and the fan sits its cards 6px apart. Eight white rectangles
   with no edge between them read as one white band across the bottom of the
   screen - the loudest thing on a navy-and-gold table, and a blank one.

   The set's own SVG says where the edge is: inside a 238.1x332.6 viewBox the
   printed body is a 222.2x310.8 rect with a 10.6 corner, so a face is a rounded
   card with 3.3% of transparent margin around it - and `.card`'s white
   background has been filling that margin in, which is exactly why the printed
   corner never showed. Two additive things put it back:

   * the box's corner is taken to 5%, so the clip follows the printed corner
     instead of cutting a square around it, and
   * a hairline sits on the box, in the ink the design already uses for a card's
     own shadow, so where two cards overlap there is a line between them.

   `::after` rather than a `box-shadow` on `.card`: the design's drop shadow and
   §25's winning ring are both `box-shadow` on the same element, and replacing
   that property would take one of them out. */
.card::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: inset 0 0 0 1px rgba(4, 22, 13, .34);
  pointer-events: none;
}
.card { border-radius: 5%; }

/* --- 34 · The back behind a face that has not arrived -----------------------
   Ruling 4's backstop, and the invariant the other three parts exist to serve:
   **white is never a frame a player sees**. An `<img>` with nothing decoded in
   it yet paints nothing, so what showed through was `.card`'s own white body -
   a blank card, on the overlapped fan an unreadable one.

   The card back is what belongs behind a face that has not turned over. It is
   the design's own art, it is 46 KB, and every opponent fan on the table is
   already made of it, so by the time a face can be late the bytes are held.
   `game/game.js:cardElement` sets `data-face` the moment the face is decoded -
   from the held `Image` when the bytes are already in, which is every render
   after the first, and from the element's own `load` otherwise. */
.card { background-image: url("../assets/back-bone-v8.svg"); background-size: cover; background-position: center; }
.card[data-face="1"] { background-image: none; }

/* --- 35 · The hand comes down to the thumb ---------------------------------
   Measured at 390x844 on the merged build: `.gm-mine` is 241px holding a 96px
   hand, and the 99px it does not use lands *between the hand and the action
   rail* - the largest continuous hole on the page, in the one place a phone
   cannot afford one. Both boxes grow (`flex:1 1 auto`), so the free height was
   being split between the table and a box whose whole content is one line and
   one row of cards.

   The table takes it instead. Its own `align-content:center` spends the extra
   as symmetric air around the mat cluster - which is the reading the design
   wants, a mat centred in a room - and the hand ends up where a thumb already
   is, one gap above the rail. §16's floor stays exactly as it was, so the box
   still does not collapse between hands; only the growing moves. */
.gm-mine { flex: 0 1 auto; }

/* --- 36 · Landscape is locked to portrait ----------------------------------
   The captain's ruling 3, durable, 2026-08-20: **no landscape layout is ever
   built**. A rotated phone keeps the portrait arrangement, cleanly, with
   nothing reflowing and nothing to scroll to. `app/ios-shell` is already
   portrait-locked by its own manifest; this is the browser's half of the same
   posture, and it replaces §13's "what does not fit is scrolled to".

   What the ruling replaces, measured on the merged build at 852x393: the
   document is 575px tall in a 393px viewport, the hand starts at y=403 and the
   action rail at y=493 - both below the fold, with no scroll position from
   which the table and the hand are on the screen together.

   A browser rotates the viewport where a portrait-locked app does not, so the
   lock is the page declining the rotation: the column is built at the
   viewport's *other* two dimensions and turned back by a quarter. Nothing
   inside it knows - every rule above, every measurement in this file and every
   captain arithmetic is still reading a 390-ish wide, 844-ish tall column, and
   `--card-w`, the 44pt targets and the button ladder all land where they land
   in portrait. The player sees what a portrait-locked app shows them: the table
   stays with the phone rather than following the hand that turned it.

   `--vh` is restated because the design derives `--g` from it and `100svh` in
   here is the *short* side; the notch is taken from whichever long edge it
   landed on, because a rotation can put it on either. */
@media (orientation: landscape) and (pointer: coarse) and (max-height: 600px) {
  html { overflow: hidden; }
  body.gm {
    position: fixed;
    top: 0;
    left: 0;
    margin: 0;
    inline-size: 100svh;
    block-size: 100svw;
    transform-origin: 0 0;
    transform: translateX(100svw) rotate(90deg);
    /* The column's own top is a screen *side* now, and which side depends on
       which way the phone was turned - so the notch is taken as whichever long
       edge reports one, and the home indicator's edge is kept clear on both
       short sides rather than guessed at. */
    --sa-t: max(env(safe-area-inset-left, 0px), env(safe-area-inset-right, 0px));
    --sa-b: 0px;
    --sa-x: max(env(safe-area-inset-top, 0px), env(safe-area-inset-bottom, 0px));
    --vh: calc(100svw - var(--sa-t) - var(--sa-b));
    padding: var(--sa-t) var(--sa-x) 0px var(--sa-x);
  }

  /* Which quarter-turn. `orientation: landscape` is true for both, and a
     portrait-locked app puts its top on the screen edge the device's own top
     turned into - the right one way, the left the other. `game/game.js`
     publishes `screen.orientation.type`; with no such reading the rule above
     stands on its own and the lock still holds, one turn out of two. */
  html[data-orient="landscape-secondary"] body.gm {
    transform: translateY(100svh) rotate(-90deg);
  }
}

/* --- P1 · Prototype: the label-then-card project picker ---------------------
   Captain's design, 2026-09-14, behind `body[data-project-picker="labels"]`
   (`game/project_pick.js` quotes it). Four labels above the hand; the armed one
   carries the same dotted gold the first chosen project's cards carry, the
   second chosen project is violet. Chosen projects are compact tags in a strip
   of their own above the hand's top-left corner, outside every card, each
   with a small ✕ button whose hit area is the full 44pt (captain, second
   round: "not overlap with my cards on the top left ... above my whole
   card"). The rail keeps exactly four flow items so nothing shifts when a
   card is raised: the confirming hint sits out of the flow, above the labels.
   Gold is the lobby's own focus token; the violet is the one literal the
   captain named and it lives here, above the Sadu Court layer, which keeps no
   literal colour of its own. A page without the flag matches none of this. */
body[data-project-picker="labels"] .gm-hand > .card[data-project-pick="1"] {
  outline: 3px dotted var(--a-hi);
  outline-offset: -5px;
  border-radius: 6px;
}
body[data-project-picker="labels"] .gm-hand > .card[data-project-pick="2"] {
  outline: 3px dotted #b57bee;
  outline-offset: -5px;
  border-radius: 6px;
}
body[data-project-picker="labels"] .gm-act > .btn[data-project-label][aria-pressed="true"] {
  outline: 3px dotted var(--a-hi);
  outline-offset: -4px;
}
body[data-project-picker="labels"] .gm-act:has(> .btn[data-project-label]) {
  position: relative;
  align-content: end;
}
body[data-project-picker="labels"] .gm-act > .gm-hint.pt-project-hint {
  position: absolute;
  inset-inline: 0;
  inset-block-start: 8px;
  margin: 0;
  text-align: center;
  pointer-events: none;
}
/* `.gm-mine` is already the strip's containing block in every port layout;
   restating its position here overrode §35 and sent the hand off-screen. */
body[data-project-picker="labels"] .pt-project-tags {
  position: absolute;
  inset-inline-start: auto;
  left: 12px;
  inset-block-start: 0;
  z-index: 6;
  display: flex;
  direction: ltr;
  gap: 6px;
  pointer-events: none;
}
body[data-project-picker="labels"] .pt-project-tag {
  pointer-events: auto;
  display: inline-flex;
  align-items: center;
  gap: 2px;
  block-size: 26px;
  padding-inline: 8px 2px;
  border-radius: 13px;
  background: color-mix(in srgb, var(--s-well, #0b1626) 88%, transparent);
  color: var(--tx, #f4ecd8);
  font-size: 12px;
  font-weight: 800;
  line-height: 1;
  white-space: nowrap;
  border: 2px dotted var(--a-hi);
}
body[data-project-picker="labels"] .pt-project-tag[data-pick="2"] { border-color: #b57bee; }
body[data-project-picker="labels"] .pt-project-tag-remove {
  position: relative;
  inline-size: 22px;
  block-size: 22px;
  min-inline-size: 22px;
  min-block-size: 22px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 12px;
  line-height: 22px;
  cursor: pointer;
}
/* The visible ✕ is small; its touch target is not. */
body[data-project-picker="labels"] .pt-project-tag-remove::before {
  content: "";
  position: absolute;
  inset: -11px;
}
body[data-project-picker="labels"] .pt-project-tag-remove:focus-visible {
  outline: 2px solid var(--a-hi);
  outline-offset: 1px;
}
/* The project controls - the four labels, or سوا - are one pill each, all of
   one width, in two rows at the hand's right edge whose lower row shares the
   band of the viewer's name. No control sits over a card. Each pill is 36px
   tall with 9px between the rows, so its hit area reaches exactly 44x44 by
   growing 4px up and down: no two hit areas overlap, and the lower row's
   stops above the resting cards. The strip paints above a prepared card, so a
   control is never hidden by the card it is about. */
body.gm { --pill-w: 64px; --ctl-right: 12px; --ctl-gap-x: 6px; }
.pt-project-labels {
  position: absolute;
  inset-inline-start: auto;
  right: var(--ctl-right);
  bottom: calc(100% - 22px);
  z-index: 80;
  display: grid;
  grid-template-columns: repeat(2, var(--pill-w));
  justify-content: end;
  gap: 9px var(--ctl-gap-x);
  direction: rtl;
}
.pt-project-labels[hidden] { display: none; }
.pt-project-label {
  position: relative;
  inline-size: var(--pill-w);
  block-size: 36px;
  min-block-size: 36px;
  padding: 0;
  border: 1px solid rgba(var(--tx-rgb), .22);
  /* One proportionate corner for every control and state: a quarter of the
     36px height - rounded, never a semicircular pill. */
  border-radius: 9px;
  background: linear-gradient(180deg, var(--s-panel-hi), var(--s-panel-lo));
  color: var(--tx);
  font-family: var(--fu);
  font-size: 13px;
  font-weight: var(--w-control, 800);
  line-height: 1;
  white-space: nowrap;
  text-align: center;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.pt-project-label::before {
  content: "";
  position: absolute;
  /* Measured from inside the 1px border: this is the border box plus 4px
     above and below, 44px tall and exactly the pill's width. */
  inset: -5px -1px;
}
/* Narrower phones keep both rows on the name band beside the name, not over
   it: the one pill width steps down (50px, then 44px with a 4px gap on the
   narrowest), and every pill still keeps a 44x44 target. */
@media (max-width: 409px) {
  body.gm { --pill-w: 50px; }
  .pt-project-label { font-size: 12px; }
}
@media (max-width: 350px) {
  body.gm { --pill-w: 44px; --ctl-right: 4px; --ctl-gap-x: 4px; }
  .pt-project-label { font-size: 10.5px; }
}
.pt-project-label[aria-pressed="true"] {
  outline: 3px dotted var(--a-hi);
  outline-offset: -4px;
  color: var(--a-hi);
}
.pt-project-label:disabled { opacity: .45; cursor: default; }
/* A bid's short in-flight lock must not make an eligible كوّش pill blink.
   The disabled button still refuses a second request; exit/outage locks retain
   the usual dim state. The Sawa and project controls are untouched. */
.pt-project-label[data-bayad][data-bid-pending="1"]:disabled { opacity: 1; }
/* The throw's own pending request is not an accepted throw. Keep the same
   button in place, full-opacity and visibly busy until the server answers;
   the disclosure/redeal still begins only on its accepted public receipt. */
.pt-project-label[data-bayad][aria-busy="true"]:disabled {
  opacity: 1;
  border-color: var(--a-hi);
}
.pt-project-label:focus-visible {
  outline: 2px solid var(--a-hi);
  outline-offset: 2px;
}

/* ============================================================================
   37 · A2 · SADU COURT / ميدان السدو
   ============================================================================
   The launch redesign supersedes the imported table composition without
   changing its DOM or binding. One arched textile court now owns the scene:
   seats attach to its perimeter, the exposed/trick cards use hand scale, the
   player's fan bleeds to the glass, and decisions sit on the thumb rail.

   This is intentionally one complete presentation layer, rather than another
   sequence of local corrections to the old page-wide frame and miniature mat.
   The frozen face SVGs remain untouched. The lobby's live tokens, woven ground,
   lamp and falloff are the palette authority; the court adds no second colour
   system. Its central surface is the original generated `mat-court-v2.webp`,
   not the superseded tak illustration or a CSS-drawn imitation.
   ========================================================================== */

body.gm {
  --a2-page-pad: 10px;
  --a2-safe-t: max(10px, var(--sa-t));
  --a2-safe-b: max(8px, var(--sa-b));
  /* One physical card system. Every seat starts from the same 5:7 rectangle;
     hierarchy changes scale and orientation, never proportion. */
  --card-w: 68px;
  --card-h: calc(var(--card-w) * 1.4);
  --a2-played-w: 58px;
  --a2-played-h: calc(var(--a2-played-w) * 1.4);
  --a2-remote-w: calc(var(--card-w) * .56);
  --a2-remote-h: calc(var(--a2-remote-w) * 1.4);
  /* Aliases only: the accepted lobby owns every game-page colour. */
  --a2-focus: var(--a-hi);
  --a2-text: var(--tx);
  --a2-muted: var(--tx3);
  --a2-field: var(--s-table);
  --a2-field-inset: var(--s-well);

  position: relative;
  isolation: isolate;
  inline-size: min(100vw, 430px);
  block-size: 100svh;
  margin: 0 auto;
  padding: var(--a2-safe-t) var(--a2-page-pad) var(--a2-safe-b);
  border: 0;
  border-image: none;
  overflow: hidden;
  display: grid;
  grid-template-rows: 64px minmax(0, 1fr) 160px 68px;
  color: var(--a2-text);
  /* `game.css` deliberately remains the one consumer of the lobby's exact
     woven ground, --lamp and --falloff composition. A background shorthand or
     pseudo-element override here would create the second atmosphere we remove. */
  box-shadow: 0 24px 80px rgba(0, 0, 0, .58);
}

/* One compact score/contract object. The two 44pt controls remain in their
   original DOM order and every published score/contract field is unchanged. */
.gm-top {
  position: relative;
  z-index: 20;
  min-inline-size: 0;
  block-size: 100%;
  padding: 4px;
  display: grid;
  grid-template-columns: 44px minmax(46px, .8fr) minmax(66px, 1.35fr) minmax(46px, .8fr) 44px;
  gap: 4px;
  align-items: center;
  border: 1px solid rgba(var(--tx-rgb), .16);
  border-radius: 16px;
  background: var(--face);
  box-shadow: 0 10px 28px rgba(0, 0, 0, .25);
}
.gm-exit, .gm-mute {
  inline-size: 44px;
  block-size: 44px;
  border-radius: 12px;
  color: var(--a2-muted);
  background: var(--face-well);
  box-shadow: inset 0 0 0 1px rgba(var(--tx-rgb), .08), 0 7px 18px rgba(0, 0, 0, .25);
}
.gm-mute[data-on="0"] {
  color: var(--a-ink);
  background: var(--face-well);
  box-shadow: inset 0 0 0 2px var(--a), 0 7px 18px rgba(0, 0, 0, .25);
}
.gm-sc { min-inline-size: 0; gap: 3px; }
.gm-sc-k { font-size: 12px; color: var(--a2-muted); }
.gm-sc-v { font-size: 21px; font-weight: 800; }
.gm-sc-us .gm-sc-v { color: var(--a); }
.gm-deal { overflow: hidden; }
.gm-ct-mode { font-size: 14px; font-weight: 800; }
.gm-deal-2 { font-size: 12px; color: var(--a2-muted); }

/* The arena stays broad and arched: the accepted A2 composition is unchanged.
   The surface itself is now one generated image, with a neutral navy edge and
   no CSS-built textile or gold frame competing with the cards. */
.gm-table {
  position: relative;
  z-index: 2;
  isolation: isolate;
  inline-size: calc(100% + 4px);
  min-block-size: 0;
  margin-inline: -2px;
  padding: 0;
  display: block;
  background: transparent;
  overflow: visible;
}
.gm-table::before, .gm-table::after {
  content: "";
  position: absolute;
  pointer-events: none;
  /* One corner for the whole mat: the top corners take the smaller radius the
     bottom ones always had, rather than a larger arch. */
  border-radius: 24px;
}
.gm-table::before {
  z-index: 0;
  inset: 96px 5% -7px;
  border: 1px solid rgba(var(--tx-rgb), .18);
  background-color: var(--a2-field);
  background-image: url("../game/mat-court-v2.webp");
  background-position: center;
  background-size: 100% 100%;
  background-repeat: no-repeat;
  box-shadow: 0 20px 36px rgba(0, 0, 0, .38);
}
.gm-table::after { display: none; }

/* Four seated players: the viewer is the near hand and the other three attach
   to the court. Counts are generated from the same hand count that builds each
   back fan, so no client-side rule is inferred. */
.gm-seat {
  position: absolute;
  z-index: 8;
  display: flex;
  align-items: center;
  gap: 4px;
}
.gm-seat-top {
  inset-block-start: 10px;
  left: 50%;
  flex-direction: column;
  transform: translateX(-50%);
}
.gm-seat-left, .gm-seat-right {
  inset-block-start: 48%;
  inline-size: 70px;
  max-inline-size: 70px;
  transform: translateY(-50%);
}
.gm-seat-left { left: 2px; }
.gm-seat-right { right: 2px; }
.gm-who {
  min-block-size: 27px;
  max-inline-size: 92px;
  padding: 3px 9px;
  gap: 5px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--s-well) 90%, transparent);
  box-shadow: inset 0 0 0 1px rgba(var(--tx-rgb), .22), 0 6px 16px rgba(0, 0, 0, .35);
  font-size: 12px;
  font-weight: 800;
}
.gm-seat-top .gm-name, .gm-seat-top .gm-role,
.gm-seat-right .gm-who, .gm-seat-left .gm-who { font-size: 12px; }
.gm-who { overflow: hidden; min-inline-size: 0; }
.gm-name {
  flex: 1 1 auto;
  min-inline-size: 0;
  max-inline-size: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.gm-role { flex: 0 0 auto; font-size: 12px; }
.gm-seat-left .gm-who, .gm-seat-right .gm-who {
  inline-size: 70px;
  max-inline-size: 70px;
  justify-content: center;
  padding-inline: 6px;
}
.gm-role { font-size: 10px; }
.gm-node { background: var(--ai-hi); box-shadow: 0 0 10px var(--ai-glow); }
.gm-seat[data-active="1"] .gm-who::after {
  content: "";
  inline-size: 5px;
  block-size: 5px;
  flex: 0 0 auto;
  border-radius: 50%;
  background: var(--a);
}

.gm-backs, .gm-backs-v {
  position: relative;
  flex: 0 0 auto;
  display: block;
  min-block-size: 0;
  inline-size: calc(var(--a2-remote-w) * 2.8);
  block-size: calc(var(--a2-remote-h) + 14px);
}
.gm-backs:not(.gm-backs-v) > i, .gm-backs-v > i {
  position: absolute;
  left: 50%;
  inset-block-start: 2px;
  inline-size: var(--a2-remote-w);
  block-size: var(--a2-remote-h);
  aspect-ratio: 5 / 7;
  margin: 0 0 0 calc(var(--a2-remote-w) / -2);
  border-radius: 5%;
  transform-origin: 50% 138%;
  transform: translateX(var(--back-spread, 0px)) translateY(var(--back-arc, 0px)) rotate(var(--back-rotation, 0deg));
  box-shadow: inset 0 0 0 1px rgba(var(--tx-rgb), .18), 0 4px 8px rgba(0, 0, 0, .35);
}
.gm-backs::after {
  content: attr(data-count);
  position: absolute;
  z-index: 4;
  right: 0;
  inset-block-end: 0;
  min-inline-size: 22px;
  block-size: 22px;
  padding-inline: 5px;
  display: grid;
  place-items: center;
  border: 1px solid var(--a-deep);
  border-radius: 999px;
  background: var(--s-well-hi);
  color: var(--tx);
  font: 800 11px/1 system-ui, sans-serif;
}
.gm-seat-left .gm-backs, .gm-seat-right .gm-backs {
  order: -1;
  inline-size: calc(var(--a2-remote-h) + 10px);
  block-size: calc(var(--a2-remote-w) * 2.8);
}
.gm-seat-left .gm-backs > i, .gm-seat-right .gm-backs > i {
  left: 50%;
  inset-block-start: 50%;
  margin: calc(var(--a2-remote-h) / -2) 0 0 calc(var(--a2-remote-w) / -2);
  transform-origin: 50% 50%;
}
.gm-seat-left .gm-backs > i {
  transform: translateX(calc(-1 * var(--back-arc, 0px))) translateY(var(--back-spread, 0px)) rotate(calc(90deg + var(--back-rotation, 0deg)));
}
.gm-seat-right .gm-backs > i {
  transform: translateX(var(--back-arc, 0px)) translateY(var(--back-spread, 0px)) rotate(calc(-90deg - var(--back-rotation, 0deg)));
}

/* A transparent 190px cross carries either a hand-scale exposed card or four
   fixed-size played cards. It has no frame and cannot collide with side seats. */
.gm-trick {
  position: absolute;
  z-index: 5;
  left: 50%;
  inset-block-start: 57%;
  transform: translate(-50%, -50%);
  inline-size: 190px;
  block-size: 190px;
  padding: 0;
  display: block;
  background: transparent;
  box-shadow: none;
  opacity: 1;
  filter: none;
  transition: none;
}
.gm-trick[data-empty="1"] { opacity: 1; filter: none; }
.gm-slot {
  position: absolute;
  inline-size: var(--a2-played-w);
  block-size: var(--a2-played-h);
  display: grid;
  place-items: center;
}
.gm-slot > .card {
  inline-size: var(--a2-played-w);
  block-size: var(--a2-played-h);
}
.gm-slot-top { left: 66px; inset-block-start: 0; }
.gm-slot-left { left: 24px; inset-block-start: 54px; }
.gm-slot-right { right: 24px; inset-block-start: 54px; }
.gm-slot-bottom { left: 66px; inset-block-start: 108px; }
/* MAIN050: played cards face their player-relative centre direction. Keep
   dimensions and the existing small lay tilt; hands/piles are not rotated. */
.gm-slot-top > .card { --tilt: 182.5deg; }
.gm-slot-right > .card { --tilt: -94deg; }
.gm-slot-left > .card { --tilt: 94deg; }
.gm-slot-bottom > .card { --tilt: -2deg; }
.gm-slot[data-won="1"] > .card {
  box-shadow: 0 0 0 2px var(--s-void), 0 0 0 4px var(--a2-focus), 0 9px 20px rgba(0, 0, 0, .5);
}
.pt-exposed {
  position: absolute;
  z-index: 8;
  left: 50%;
  inset-block-start: 50%;
  transform: translate(-50%, -50%);
  display: flex;
  gap: 7px;
}
.pt-exposed-k {
  min-block-size: 25px;
  padding: 3px 10px;
  display: flex;
  align-items: center;
  border: 1px solid rgba(var(--tx-rgb), .2);
  border-radius: 999px;
  background: var(--s-well-lo);
  color: var(--tx);
  font-size: 12px;
  font-weight: 800;
  letter-spacing: 0;
}
.pt-exposed > .card {
  inline-size: var(--card-w);
  block-size: var(--card-h);
  min-inline-size: var(--card-w);
  min-block-size: var(--card-h);
  aspect-ratio: 5 / 7;
}

/* The hand is the primary control. It alone bleeds through the page gutter;
   JavaScript publishes a small quadratic arc and one-degree-per-step rotation
   from the authority-ordered hand, while CSS owns selection and legality. */
.gm-mine {
  position: relative;
  z-index: 15;
  inline-size: calc(100% + 2 * var(--a2-page-pad));
  block-size: 100%;
  margin-inline: calc(-1 * var(--a2-page-pad));
  padding: 0;
  display: grid;
  grid-template-rows: 26px minmax(0, 1fr);
  gap: 0;
  align-items: start;
  justify-content: stretch;
  min-block-size: 0;
  overflow: visible;
}
.gm-mine::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset-inline: 4%;
  inset-block-end: 2px;
  block-size: 72px;
  border-radius: 50%;
  background: radial-gradient(ellipse, rgba(0, 0, 0, .42), transparent 68%);
  filter: blur(7px);
}
.gm-turn {
  justify-self: center;
  min-block-size: 24px;
  display: flex;
  align-items: center;
  gap: 7px;
  color: var(--a);
  font-size: 12px;
  font-weight: 800;
}
.gm-turn::before, .gm-turn::after { content: ""; inline-size: 18px; block-size: 1px; }
.gm-turn::before { background: linear-gradient(90deg, transparent, currentColor); }
.gm-turn::after { background: linear-gradient(90deg, currentColor, transparent); }
.gm-turn[data-wait="1"] { color: var(--a2-muted); }
.gm-hand {
  --fan-rail-lift: -22px;
  position: relative;
  justify-self: stretch;
  inline-size: auto;
  block-size: 126px;
  margin-inline: 8px;
  padding: 0;
  display: grid;
  direction: ltr;
  align-content: end;
  justify-content: center;
  outline: none;
}
.gm-hand > .card {
  grid-row: 1;
  align-self: end;
  z-index: var(--z, 1);
  border-radius: 7px;
  transform-origin: 50% 155%;
  transform: translateY(calc(var(--fan-arc, 0px) + var(--fan-rail-lift))) rotate(var(--fan-rotation-live, var(--fan-rotation, 0deg)));
  transition: transform 160ms cubic-bezier(.2, .8, .2, 1), box-shadow 160ms linear, filter 160ms linear;
  pointer-events: none;
}
/* Outside an actual play choice, data-ok=0 means inert or off-turn, not illegal. */
.gm-hand > .card[data-ok="0"] {
  filter: none;
  opacity: 1;
  transform: translateY(calc(var(--fan-arc, 0px) + var(--fan-rail-lift))) rotate(var(--fan-rotation-live, var(--fan-rotation, 0deg)));
  cursor: pointer;
}
body[data-phase="PLAY_TRICKS"].is-your-turn .gm-hand > .card[data-ok="0"] {
  filter: brightness(.78) saturate(.76);
  opacity: .84;
  transform: translateY(calc(var(--fan-arc, 0px) + var(--fan-rail-lift) + 6px)) rotate(var(--fan-rotation-live, var(--fan-rotation, 0deg)));
  cursor: not-allowed;
}
.gm-hand > .card[data-sel="1"] {
  --fan-selection-lift: -14px;
  transform: translateY(calc(var(--fan-arc, 0px) + var(--fan-rail-lift) + var(--fan-selection-lift))) rotate(var(--fan-rotation-live, var(--fan-rotation, 0deg)));
  box-shadow: 0 0 0 2px var(--s-void), 0 0 0 5px var(--a2-focus), 0 14px 28px rgba(0, 0, 0, .5);
}
.gm-hand > .card[data-premove="1"] {
  box-shadow: 0 0 0 2px var(--s-void), 0 0 0 5px var(--ai-hi), 0 14px 28px rgba(0, 0, 0, .5);
}
.gm-hand > .card:focus-visible {
  outline: 3px solid var(--a2-focus);
  outline-offset: 3px;
}
/* Pointer ownership is independent of paint order: one independent 44pt lane
   per card sits above the overlapping faces, placed by `game/fan.js`
   `handHitLanes` on the strip of its face a player can see, while the
   authority-ordered card elements remain the keyboard and accessibility
   controls. Selection can lift and paint naturally without taking a
   neighbour's target. */
.pt-card-hit {
  position: absolute;
  z-index: 60;
  inset-block: 0;
  inline-size: 44px;
  cursor: pointer;
  touch-action: manipulation;
  background: transparent;
}

/* Warm restrained thumb rail. One to four choices remain in one row; five to
   eight use two rows with four launch-readable targets rather than scrolling. */
.gm-act, .gm-act.is-empty {
  position: relative;
  z-index: 25;
  inline-size: 100%;
  block-size: 100%;
  min-block-size: 0;
  padding: 5px 0;
  display: flex;
  align-items: stretch;
  justify-content: center;
  gap: 8px;
  row-gap: 8px;
}
.gm-act > .btn {
  min-inline-size: 44px;
  min-block-size: 50px;
  block-size: 50px;
  flex: 1 1 0;
  padding-inline: 8px;
  border: 1px solid rgba(var(--tx-rgb), .17);
  border-radius: 15px;
  color: var(--tx);
  background: var(--face);
  box-shadow: 0 5px 12px rgba(0, 0, 0, .3);
  font-size: 16px;
  font-weight: 850;
}
.gm-act > .btn-1 {
  color: var(--ink);
  border-color: var(--a-hi);
  background: var(--face-hot);
  box-shadow: var(--bloom-sunk), var(--rim-hot);
}
body.gm[data-action-count="5"], body.gm[data-action-count="6"],
body.gm[data-action-count="7"], body.gm[data-action-count="8"],
body.gm.is-leaving {
  grid-template-rows: 64px minmax(0, 1fr) 160px 108px;
}
body.gm.is-waiting.is-leaving, body.gm.has-outcome.is-leaving {
  grid-template-rows: 64px minmax(0, 1fr) 0 108px;
}
.gm-act[data-count="5"], .gm-act[data-count="6"],
.gm-act[data-count="7"], .gm-act[data-count="8"] {
  min-block-size: 108px;
  padding-block: 4px;
}
.gm-act[data-count="5"] > .btn, .gm-act[data-count="6"] > .btn,
.gm-act[data-count="7"] > .btn, .gm-act[data-count="8"] > .btn {
  flex: 1 1 calc(25% - 8px);
  max-inline-size: none;
  min-inline-size: 64px;
  block-size: 48px;
  min-block-size: 48px;
  font-size: 14px;
}
.gm-act .gm-hint { align-self: center; font-size: 12px; }
.gm-act.is-empty .gm-hint.pt-confirm {
  min-block-size: 40px;
  padding: 8px 18px;
  display: flex;
  align-items: center;
  font-size: 13px;
}

/* Waiting, outcomes and failures retain their lifecycle and controls, now on
   the same court materials. They overlay the field rather than reserving a
   phantom third grid row from the superseded composition. */
.pt-wait, .pt-over {
  position: absolute;
  z-index: 30;
  inset-inline: max(16px, 8%);
  inset-block-start: 50%;
  transform: translateY(-50%);
  margin: 0;
  padding: 24px 16px 16px;
  border: 1px solid rgba(var(--tx-rgb), .16);
  border-radius: 22px;
  background: var(--face);
  box-shadow: 0 16px 36px rgba(0, 0, 0, .46);
}
.pt-over[data-kind="match"] { box-shadow: inset 0 0 0 1px rgba(var(--a-rgb), .55), 0 16px 40px rgba(0, 0, 0, .52); }
.pt-wait-seats {
  inline-size: 100%;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 6px;
  list-style: none;
}
.pt-wait-seats > li {
  min-inline-size: 0;
  padding: 6px 10px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  border-radius: 12px;
  background: color-mix(in srgb, var(--s-well) 58%, transparent);
  box-shadow: inset 0 0 0 1px rgba(var(--tx-rgb), .12);
  font-size: 12px;
}
.pt-wait-seats span { min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pt-wait-seats strong { flex: 0 0 auto; color: var(--a); font-weight: 800; }
.pt-wait-copy, .pt-wait-leave { inline-size: 100%; min-block-size: 48px; }
.pt-wait-leave {
  color: var(--a2-text);
  background: var(--face);
}
body.is-waiting .gm-mine, body.has-outcome .gm-mine { visibility: hidden; overflow: hidden; }
body.is-waiting, body.has-outcome { grid-template-rows: 64px minmax(0, 1fr) 0 68px; }
.pt-bar {
  inset-block-start: calc(var(--a2-safe-t) + 64px);
  inset-inline: 0;
  inline-size: min(calc(100vw - 20px), 406px);
  margin-inline: auto;
  border: 1px solid rgba(var(--tx-rgb), .16);
  background: var(--face);
}

@media (max-height: 880px) {
  body.gm { grid-template-rows: 60px minmax(0, 1fr) 145px 60px; }
  .gm-table::before { inset-block-start: 88px; }
  .gm-table::after { inset-block-start: 100px; }
  body.is-waiting, body.has-outcome { grid-template-rows: 60px minmax(0, 1fr) 0 60px; }
  body.gm[data-action-count="5"], body.gm[data-action-count="6"],
  body.gm[data-action-count="7"], body.gm[data-action-count="8"],
  body.gm.is-leaving {
    grid-template-rows: 60px minmax(0, 1fr) 145px 108px;
  }
  body.gm.is-waiting.is-leaving, body.gm.has-outcome.is-leaving {
    grid-template-rows: 60px minmax(0, 1fr) 0 108px;
  }
  .pt-bar { inset-block-start: calc(var(--a2-safe-t) + 60px); }
}

@media (min-width: 415px) {
  body.gm { --a2-page-pad: 12px; --card-w: 70px; --a2-played-w: 58px; }
  .gm-act > .btn { min-block-size: 52px; block-size: 52px; }
}

@media (max-height: 740px) {
  body.gm {
    --card-w: 64px;
    --a2-played-w: 54px;
    grid-template-rows: 58px minmax(0, 1fr) 132px 58px;
  }
  .gm-table::before { inset-block-start: 58px; }
  .gm-table::after { inset-block-start: 70px; }
  .gm-seat-top { transform: translateX(-50%) scale(.9); transform-origin: top center; }
  .gm-trick { inset-block-start: 56%; transform: translate(-50%, -50%) scale(.9); }
  .gm-hand { block-size: 112px; }
  .gm-act > .btn { min-block-size: 48px; block-size: 48px; }
  body.is-waiting, body.has-outcome { grid-template-rows: 58px minmax(0, 1fr) 0 58px; }
  body.gm[data-action-count="5"], body.gm[data-action-count="6"],
  body.gm[data-action-count="7"], body.gm[data-action-count="8"],
  body.gm.is-leaving {
    grid-template-rows: 58px minmax(0, 1fr) 132px 108px;
  }
  body.gm.is-waiting.is-leaving, body.gm.has-outcome.is-leaving {
    grid-template-rows: 58px minmax(0, 1fr) 0 108px;
  }
  .pt-bar { inset-block-start: calc(var(--a2-safe-t) + 58px); }
}

/* A 360px portrait column has room for eight independent 44px lanes.
   Flatten the eight-card fan there, including the same column after rotation;
   the final viewer sizing and count-aware reserve below own its face strips. */
@media (max-width: 374px),
       (orientation: landscape) and (pointer: coarse) and (max-height: 374px) {
  body.gm { --card-w: 60px; }
  .gm-hand > .card { --fan-rotation-live: var(--fan-rotation-narrow); }
}

@media (prefers-reduced-motion: reduce) {
  .gm-hand > .card, .gm-trick, .gm-slot > .card { transition-duration: .01ms !important; }
}

/* §36 remains the single landscape posture. Restated after A2 because the A2
   body composition intentionally supersedes the ordinary portrait body rule,
   while a coarse rotated phone must still decline the rotation entirely. */
@media (orientation: landscape) and (pointer: coarse) and (max-height: 600px) {
  html { overflow: hidden; }
  body.gm {
    position: fixed;
    top: 0;
    left: 0;
    inline-size: 100svh;
    block-size: 100svw;
    margin: 0;
    transform-origin: 0 0;
    transform: translateX(100svw) rotate(90deg);
    --sa-t: max(env(safe-area-inset-left, 0px), env(safe-area-inset-right, 0px));
    --sa-b: 0px;
    --sa-x: max(env(safe-area-inset-top, 0px), env(safe-area-inset-bottom, 0px));
    --vh: calc(100svw - var(--sa-t) - var(--sa-b));
    padding: var(--a2-safe-t) var(--sa-x) var(--a2-safe-b);
  }
  html[data-orient="landscape-secondary"] body.gm {
    transform: translateY(100svh) rotate(-90deg);
  }
}

/* ============================================================================
   38 · LAUNCH CONTINUITY · an honest open, a legible partner
   ============================================================================
   A fresh production audit found two first-order states that the completed A2
   composition still answered with placeholders rather than information.

   During the bounded face gate the court showed the imported opponent names,
   empty score labels and the hand's shadow with no sentence saying that the
   table was opening. The binding's `.pt-live` boundary already says exactly
   when the first authoritative frame exists, so the same boundary now owns a
   quiet loading chip and hides every inert example. An opening outage removes
   `aria-busy` and publishes the ordinary retry notice instead; the two states
   never compete.

   A real 32-character teammate name had only 22–32px left in the top seat's
   92px pill after padding, presence and `· خويك`. The seat is centred over a
   375–430px court with no neighbour on its row, so a bounded 148px pill buys
   78–88px for identity at supported phone heights without moving the cards or
   touching either side seat. The complete string remains DOM text and title. */
body.gm:not(.pt-live) .gm-sc,
body.gm:not(.pt-live) .gm-seat,
body.gm:not(.pt-live) .gm-mine,
body.gm:not(.pt-live) .gm-act { visibility: hidden; }

body.gm:not(.pt-live):not([data-connection="offline"])::after {
  content: "نجهّز الطاولة…";
  position: absolute;
  z-index: 40;
  inset: auto;
  left: 50%;
  inset-block-start: 49%;
  transform: translate(-50%, -50%);
  inline-size: max-content;
  min-block-size: 36px;
  padding: 7px 14px;
  display: flex;
  align-items: center;
  border: 1px solid rgba(var(--tx-rgb), .22);
  border-radius: 999px;
  color: var(--a2-text);
  background: color-mix(in srgb, var(--s-well) 90%, transparent);
  box-shadow: 0 8px 22px rgba(0, 0, 0, .34);
  font-size: 13px;
  font-weight: 800;
}

.gm-seat-top { inline-size: 148px; }
.gm-seat-top .gm-who {
  inline-size: 100%;
  max-inline-size: 100%;
}

/* ============================================================================
   39 · INDEPENDENT AUDIT ROUND 1 · truthful status, stable open, short glass
   ============================================================================
   The production-only round-1 audit found three presentation defects whose
   fixes belong together at the court edge rather than in the imported sheets.

   A recovered table changed from a small loading pseudo-element to the full
   court and scored CLS .914. The loading cover now owns the final fixed
   viewport from its first frame; only its paint disappears at `.pt-live`, so
   the unchanged 2.5s decoded-face gate moves no layout box.

   At 844x390 with a non-coarse pointer, the centred 430px portrait court is
   necessarily short. Keep that same court and zero-scroll posture, but fit the
   top seat into the free upper-right perimeter and move the trick cross into
   the free centre-left. The teammate label remains 12pt and the unified 5:7
   fan remains the same fan; only its remote scale and the played-card scale
   tighten. This is not a landscape layout and the coarse-pointer quarter-turn
   in §36 is untouched.

   Exact project declarations can be longer than ordinary action labels. Their
   server-vetted card list may wrap inside the existing button, without adding a
   picker or changing the count-aware rail. */
body.gm:not(.pt-live):not([data-connection="offline"])::after {
  position: fixed;
  inset: 0;
  left: auto;
  transform: none;
  inline-size: auto;
  min-block-size: 0;
  padding: 0;
  display: grid;
  place-items: center;
  contain: strict;
  pointer-events: none;
  border: 0;
  border-radius: 0;
  background: radial-gradient(
    ellipse 92px 30px at center,
    color-mix(in srgb, var(--s-well) 92%, transparent) 0 96%,
    transparent 100%
  );
  box-shadow: none;
  text-shadow: 0 2px 8px rgba(0, 0, 0, .7);
}

.gm-act > .btn[data-project="1"] {
  font-size: 12px;
  line-height: 1.15;
  white-space: normal;
  overflow: hidden;
}

@media (orientation: landscape) and (max-height: 600px) and (not (pointer: coarse)) {
  .gm-seat-top {
    inline-size: 105px;
    transform: translateX(calc(-50% + 80px));
  }
  .gm-seat-top .gm-backs {
    transform: scale(.72);
    transform-origin: top center;
  }
  .gm-trick {
    left: calc(50% - 45px);
    inset-block-start: 64%;
    transform: translate(-50%, -50%) scale(.84);
  }
}

/* 40. Captain's September document: one identity, no redundant headings.
   Imported sheets/art remain untouched. #turn stays in the accessibility tree. */
#turn {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
#contract, .gm-role { display: none; }
.card, .gm-backs > i { background-image: url("../assets/back-bone-v8.svg"); }
.gm-backs::after { content: none; display: none; }
.gm-seat[data-dealer="1"] .gm-node { box-shadow: 0 0 10px var(--ai-glow); }
.gm-seat[data-active="1"] .gm-who::after { content: none; }
.pt-seat-markers {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  font-size: 12px;
  line-height: 1.15;
  text-align: center;
  color: var(--tx);
}
.gm-seat > .pt-seat-markers {
  position: absolute;
  inset-block-start: calc(100% + 3px);
  inline-size: 100%;
}
.pt-seat-markers [data-kind="turn"], .pt-seat-markers [data-kind="winner"] {
  color: var(--a);
  font-weight: 900;
}
.pt-seat-markers [data-kind="declarer"] { font-weight: 800; }
.pt-seat-markers [data-kind="dealer"] { opacity: .72; }
.pt-seat-markers [data-kind="project"] { white-space: normal; overflow-wrap: anywhere; max-inline-size: 100%; }
#viewerIdentity { min-block-size: 16px; }
#viewerIdentity .pt-seat-markers { flex-direction: row; justify-content: center; gap: 10px; flex-wrap: wrap; }
.gm-backs.has-project > i { visibility: hidden; }
.pt-project-faces {
  position: absolute;
  inset: 0;
  display: flex;
  direction: ltr;
  justify-content: center;
  align-items: center;
  pointer-events: none;
}
.pt-project-faces .card {
  position: relative;
  flex: 0 0 40px;
  width: 40px;
  height: 56px;
  min-width: 0;
  margin: 0 0 0 -28px;
  transform: none;
  animation: none;
}
.pt-project-faces .card:first-child { margin-left: 0; }
.gm-seat-left .pt-project-faces .card, .gm-seat-right .pt-project-faces .card {
  margin-left: -34px;
}
.gm-seat-left .pt-project-faces .card:first-child, .gm-seat-right .pt-project-faces .card:first-child { margin-left: 0; }
.pt-score-breakdown { inline-size: 100%; border-collapse: collapse; margin-block-start: 10px; font-size: 13px; }
.pt-score-breakdown caption { font-weight: 800; margin-block-end: 4px; }
.pt-score-breakdown th { text-align: start; font-weight: 500; }
.pt-score-breakdown td { text-align: center; min-inline-size: 36px; }
.pt-score-breakdown th, .pt-score-breakdown td { padding: 3px 2px; border-bottom: 1px solid rgba(var(--tx-rgb), .12); }
.pt-score-note { font-size: 12px; line-height: 1.3; margin: 8px 0 0; color: var(--tx-2); white-space: normal; overflow-wrap: anywhere; }
body.has-outcome #viewerIdentity { display: none; }
body.has-outcome .pt-over { padding: 14px 18px; }
body.has-outcome .pt-over .gm-turn { margin-block: 4px; font-size: 22px; }
body.has-outcome .pt-over .gm-sc-v { font-size: 36px; }
#hand > .card.is-shown-project { outline: 2px dashed var(--a); outline-offset: 1px; }
#viewerIdentity [data-kind="project"] { font-size: 12px; flex-basis: 100%; }
/* Captain review: the hidden contract leaves four visible header items. */
.gm-top { grid-template-columns: 44px minmax(46px, 1fr) minmax(46px, 1fr) 44px; }

/* Short partner names use the same compact pill, long names stay bounded. */
.gm-seat-top .gm-who { inline-size: fit-content; min-inline-size: 70px; max-inline-size: 100%; padding-inline: 6px; align-self: center; }

/* Viewer identity shares the seat treatment without changing the hand lane. */
#viewerIdentity { position: relative; display: flex; align-items: center; justify-content: center; gap: 8px; block-size: 16px; }
#viewerIdentity .pt-viewer-who { min-inline-size: 70px; max-inline-size: 148px; flex: 0 1 auto; }
#viewerIdentity .pt-viewer-who[data-active="1"] { box-shadow: inset 0 0 0 1px var(--a), 0 0 12px rgba(var(--a-rgb), .22); }
#viewerIdentity .pt-seat-markers [data-kind="turn"] { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); }
body.has-outcome #viewerIdentity .pt-viewer-who { visibility: hidden; }

/* Declarer identity remains readable on the unchanged court. */
.pt-seat-markers [data-kind="declarer"] { background: var(--tx); color: var(--ink); border-radius: 6px; padding: 2px 5px; max-inline-size: 100%; }

/* Captain review: equal resting faces; legality and refusal stay in tapCard. */
body[data-phase="PLAY_TRICKS"].is-your-turn .gm-hand > .card[data-ok="0"] {
  filter: none; opacity: 1; cursor: pointer;
  transform: translateY(calc(var(--fan-arc, 0px) + var(--fan-rail-lift) + var(--fan-selection-lift, 0px))) rotate(var(--fan-rotation-live, var(--fan-rotation, 0deg)));
}
/* Keep the two-tap guidance and viewer narration available without visible copy. */
.gm-act.is-empty .gm-hint.pt-confirm,
#viewerIdentity .pt-seat-markers [data-kind="project"],
#viewerIdentity .pt-seat-markers [data-kind="winner"] {
  position: absolute; inline-size: 1px; block-size: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

/* All seats indicate turns through their existing pill lighting. */
.pt-seat-markers [data-kind="turn"],
.pt-seat-markers [data-kind="winner"],
.pt-say {
  position: absolute; inline-size: 1px; block-size: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

/* Captain review: a stable square court and fixed control reservation. */
body.gm {
  --review-header: 64px;
  --review-hand: 160px;
  --review-court: min(calc((min(100vw, 430px) - var(--a2-page-pad) * 2 + 4px) * .9), calc(100svh - 260px));
}
@media (max-height: 880px) { body.gm { --review-header: 60px; --review-hand: 145px; } }
@media (max-height: 740px) { body.gm { --review-header: 58px; --review-hand: 132px; } }
body.gm[class] {
  grid-template-rows: var(--review-header) minmax(0, 1fr) var(--review-hand) 108px !important;
}
.gm-table::before {
  inset: auto;
  left: 50%;
  top: calc(50svh - var(--a2-safe-t) - var(--review-header) - var(--review-court) / 2);
  inline-size: var(--review-court);
  block-size: var(--review-court);
  transform: translateX(-50%);
}
.gm-seat-left, .gm-seat-right {
  inset-block-start: calc(50svh - var(--a2-safe-t) - var(--review-header));
}
.gm-trick, .pt-over, .pt-wait {
  inset-block-start: calc(50svh - var(--a2-safe-t) - var(--review-header));
}
/* Keep names centered independently of the computer-role dot. */
.gm-who { position: relative; justify-content: center; }
.gm-who .gm-name { text-align: center; }
.gm-who .gm-node { position: absolute; inset-inline-start: 5px; }
.gm-seat .gm-who { padding-inline: 14px; }

/* Put the side name bars themselves on the court's middle line. */
.gm-seat-left, .gm-seat-right { transform: translateY(calc(-100% + 13.5px)); }
/* Modest, proportional viewer/played enlargement; remote backs keep their size. */
body.gm { --card-w: 75px; --a2-played-w: 64px; --a2-remote-w: 38.08px; }
@media (min-width: 415px) { body.gm { --card-w: 77px; --a2-remote-w: 39.2px; } }
@media (max-height: 740px) { body.gm { --card-w: 70px; --a2-played-w: 60px; --a2-remote-w: 35.84px; } }

/* Fit the square between the partner fan and viewer identity on short glass. */
body.gm {
  --review-court-top: calc(var(--a2-safe-t) + var(--review-header) + 100px);
  --review-court-bottom: calc(100svh - var(--a2-safe-b) - 108px - var(--review-hand) - 16px);
  --review-court: min(calc((min(100vw, 430px) - var(--a2-page-pad) * 2 + 4px) * .9), calc(var(--review-court-bottom) - var(--review-court-top)));
  --review-court-center: calc((var(--review-court-top) + var(--review-court-bottom)) / 2);
}
.gm-table::before { top: calc(var(--review-court-center) - var(--a2-safe-t) - var(--review-header) - var(--review-court) / 2); }
.gm-trick { inset-block-start: calc(var(--review-court-center) - var(--a2-safe-t) - var(--review-header)); }
.gm-seat-left, .gm-seat-right { inline-size: 84px; max-inline-size: 84px; }
.gm-seat-left .gm-who, .gm-seat-right .gm-who { inline-size: 84px; max-inline-size: 84px; }
.gm-seat-top .gm-who, #viewerIdentity .pt-viewer-who { min-inline-size: 84px; }
@media (min-width: 375px) { .gm-hand { margin-inline: 10px; } }

/* Keep the accepted larger faces lower, clear of viewer identity even selected. */
.gm-hand { --fan-rail-lift: 2px; }

/* Keep controls below the larger hand: single rows use the rail's lower edge. */
.gm-act { align-content: end; }
body.gm { --review-hand: 176px; }
@media (max-height: 880px) { body.gm { --review-hand: 161px; } }
@media (max-height: 740px) { body.gm { --review-hand: 148px; } }
.gm-hand { --fan-rail-lift: 8px; }
body.gm[data-action-count="5"] .gm-hand,
body.gm[data-action-count="6"] .gm-hand,
body.gm[data-action-count="7"] .gm-hand,
body.gm[data-action-count="8"] .gm-hand { --fan-rail-lift: -10px; }

.gm-act[data-count="5"] > .btn, .gm-act[data-count="6"] > .btn,
.gm-act[data-count="7"] > .btn, .gm-act[data-count="8"] > .btn {
  block-size: 44px; min-block-size: 44px;
}

/* The court is a large square beneath the players, not a content boundary. */
body.gm {
  --review-court: min(calc((min(100vw, 430px) - var(--a2-page-pad) * 2 + 4px) * .9), calc(100svh - 260px));
  --review-court-center: 50svh;
}
/* Reuse the lobby's exact violet brand-node silhouette. */
body.gm .gm-seat .gm-node {
  inline-size: 8px; block-size: 12px; border-radius: 0;
  background: var(--ai); box-shadow: none;
  -webkit-mask: url("../assets/saikal-node.png") center / contain no-repeat;
  mask: url("../assets/saikal-node.png") center / contain no-repeat;
}
/* Public projects sit separately from the intact remote back fans. */
.gm-backs.has-project > i { visibility: visible; }
.gm-seat > .pt-project-faces {
  inset: auto; left: 0; inline-size: 100%; block-size: 56px;
}
.gm-seat-left > .pt-project-faces, .gm-seat-right > .pt-project-faces { bottom: calc(100% + 8px); }
.gm-seat-top > .pt-project-faces { top: calc(100% + 44px); }
/* September comments: show the viewer's public project above the name, too.
   It shares the remote 40x56 decoded faces and five-second event lifetime. */
#viewerIdentity > .pt-project-faces {
  inset: auto; left: 50%; bottom: calc(100% + var(--viewer-project-gap, 16px));
  inline-size: min(280px, calc(100vw - 32px)); block-size: 56px;
  transform: translateX(-50%);
}
#viewerIdentity > .pt-project-faces .card { margin-left: -16px; }
#viewerIdentity > .pt-project-faces .card:first-child { margin-left: 0; }
/* If a stacked caption would meet the trick, split the announcement between
   the lower court corners. Each row shows at most four unchanged public faces. */
#viewerIdentity.has-compact-project > .pt-project-faces {
  left: 13px; bottom: calc(100% + 16px); transform: none;
  inline-size: calc(var(--shown-columns) * 24px + 16px); block-size: auto;
  display: grid; grid-template-columns: repeat(var(--shown-columns), 24px);
  grid-auto-rows: 56px; gap: 4px 0; justify-content: start;
}
#viewerIdentity.has-compact-project > .pt-project-faces .card { margin: 0; }
#viewerIdentity.has-compact-project > .pt-say[data-kind="project"] {
  left: 137px; right: 13px; bottom: calc(100% + 16px); top: auto;
  inline-size: auto; max-inline-size: none; transform: none;
}

/* Use the lower play space without sacrificing dense controls' clearances. */
body.gm { --review-lower: 12px; --review-court-center: calc(50svh + var(--review-lower)); }
.gm-seat-top { inset-block-start: calc(10px + var(--review-lower)); }
.gm-seat-left, .gm-seat-right { inset-block-start: calc(50svh - var(--a2-safe-t) - var(--review-header) + var(--review-lower)); }
.gm-act, .gm-act[data-count] { align-items: flex-end; padding-block-end: 0; }
.gm-hand { --fan-rail-lift: 20px; }
body.gm[data-action-count="5"] .gm-hand,
body.gm[data-action-count="6"] .gm-hand,
body.gm[data-action-count="7"] .gm-hand,
body.gm[data-action-count="8"] .gm-hand { --fan-rail-lift: -7px; }

/* iPhone review: larger viewer faces; played cards and backs retain their sizes. */
body.gm { --card-w: 84px; }
@media (min-width: 415px) { body.gm { --card-w: 86px; } }
@media (max-height: 740px) { body.gm { --card-w: 78px; } }
@media (min-width: 375px) { .gm-hand { margin-inline: 14px; } }
@media (min-width: 375px) and (max-width: 379px) { .gm-hand { margin-inline: 11px; } }

/* Keep viewer identity attached as the larger hand extends beneath the rail. */
.gm-mine { transform: translateY(16px); }
.gm-seat-left, .gm-seat-right { transform: translateY(-50%); }

/* All remote hands use upright cards with the name directly below. */
.gm-seat-top .gm-backs { order: -1; }
.gm-seat-left .gm-backs, .gm-seat-right .gm-backs {
  inline-size: calc(var(--a2-remote-w) * 2.8);
  block-size: calc(var(--a2-remote-h) + 14px);
}
.gm-seat-left .gm-backs > i, .gm-seat-right .gm-backs > i {
  inset-block-start: 2px;
  margin: 0 0 0 calc(var(--a2-remote-w) / -2);
  transform-origin: 50% 138%;
  transform: translateX(var(--back-spread, 0px)) translateY(var(--back-arc, 0px)) rotate(var(--back-rotation, 0deg));
}

/* Eight upright remote backs retain a small viewport gutter. */
.gm-seat-left { left: 14px; }
.gm-seat-right { right: 14px; }

/* Auction exposed face shares the accepted played-card size, not viewer size. */
.pt-exposed > .card {
  inline-size: var(--a2-played-w); min-inline-size: var(--a2-played-w);
  block-size: var(--a2-played-h); min-block-size: var(--a2-played-h);
}

/* Review spacing: draw the upper seats toward the court, lower own identity/hand. */
body.gm { --review-lower: 0px; }
.gm-seat-top { inset-block-start: 34px; }
.gm-mine { transform: translateY(24px); }
#viewerIdentity { transform: translateY(4px); }

/* Keep names just inside the edge; outer back tips may extend beyond it. */
.gm-seat-left { left: 0; }
.gm-seat-right { right: 0; }

/* Captain priority: visibly lower the whole hand/name, independent of buttons. */
.gm-mine { transform: translateY(56px); }
body.gm[data-action-count] .gm-hand { --fan-rail-lift: 20px; }

/* Selection rises in its existing slot above every other face, below hit lanes. */
.gm-hand > .card[data-sel="1"] { z-index: 20; }

/* Empty control space must not intercept the hand beneath it. */
.gm-act, .gm-act.is-empty { pointer-events: none; }
.gm-act > button { pointer-events: auto; }
.gm-hand > .card[data-sel="1"] { z-index: 70; }
.gm-hand > .card { pointer-events: auto; }

/* Larger readable faces retain the accepted bottom crop. The viewer name's
   group height stays fixed as the faces grow, preserving its court clearance. */
body.gm { --card-w: 108px; --review-viewer-height: 198px; --review-viewer-viewport: 100svh; --review-viewer-bottom: 30px; }
@media (min-width: 415px) { body.gm { --card-w: 114px; --review-viewer-height: 200.8px; } }
@media (max-height: 740px) { body.gm { --card-w: 100px; --review-viewer-height: 186.8px; } }
.gm-mine {
  position: absolute; left: 0; bottom: calc(-1 * var(--review-viewer-bottom)); top: auto;
  inline-size: 100%;
  /* Constrained browser chrome can shorten even a portrait phone. Keep the
     full name pill 8px below the court; extra face height crops at the bottom. */
  block-size: min(var(--review-viewer-height), calc(var(--review-viewer-viewport) - var(--review-court-center) - var(--review-court) / 2 + var(--review-viewer-bottom) - 14px));
  margin-inline: 0; transform: none;
}
.gm-act { grid-row: 4; }
#viewerIdentity { transform: none; }
/* The full fan uses the available width while keeping 5px treatment + 8px
   glass gutter. The lowered faces stay clear of the attached viewer name. */
/* 8px outside the 3px refusal ring plus its maximum 3px nudge. */
body.gm[data-action-count] .gm-hand, .gm-hand { --fan-rail-lift: 52px; margin-inline: 14px; }

/* Taller glass has room to lift the attached viewer group; short glass
   retains the lower anchor that keeps its whole name pill below the mat. */
@media (min-height: 741px) { body.gm { --review-viewer-bottom: 8px; } }

/* Leave warning remains legible over the deliberately lowered hand. */
.gm-act .gm-hint.pt-ask {
  background: var(--s-void); color: var(--tx);
  padding: 7px 12px; border-radius: 8px;
}

/* Public project faces carry the visual announcement themselves. */
.pt-seat-markers [data-kind="project"] { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.pt-project-faces .card { inline-size: 40px; block-size: 56px; min-inline-size: 40px; min-block-size: 56px; max-inline-size: 40px; max-block-size: 56px; flex: 0 0 40px; }
/* The owner's mark as he picked it (C, 2026-09-30: docs/logo/README.md), on the same quiet
   ground and in the same box, so nothing around it moves. mark.svg keeps room past its ink for
   the lamp's glow, so it is drawn on a layer larger than the box and centred on it: the numbers
   are the file's viewBox over its ink, which puts the ink exactly where his first mark filled
   the box, and `node docs/logo/tools/build.mjs --check` holds them to the file. His first mark,
   assets/saikal-mark.png, stays byte-identical as the imported record. The majlis lobby draws C
   on its own layer (styles/majlis-v15.css), so this one steps aside under that lobby: its
   centring transform would otherwise shift the majlis mark by half its size. */
.tak .stage .tkmark-img { background: radial-gradient(ellipse at center, rgba(10,11,18,.96) 0%, rgba(10,11,18,.82) 44%, rgba(10,11,18,0) 74%); }
html:not([data-majlis~="lobby"]) .tak .stage .tkmark-img::before {
  content: ""; position: absolute; left: 50%; top: 50%; pointer-events: none;
  inline-size: calc(100% * 707.97 / 511.97); aspect-ratio: 707.97 / 681.23;
  transform: translate(-50%, -50%);
  background: url("../assets/saikal-mark-v2/mark.svg") center / contain no-repeat;
}
.tak .tkfan > .tkcardbody { --dim: 1 !important; }
.tak .tkback.has-art > .fc { background: url("../assets/back-bone-v8.svg") center / cover no-repeat; }
.tak .ledger { margin-block-start: max(18px, calc(6px + 14.75 * var(--g))); }

.gm-seat .pt-project-faces .card { margin-left: -16px; }
.gm-seat .pt-project-faces .card:first-child { margin-left: 0; }

/* Captain-approved polished mat A; square geometry and image bytes stay fixed.
   Comments 2: source has a light outer 1–2px band (1254px square). A centred
   0.5% overscan crops ~3px per edge, within its plain navy rim. */
.gm-table::before {
  background-image: url("../game/mat-sadu-polished-v3.webp");
  background-size: 100.5% 100.5%;
  background-position: center;
  background-repeat: no-repeat;
  box-shadow: none;
  border: 0;
}

/* Gameplay is a fixed viewport; the approved low hand clips, never scrolls it. */
html:has(> body.gm) { overflow: hidden; overscroll-behavior: none; }
body.gm { position: fixed; inset: 0; overflow: clip; }

/* Captain-approved background A on both private-preview pages. */
html:has(> body.gm), .tak .tk-ground {
  background: linear-gradient(rgba(8,13,23,.77), rgba(8,13,23,.77)),
    url("../assets/sadu-woven-v1.jpg") 0 0 / 360px repeat;
  background-blend-mode: normal;
}
body.gm { background: transparent; box-shadow: none; }
body.gm::before, body.gm::after, .tak .tk-lamp, .tak .tk-fall { display: none; }

/* Inset the remote AI mark and clarify dealer identity over the mat. */
.gm-seat .gm-who .gm-node { inset-inline-start: 9px; }
.gm-seat .gm-who .gm-name { transform: translateX(-2px); }
.pt-seat-markers [data-kind="dealer"], .pt-seat-markers [data-kind="doubler"], .pt-seat-markers [data-kind="raise"] {
  opacity: 1; color: var(--ink); background: var(--tx);
  border-radius: 6px; padding: 2px 5px;
}

/* Hover is stationary; selection retains the natural fan's index order.
   Override the imported hover transforms here, rather than deleting them from
   the captain's byte-identical game.css. Same accepted resting geometry. */
.gm-hand > .card:hover:not([data-sel="1"]) {
  transform: translateY(calc(var(--fan-arc, 0px) + var(--fan-rail-lift))) rotate(var(--fan-rotation-live, var(--fan-rotation, 0deg)));
}
.gm-hand > .card[data-hover="1"]:not([data-sel="1"]) {
  box-shadow: 0 0 0 2px var(--a2-focus), 0 4px 10px rgba(0,0,0,.28);
}
.gm-hand > .card[data-sel="1"] { z-index: var(--z, 1); }

.gm-hand > .card[data-sel="1"] + .card { z-index: var(--z, 1); }

/* Actual bid/project declarations are visible speech, not turn narration. */
.pt-say {
  inline-size: max-content; block-size: auto; max-inline-size: min(44vw, 190px);
  margin: 0; padding: 7px 11px; overflow: visible; clip-path: none;
  white-space: normal; font-size: 18px; line-height: 1.35; font-weight: 800;
  background: var(--face); color: var(--tx); border: 1px solid rgba(var(--tx-rgb),.35);
  z-index: 30; left: 50%; right: auto; transform: translateX(-50%);
}
.pt-say[data-kind="auction"] {
  padding: 8px 13px; border: 1.5px solid var(--a);
  box-shadow: var(--sh2), 0 0 0 3px rgba(var(--a-rgb),.09);
}
.gm-seat-top .pt-say { top: calc(100% + 28px); bottom: auto; }
.gm-seat-right .pt-say { left: auto; right: 0; transform: none; }
.gm-seat-left .pt-say { left: 0; right: auto; transform: none; }
#viewerIdentity > .pt-say { bottom: calc(100% + 8px); top: auto; }

.gm-seat-left .pt-say[data-kind="project"], .gm-seat-right .pt-say[data-kind="project"] { bottom: calc(100% + 8px); }
.gm-seat-top .pt-say[data-kind="project"] { top: calc(100% + 8px); }

/* Preserve the accepted 9.8deg count-centred fan at every ordinary count.
   Its reserve scales with the larger faces; the 14px margin owns treatment + gutter.
   The established narrow-column eight-card exception remains flat. */
.gm-hand { --a2-hand-treatment-reserve: calc(var(--card-w) * .74); }
@media (max-width: 374px),
       (orientation: landscape) and (pointer: coarse) and (max-height: 374px) {
  #hand[data-count="8"] { --a2-hand-treatment-reserve: 0px; }
}

/* Keep the accepted portrait arrangement when a coarse phone rotates.
   The later inset:0 otherwise over-constrains a fixed-width RTL body: right
   wins and shifts the whole rotated court off the glass. Physical svh is the
   short side here, not the portrait column's height. No portrait change. */
@media (orientation: landscape) and (pointer: coarse) and (max-height: 600px) {
  body.gm {
    right: auto;
    bottom: auto;
    --review-court: min(calc((min(100svh, 430px) - var(--a2-page-pad) * 2 + 4px) * .9), calc(100svw - 260px));
    --review-court-center: calc(50svw + var(--review-lower));
    --review-viewer-viewport: 100svw;
  }
  .gm-seat-left, .gm-seat-right {
    inset-block-start: calc(var(--review-court-center) - var(--a2-safe-t) - var(--review-header));
  }
  .gm-trick { transform: translate(-50%, -50%); }
}

/* Sawa: turn held fans toward the table, never throw them into trick slots.
   Only accepted disclosure enables this state. Frozen decoded faces are intact. */
#sawaReveal {
  position: absolute; z-index: 35; pointer-events: none;
  inset: 0;
}
#table[data-sawa] .gm-backs { visibility: hidden; }
#table[data-sawa] .gm-hand { pointer-events: none; }
#sawaTitle {
  position: absolute;
  top: calc(var(--review-court-center) - var(--a2-safe-t) - var(--review-header));
  left: 50%; transform: translate(-50%, -50%);
  max-inline-size: 54%; margin: 0; padding: 4px 10px; border-radius: 9px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 20px; line-height: 1.3; color: var(--tx); background: var(--face);
  border: 1px solid var(--a); box-shadow: var(--sh2);
}
.pt-sawa-hand {
  position: absolute; direction: ltr; inline-size: 44px; block-size: 61.6px;
  transform: translate(-50%, -50%); perspective: 500px;
}
/* Placement is atomic. The imported reduced-motion rule gives EVERY element
   a .01ms transition; without this, a fresh fan briefly paints at its old 13%
   anchor/spread before the measured bounds take effect. Keep the opening
   keyframe, not an accidental second layout/transform animation. */
.pt-sawa-hand, .pt-sawa-hand > .card { transition-property: none; }
.pt-sawa-hand[data-position="top"] { top: 80px; left: 50%; }
.pt-sawa-hand[data-position="bottom"] { display: none; } /* actual viewer fan stays */
.pt-sawa-hand[data-position="left"] { top: 49%; left: 13%; }
.pt-sawa-hand[data-position="right"] { top: 49%; left: 87%; }
.pt-sawa-hand > .card {
  position: absolute; inset: 0; inline-size: 44px; block-size: 61.6px; border-radius: 4px;
  transform-origin: 50% 115%;
  transform: translate(var(--sawa-x), var(--sawa-arc)) rotate(var(--sawa-angle));
  box-shadow: var(--sh2);
}
.pt-sawa-button { min-inline-size: 78px; }
@media (prefers-reduced-motion: no-preference) {
  .pt-sawa-hand > .card { animation: pt-sawa-open 560ms var(--ease-out) both; }
  /* Captain 2026-09-13: the claimant's own view performs the reveal too. The
     hand is a SIBLING of `#table` (see `game/index.html`), so the marker is
     read through `:has`; a `#table ... .gm-hand` descendant selector can never
     match it. The fan leans its top edge away and lifts, the held-fan gesture
     that shows the cards to the table. Observers keep the claimant's unfolding
     strip and their own fan still, so only the claimant's own hand moves. */
  body:has(#table[data-sawa-own="1"]) .gm-hand { animation: pt-sawa-own 620ms var(--ease-out) both; }
}
body:has(#table[data-sawa-own="1"]) .gm-mine { perspective: 640px; perspective-origin: 50% 100%; }
body:has(#table[data-sawa-own="1"]) .gm-hand { transform-origin: 50% 100%; }
@keyframes pt-sawa-open {
  from { transform: translate(calc(var(--sawa-x) * .65), 4px) rotate(var(--sawa-angle)) rotateX(65deg); }
  to { transform: translate(var(--sawa-x), var(--sawa-arc)) rotate(var(--sawa-angle)) rotateX(0deg); }
}
@keyframes pt-sawa-own {
  from { rotate: x 0deg; translate: 0 0; }
  55% { rotate: x 42deg; translate: 0 -10px; }
  to { rotate: x 34deg; translate: 0 -8px; }
}

/* صكاتي's replay at the table (docs/majlis/my-games.md, game.js
   `startHistoryReplay`): the live table from the player's own seat, every
   other seat holding its own live fan with the cards' faces showing. */
/* The rail's two rows: the hand and the step to jump to, then السابقة,
   «أرسل ملاحظتك» and التالية. A drop-down wears its rail button; the phone's own
   list opens. The rail lets touches through to the hand (`.gm-act`, above) and
   takes back only its buttons, so a drop-down, a label, takes them back too. */
body[data-history-replay] .gm-act > .pt-pick { flex-basis: calc(50% - 4px); }
/* «أرسل ملاحظتك» is the rail's longest word: it takes the room السابقة and
   التالية, the same width as each other, do not need. */
body[data-history-replay] .gm-act > .btn:not(.pt-pick) { flex: 0 0 calc(27% - 4px); }
body[data-history-replay] .gm-act > .btn.pt-report { flex: 1 1 auto; }
.gm-act > .pt-pick {
  pointer-events: auto;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  white-space: nowrap;
}
.gm-act > .pt-pick > span { overflow: hidden; text-overflow: ellipsis; }
.gm-act > .pt-pick::after {
  content: "";
  flex: none;
  inline-size: 7px;
  block-size: 7px;
  margin-block-start: -4px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  rotate: 45deg;
  opacity: .8;
}
.gm-act > .pt-pick > select {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  opacity: 0;
  font-size: 16px; /* under 16px iOS zooms the page into the list */
  cursor: pointer;
}
.gm-act > .pt-pick:has(> select:focus-visible) {
  outline: 2px solid var(--mj-gold-300, var(--a-hi));
  outline-offset: 3px;
}
/* The result has no hand to hold: its table keeps the room the hand row had. */
body.gm[data-history-replay].has-outcome { grid-template-rows: 64px minmax(0, 1fr) 0 108px; }
@media (max-height: 740px) {
  body.gm[data-history-replay].has-outcome { grid-template-rows: 58px minmax(0, 1fr) 0 108px; }
}

/* The lobby spends tall-screen space on the notebook before a blank footer
   gap. Short browser glass reduces decoration and panel overhead first; every
   control retains 44px and the join row keeps a stable reservation. */
body.tak:has(#lobbyPanel) { min-block-size: var(--app-vh, 100dvh); overscroll-behavior-y: none; }
#lobbyPanel {
  --vh: calc(var(--app-vh, 100dvh) - var(--sa-t) - var(--sa-b));
  --lobby-notice: 0px;
  --lobby-fit: calc(var(--vh) - var(--lobby-notice));
  --lobby-gap: clamp(4px, calc(6px + (var(--lobby-fit) - 540px) * .03), 12px);
  --lobby-field: clamp(44px, calc(var(--lobby-fit) - 530px), 48px);
  --lobby-mark: clamp(56px, calc(64px + (var(--lobby-fit) - 540px) * .4), 104px);
  block-size: var(--app-vh, 100dvh); min-block-size: var(--app-vh, 100dvh);
  padding-block: var(--sa-t) calc(var(--sa-b) + 8px);
}
#lobbyPanel:has(#lobbyNotice:not([hidden])) { --lobby-notice: 80px; }
#lobbyPanel .stage {
  flex: 1 0 auto; justify-content: center; padding-block-start: 0;
  margin-block-end: clamp(6px, calc((var(--lobby-fit) - 540px) * .1), 28px);
}
#lobbyPanel .stage .tkmark-img {
  inline-size: var(--lobby-mark);
}
/* Keep the three decorative card tops inside browser glass. Their deliberate
   left-edge bleed remains, but negative imported offsets no longer crop the
   whole fan vertically when there is no installed-app top safe area. */
#lobbyPanel .stage .tkfan {
  --fan-bw: calc(var(--lobby-mark) * .56);
  top: calc((100% - var(--lobby-mark) * 499 / 512) / 2 + var(--fan-bw) * .58);
}
#lobbyPanel .panel > .pin {
  padding-block: clamp(4px, calc(6px + (var(--lobby-fit) - 540px) * .015), 12px);
  row-gap: var(--lobby-gap);
}
#lobbyPanel .panel .tkfield + .tkbite { margin-block-start: 0; }
#lobbyPanel .panel .tkseats {
  /* The accepted plain panel has no notch to reserve beside this sentence. */
  padding-inline: 0;
  min-block-size: clamp(26px, calc(26px + (var(--lobby-fit) - 540px) * .05), 40px);
}
#lobbyPanel .tkfield-in {
  block-size: var(--lobby-field); min-block-size: var(--lobby-field);
  padding-block: 6px;
}
#lobbyPanel .tkfield-lb { min-block-size: 44px; }
#lobbyPanel .panel .tkb-1 {
  min-block-size: clamp(64px, calc(64px + (var(--lobby-fit) - 600px) * .08), 88px);
  padding-block: 6px;
}
#lobbyPanel .panel .tkb-2 { min-block-size: 50px; padding-block: 4px; }
#lobbyPanel .roomfold { min-block-size: calc(48px + var(--lobby-field)); }
#lobbyPanel .roomfold > summary {
  block-size: 44px; min-block-size: 44px; padding-block: 0;
}
#lobbyPanel .roomfold .joinrow { margin-block-start: 4px; }
#lobbyPanel .joinrow .tkb-q { min-block-size: 44px; }
#lobbyPanel .ledger {
  margin-block-start: clamp(8px, calc(8px + (var(--lobby-fit) - 600px) * .025), 14px);
  min-block-size: clamp(78px, calc(84px + (var(--lobby-fit) - 580px) * .42), 184px);
  padding-block: clamp(3px, calc(6px + (var(--lobby-fit) - 580px) * .045), 18px);
  align-content: center;
  row-gap: clamp(2px, calc(4px + (var(--lobby-fit) - 580px) * .035), 12px);
}
#lobbyPanel .ledger h2 {
  font-size: clamp(18px, calc(18px + (var(--lobby-fit) - 580px) * .025), 24px);
}
#lobbyPanel .ledger .n {
  font-size: clamp(20px, calc(22px + (var(--lobby-fit) - 580px) * .04), 32px);
}
#lobbyPanel .grow { flex: 0 1 12px; }
#lobbyPanel #lobbyHelp { flex: 0 0 44px; }
#lobbyPanel #lobbyHelp > a {
  display: inline-flex; align-items: center; min-block-size: 44px;
  padding-block: 0;
}
#lobbyPanel #lobbyNotice { margin-block: 0; padding-block: 4px; }
#lobbyPanel #lobbyNotice[hidden] { display: none; }

/* A keyboard can leave less glass than the complete lobby's 44px controls
   require. Let the focused form use normal document panning in that editing
   posture; never trap a focused name/code input inside a clipped fixed box. */
@media (max-height: 539px) {
  html:has(#lobbyPanel input:focus) { overflow-y: auto; }
  #lobbyPanel:has(input:focus) {
    block-size: auto; min-block-size: var(--app-vh, 100dvh);
    overflow-y: visible;
  }
}

/* ============================================================================
   45 · September feedback round 2: a slightly higher, more upright viewer fan.
   Start from the landed hand's position and lift by about 5% of visible height.
   Larger width-based faces still bleed below the glass; only the upper table
   gives up space when browser chrome shortens the available portrait viewport.
   ========================================================================== */
body.gm {
  --table-height: var(--app-vh, 100dvh);
  --table-width: min(var(--app-vw, 100dvw), 430px);
  --card-w: clamp(114px, calc(var(--table-width) * .3128), 132px);
  --review-header: 56px;
  --review-rail: 108px;
  /* The previous composition is the height reference, not a second layout. */
  --review-baseline-card: clamp(100px, calc(100px + (var(--table-height) - 740px)), 108px);
  --review-baseline-hand: clamp(112px, calc(112px + (var(--table-height) - 740px)), 126px);
  --review-baseline-height: clamp(186.8px, calc(186.8px + (var(--table-height) - 740px)), 198px);
  --review-baseline-bottom: clamp(8px, calc(30px - (var(--table-height) - 740px)), 30px);
  --review-baseline-court: min(calc((var(--table-width) - var(--a2-page-pad) * 2 + 4px) * .9), calc(var(--table-height) - 260px));
  --review-baseline-start: max(calc(var(--table-height) + var(--review-baseline-bottom) - var(--review-baseline-height)), calc(var(--table-height) / 2 + var(--review-baseline-court) / 2 + 14px));
  --review-baseline-card-top: calc(var(--review-baseline-start) + 78px + var(--review-baseline-hand) - var(--review-baseline-card) * 1.4);
  --review-dense-lift: 0px;
  --review-resting-card-top: max(calc(var(--review-baseline-card-top) - var(--table-height) * .05), calc(var(--table-height) - var(--card-h) + 8px));
  --review-card-top: max(calc(var(--review-resting-card-top) - var(--review-dense-lift)), calc(var(--table-height) - var(--card-h) + 8px));
  --review-viewer-height: calc(var(--card-h) + 54px);
  /* Balance the complete upper composition, not the partner in isolation:
     header-to-partner air equals mat-to-viewer air, with the partner's name
     attached 12px above the court. The face fan and name own their real height. */
  --review-partner-scale: 1;
  --review-partner-height: calc(var(--a2-remote-h) + 45px);
  --review-partner-gap: 12px;
  --review-upper-start: calc(var(--a2-safe-t) + var(--review-header));
  --review-viewer-name-top: calc(var(--review-card-top) - 48.5px);
  --review-court-top: calc(var(--review-upper-start) + 8px + var(--review-partner-height) + var(--review-partner-gap));
  --review-court-bottom: calc(var(--review-viewer-name-top) - 8px);
  --review-court: min(calc((var(--table-width) - var(--a2-page-pad) * 2 + 4px) * .9), calc(var(--review-court-bottom) - var(--review-court-top)));
  --review-court-center: calc((var(--review-upper-start) + var(--review-viewer-name-top) + var(--review-partner-height) + var(--review-partner-gap)) / 2);
  block-size: var(--table-height);
  min-block-size: 0;
}
/* Dense menus may cover the foot of the fan, but never its rank/suit strip.
   Lift only the missing readable space and retain the same bottom-bleed cap. */
body.gm[data-action-count="5"], body.gm[data-action-count="6"],
body.gm[data-action-count="7"], body.gm[data-action-count="8"] {
  --review-dense-lift: max(0px, calc(var(--review-resting-card-top) - var(--table-height) + var(--a2-safe-b) + 126px));
}
@media (min-width: 415px) {
  body.gm {
    --review-baseline-card: clamp(100px, calc(100px + (var(--table-height) - 740px)), 114px);
    --review-baseline-height: clamp(186.8px, calc(186.8px + (var(--table-height) - 740px)), 200.8px);
  }
}
/* Preserve the existing compact remote-seat scale and account for it in the
   same composition budget; viewer faces never inherit this scale. */
@media (max-height: 740px) {
  body.gm {
    --review-partner-scale: .9;
    --review-partner-height: calc((var(--a2-remote-h) + 45px) * .9);
  }
}
body.gm[class] {
  grid-template-rows: var(--review-header) minmax(0, 1fr) 0 var(--review-rail) !important;
}
.gm-mine {
  top: calc(var(--review-card-top) - 43px);
  bottom: auto;
  block-size: var(--review-viewer-height);
}
body.gm[data-action-count] .gm-hand, .gm-hand {
  --fan-rail-lift: 17px;
  --a2-hand-treatment-reserve: calc(var(--card-w) * .46);
  block-size: var(--card-h);
}
.gm-seat-top {
  inset-block-start: calc(var(--review-court-center) - var(--review-court) / 2 - var(--a2-safe-t) - var(--review-header) - var(--review-partner-gap));
  transform-origin: 50% 100%;
  transform: translate(-50%, -100%) scale(var(--review-partner-scale));
}
.gm-seat-left, .gm-seat-right {
  inset-block-start: calc(var(--review-court-center) - var(--a2-safe-t) - var(--review-header));
}
.gm-trick { transform: translate(-50%, -50%); }
.pt-over, .pt-wait {
  inset-block-start: calc(var(--review-court-center) - var(--a2-safe-t) - var(--review-header));
}
@media (max-width: 374px),
       (orientation: landscape) and (pointer: coarse) and (max-height: 374px) {
  #hand[data-count="8"] { --a2-hand-treatment-reserve: 0px; }
}
@media (orientation: landscape) and (pointer: coarse) and (max-height: 600px) {
  body.gm {
    --table-height: var(--app-vw, 100dvw);
    --table-width: min(var(--app-vh, 100dvh), 430px);
    --review-baseline-card: clamp(100px, calc(100px + (var(--table-height) - 740px)), 108px);
    --review-baseline-height: clamp(186.8px, calc(186.8px + (var(--table-height) - 740px)), 198px);
    inline-size: var(--table-width);
    block-size: var(--table-height);
    transform: translateX(var(--app-vw, 100dvw)) rotate(90deg);
  }
  html[data-orient="landscape-secondary"] body.gm {
    transform: translateY(var(--app-vh, 100dvh)) rotate(-90deg);
  }
}

/* Public faces stay at their original 40x56 size. The partner shows them in
   the free upper corners beside its fan; a compact viewer show uses the two
   lower corners on one row, so neither declaration can cover a played card
   or another seat's name. The existing five-second disclosure owns visibility. */
.gm-seat-top > .pt-project-faces,
#viewerIdentity.has-compact-project > .pt-project-faces {
  inline-size: calc(var(--table-width) - 26px);
  block-size: 56px;
  display: grid;
  grid-template-columns: repeat(3, 24px) 40px minmax(0, 1fr) repeat(3, 24px) 40px;
  grid-template-rows: 56px;
  gap: 0;
  align-items: start;
  justify-content: stretch;
}
.gm-seat-top > .pt-project-faces { top: 0; bottom: auto; left: 50%; transform: translateX(-50%); }
#viewerIdentity.has-compact-project > .pt-project-faces { left: 13px; }
.gm-seat-top > .pt-project-faces .card,
#viewerIdentity.has-compact-project > .pt-project-faces .card { margin: 0; grid-row: 1; }
.gm-seat-top > .pt-project-faces .card:nth-child(5),
#viewerIdentity.has-compact-project > .pt-project-faces .card:nth-child(5) { grid-column: 6; }
.gm-seat-top > .pt-project-faces .card:nth-child(6),
#viewerIdentity.has-compact-project > .pt-project-faces .card:nth-child(6) { grid-column: 7; }
.gm-seat-top > .pt-project-faces .card:nth-child(7),
#viewerIdentity.has-compact-project > .pt-project-faces .card:nth-child(7) { grid-column: 8; }
.gm-seat-top > .pt-project-faces .card:nth-child(8),
#viewerIdentity.has-compact-project > .pt-project-faces .card:nth-child(8) { grid-column: 9; }
/* Side disclosures wrap four faces per row inside their own viewport edge. */
.gm-seat-left > .pt-project-faces, .gm-seat-right > .pt-project-faces {
  inline-size: 112px; block-size: auto;
  display: grid; grid-template-columns: repeat(3, 24px) 40px;
  grid-auto-rows: 56px; gap: 4px 0;
}
.gm-seat-left > .pt-project-faces { left: 2px; right: auto; }
.gm-seat-right > .pt-project-faces { left: auto; right: 2px; }
.gm-seat-left > .pt-project-faces .card,
.gm-seat-right > .pt-project-faces .card { margin: 0; }


/* September 11: a turn is visible at a glance, without moving any seat/card. */
.gm-seat[data-active="1"] .gm-who,
#viewerIdentity .pt-viewer-who[data-active="1"] {
  background: color-mix(in srgb, var(--a) 32%, var(--s-well));
  box-shadow: inset 0 0 0 2px var(--a), 0 0 0 2px rgba(var(--a-rgb), .55),
    0 0 20px 4px rgba(var(--a-rgb), .42);
}

@keyframes round2-turn-glow {
  0%, 100% { box-shadow: inset 0 0 0 2px var(--a), 0 0 0 2px rgba(var(--a-rgb), .4), 0 0 10px 1px rgba(var(--a-rgb), .22); }
  50% { box-shadow: inset 0 0 0 2px var(--a), 0 0 0 2px rgba(var(--a-rgb), .8), 0 0 24px 5px rgba(var(--a-rgb), .58); }
}
@media (prefers-reduced-motion: no-preference) {
  .gm-seat[data-active="1"] .gm-who,
  #viewerIdentity .pt-viewer-who[data-active="1"] {
    animation: round2-turn-glow 1.6s ease-in-out infinite;
  }
}

/* Comments 2: refusal must not replace the fan transform or its natural layer.
   Independent translate adds a small in-place nudge to the existing arc/lift. */
@keyframes comments2-deny {
  0%, 100% { translate: 0 0; }
  25% { translate: -3px 0; }
  75% { translate: 3px 0; }
}
.gm-hand > .card[data-deny="1"] {
  z-index: var(--z, 1);
  animation: none;
  box-shadow: 0 0 0 3px var(--suit-dark);
}
@media (prefers-reduced-motion: no-preference) {
  .gm-hand > .card[data-deny="1"] {
    animation: comments2-deny 320ms ease-in-out;
  }
}
/* Bids and passes share a light, opaque ground. Suit glyphs, not the whole
   call, take conventional colors so black suits stay legible on navy. */
/* Captain clarification: announcements only; interactive choices retain their
   original navy/gold palette, including all native focus/pending affordances. */
/* EKKA relief uses this same accepted announcement ground (MAIN042).
   The public event gate and interactive control palette are unchanged. */
.pt-say[data-kind="auction"], .pt-say[data-kind="project"], .pt-say[data-kind="relief"] {
  background: var(--tx); color: var(--ink);
}

/* Durable host only: explicit SOLO resume, added by lobby.js, never a DUO picker.
   MAIN152: adjacent equal-width controls, exact captain label كمل صكتك.
   A removed/ineligible resume leaves the ordinary SOLO button full width. */
.pt-solo-controls { display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; }
.pt-solo-controls:has(> #resumeSoloButton) { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.pt-solo-controls > #soloButton,
.pt-solo-controls > #resumeSoloButton { min-inline-size: 0; inline-size: 100%; margin: 0; }
.pt-solo-controls > #resumeSoloButton { padding-inline: 8px; }
.pt-solo-resume {
  min-block-size: 44px; inline-size: 100%; margin-block: 6px;
  border: 1px solid var(--a); border-radius: 12px;
  background: transparent; color: var(--tx); font: inherit; cursor: pointer;
}
.pt-solo-resume:focus-visible { outline: 2px solid var(--a); outline-offset: 2px; }
.pt-solo-resume:disabled { cursor: wait; opacity: .6; }

/* Captain comparison: reclaim the 14px margin and permit about 6px of face
   past each phone edge, symmetrically. Only 5–8-card tracks spend this budget;
   sizes, selection lift and independently bounded hit lanes remain unchanged. */
.gm-hand { --a2-hand-edge-bleed: 20px; }

/* Collected tricks: one fixed pile per PLAYER, on the same square as the mat.
   Captain comparison023: place each pile on the EXISTING woven golden band,
   beside its owner rather than over their fan. Comparison025 insets the pile
   another4% of mat width so rotated backs rest securely inside the edge.
   No new frame or art is introduced; the hand's separate overflow is unchanged.
   MAIN054: retain four backs per completed trick, all eight tricks. Empty anchors stay
   measurable so the very first collection lands at the same destination. */
.pt-taken-surface {
  position: absolute; left: 50%;
  top: calc(var(--review-court-center) - var(--a2-safe-t) - var(--review-header) - var(--review-court) / 2);
  inline-size: var(--review-court); block-size: var(--review-court);
  transform: translateX(-50%); pointer-events: none; z-index: 2;
}
.pt-taken-pile {
  position: absolute; inline-size: min(calc(var(--review-court) * .09), 30px);
  aspect-ratio: 5 / 7; transform: translate(-50%, -50%);
  pointer-events: none;
}
.pt-taken-pile[data-position="bottom"] { left: 74%; top: 88%; }
.pt-taken-pile[data-position="right"] { left: 88%; top: 26%; }
.pt-taken-pile[data-position="top"] { left: 26%; top: 12%; }
.pt-taken-pile[data-position="left"] { left: 12%; top: 74%; }
.pt-taken-pile[data-present="0"] { opacity: 0; }
.pt-taken-card {
  display: block; position: absolute; inset: 0; border-radius: 4px;
  background-color: var(--s3); background-size: 100% 100%; background-repeat: no-repeat;
  box-shadow: var(--sh1);
  transform: translate(var(--pile-x, 0px), var(--pile-y, 0px)) rotate(var(--pile-turn, 0deg));
  transition: transform 180ms ease-out;
}
/* Captured cards keep their original .card and face Image nodes. The face is
   hidden only at the flip's edge; the same card paints the held back thereafter.
   Flight and resting wrappers share geometry, with no substitute mini-card. */
.pt-collect-card, .pt-taken-card { pointer-events: none; }
.pt-collect-card > .card, .pt-card-turn > .card, .pt-taken-card > .card {
  position: absolute; inset: 0; width: 100%; height: 100%;
  inline-size: 100%; block-size: 100%; margin: 0;
  animation: none; transition: none; transform: none;
  border-radius: inherit;
}
.pt-collect-card > .card > .face, .pt-card-turn > .card > .face, .pt-taken-card > .card > .face {
  width: 100%; height: 100%; object-fit: fill;
}
@media (prefers-reduced-motion: reduce) {
  .pt-taken-card { transition: none; }
}

/* A played card is carried by the one shared flight in `game/card_motion.js`.
   That flight measures its own origin, so the slot's CSS lay keyframe is
   suppressed for the card it delivered: the marker is set on the frame that
   starts the flight and lives until the slot's next card arrives, which keeps
   the release (or any later unchanged-token frame) from replaying the keyframe
   from a stale offset. The flight itself paints and releases the resting card. */
@media (prefers-reduced-motion: no-preference) {
  .gm-slot[data-motion="1"] > .card { animation: none; }
}
/* A slot's chronological rank is discrete, not motion. The imported universal
   reduced-motion .01ms duration otherwise transitions its new z-index through
   the previous rank for a painted frame. Flights and child-card motion keep
   their existing owners; the stationary stacking wrapper never interpolates. */
.gm-slot { transition-property: none; }


/* Saikal v2.02 release (captain, 2026-09-19): the one-time announcement that the
   notebooks were reset, mounted by `release_notice.js`. It is written across the
   notebook plate it is about. The lobby column is exactly one screen tall (see
   the support line above), so a line added to it is a scrollbar, and a line
   floated over it covers the join field on a 320pt phone; the plate holds no
   control, so covering it costs nothing, and the whole plate is the one-tap
   dismiss. Every colour is the lobby's own token. */
.ledger > .pt-release {
  position: absolute;
  inset: 0;
  z-index: 4;
  inline-size: 100%;
  min-block-size: 44px;
  margin: 0;
  padding: var(--k2, 8px) var(--k3, 12px);
  border: 0;
  border-radius: inherit;
  background: linear-gradient(180deg, var(--s-panel-hi), var(--s-panel-lo));
  box-shadow: inset 0 0 0 1px rgba(var(--tx-rgb), .16);
  color: var(--tx);
  font-family: var(--fu);
  font-size: clamp(.95rem, calc(.95rem + 1.1 * var(--g, 0px)), 1.0625rem);
  font-weight: var(--w-control, 800);
  line-height: 1.5;
  text-align: center;
  direction: rtl;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  animation: pt-release-in 240ms ease-out both;
}
.pt-release[hidden] { display: none; }
/* Saikal v2.03 (captain, 2026-09-20): his words are two lines, and they carry
   Latin tokens (`v2.03`, `60%`) inside the Arabic. Each line is its own block;
   each token is a left-to-right isolate, stated here as well as by `<bdi dir>`
   so the order does not rest on a UA stylesheet. The size steps down once so the
   longer second line holds on one row of a 320pt plate where it can, and wraps
   inside the plate - never past it - where it cannot. */
.pt-release-line { display: block; text-wrap: balance; }
.pt-release-line + .pt-release-line {
  font-size: .86em;
  font-weight: var(--w-body, 600);
}
.pt-release bdi { unicode-bidi: isolate; direction: ltr; }
@keyframes pt-release-in { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .ledger > .pt-release { animation: none; }
}

/* ============================================================================
   46 · September feedback round 3: shown projects rest on display
   ============================================================================
   A shown project stays on display while play goes on, for as long as an إكة
   is read, rather than being picked straight back up.
   The render owns where a shown card is: on the display, so not also in the
   hand or the fan behind it. A card played off the display leaves it for the
   mat (`data-gone`) without the display being rebuilt under its flight. */
#hand > .card.is-shown-project { visibility: hidden; pointer-events: none; }
.gm-backs > i.is-on-show { visibility: hidden; }
.pt-project-faces > .card[data-gone="1"] { visibility: hidden; }

/* ============================================================================
   47 · September feedback round 3: a prepared card rises half its height
   ============================================================================
   The first tap on a card prepares it: it now rises about half its height
   instead of about a tenth, and the cards beside it part a little to make room,
   both on the same eased transition. Only paint moves - the 44pt hit lanes stay
   where the resting fan put them, and the raised face keeps its own pointer
   target - so nothing shifts under a finger between the two taps. The parting
   uses the individual `translate` property, which composes with each card's
   fan transform rather than replacing it. Reduced motion keeps the pose and
   drops the travel (§36's transition-duration rule). */
.gm-hand > .card {
  translate: var(--fan-part, 0px) 0;
  transition: transform var(--t-up, 180ms) var(--ease-out, ease-out),
    translate var(--t-up, 180ms) var(--ease-out, ease-out),
    box-shadow var(--t-tint, 120ms) linear;
}
.gm-hand > .card[data-sel="1"] {
  --fan-selection-lift: calc(var(--card-h, 150px) * -.5);
}
/* The first-turn window keeps the same half-height rise: a prepared card
   under the project controls passes beneath them, and the pills stay painted
   above it and keep their own hit areas (§P1's strip is above the hand). */
.gm-hand:has(> .card[data-sel="1"]) > .card:has(~ .card[data-sel="1"]) { --fan-part: -10px; }
.gm-hand > .card[data-sel="1"] ~ .card { --fan-part: 10px; }
@media (prefers-reduced-motion: reduce) {
  .gm-hand > .card { transition-duration: .01ms; }
}

/* ============================================================================
   48 · September feedback round 3: the whole hand stays on the glass
   ============================================================================
   The hand's faces had grown past the screen - below its bottom edge and a
   few pixels past each side - so part of every card and of every motion from
   the hand was off screen. The viewer's cards are a little smaller and rest
   on the glass: the whole face, the fan's outer tilt and the prepared card's
   half-height rise stay in view at phone and desktop sizes. Only the viewer's
   hand changes; played, shown and remote cards keep their sizes. The upper
   composition (§45) is computed from this resting line, so it follows. */
body.gm {
  --card-w: clamp(96px, calc(var(--table-width) * .255), 108px);
  /* The fan's rail lift, its own translate and the outer tilt add about 16px
     below the resting line; the whole painted face ends above the safe area. */
  --review-resting-card-top: calc(var(--table-height) - var(--a2-safe-b) - var(--card-h) - 14px + var(--hand-edge-crop));
  /* Keep the original safe-inset compensation and zero-crop override. §55's
     lift is subtracted here: the face rests on the glass and the stand's lip
     holds only its far bottom edge; rank/suit and the dense rail retain their
     clearance. A tall
     portrait table's mat is width-bound, so its hand rises further for free;
     elsewhere 28px is what the mat gives up. */
  --hand-stand-lift: clamp(28px, calc(28px + var(--table-height) - var(--table-width) * 1.95), 44px);
  --hand-edge-crop: calc(var(--card-h) * .18 - var(--hand-stand-lift) + max(0px, var(--a2-safe-b) - 8px));
  /* §45 floored the line at a bottom bleed; the floor is what put faces off
     screen, so the resting line itself is the position now. */
  --review-card-top: calc(var(--review-resting-card-top) - var(--review-dense-lift));
}
/* One extra pixel of track reserve (half per edge) keeps the selected/refused
   treatment's existing 8px outer gutter at 360px/five cards, too. Face size,
   fan angles, selection lift and the independent hit lanes are unchanged. */
body.gm[data-action-count] .gm-hand, .gm-hand { --a2-hand-treatment-reserve: calc(var(--card-w) * .86 + 1px); }

/* ============================================================================
   49 · September feedback round 3: the viewer's name beside the controls
   ============================================================================
   The name tag is centred on the two rows of project controls (§P1: 36px
   pills, 9px apart, the lower row ending on the band's baseline), and the
   role labels - الموزّع, the bought contract - sit in one row directly below
   the name instead of beside it, so with both labels nothing reaches the
   controls. The same structure holds with two labels, one or none, so the
   name never jumps. Only the name and its labels move: the speech bubble and the
   project display above keep their anchors. */
#viewerIdentity { --name-lift: -27px; --viewer-name-rise: 27px; }
/* The court is sized to end above the viewer's name; the name now sits 27px
   higher, so the court's budget ends 27px higher too (only where the court is
   height-bound - short phones - does the mat actually get smaller). */
body.gm { --review-viewer-name-top: calc(var(--review-card-top) - 48.5px - 27px); }
#viewerIdentity .pt-viewer-who { transform: translateY(var(--name-lift)); }
#viewerIdentity .pt-seat-markers {
  position: absolute;
  left: 50%;
  top: calc(50% + var(--name-lift) + 13.5px + 3px);
  transform: translateX(-50%);
  flex-direction: row;
  align-items: center;
  flex-wrap: nowrap;
  gap: 6px;
  white-space: nowrap;
  pointer-events: none;
}

/* ============================================================================
   50 · The viewer's announcements clear the name, roles and controls
   ============================================================================
   A bid, a project declaration, إكة, بلوت or سوا said by the viewer rises from
   above the lifted name tag (§49), never over it or the role row below it.
   While the project controls are up it is also kept narrower than the room
   left of them, so a long call wraps instead of running under a button; the
   centred name is the axis, so that room is the same on both sides. */
#viewerIdentity > .pt-say { bottom: calc(100% + 12px + var(--viewer-name-rise, 0px)); top: auto; }
.gm-mine:has(#projectLabels:not([hidden])) #viewerIdentity > .pt-say {
  max-inline-size: calc(var(--table-width) - 2 * (var(--ctl-right) + 2 * var(--pill-w) + var(--ctl-gap-x) + 8px));
}

/* ============================================================================
   51 · Independent-QA corrections to the viewer's band
   ============================================================================
   Four collisions, one rule: every element in the viewer's band is anchored
   where nothing else can move into it.

   1. The name tag takes only the room the controls leave it. It stays centred,
      so its widest is twice the distance from the centre to the controls, less
      an 8px gap; a long name (32 characters is the lobby's limit) ends in an
      ellipsis rather than running under a button. The same width holds with or
      without the controls, so nothing moves when they appear. */
#viewerIdentity .pt-viewer-who {
  max-inline-size: min(148px, calc(var(--table-width) - 2 * (var(--ctl-right) + 2 * var(--pill-w) + var(--ctl-gap-x) + 8px)));
}
#viewerIdentity .pt-viewer-who .gm-name { white-space: nowrap; }
/* 2. The chosen-project tags sit centred above the name tag - the one band no
      prepared card reaches (the half-height rise stops below the name's top) -
      so a raised card is never under a tag and a tag's ✕ never catches a tap
      meant for a card. Within the controls' room, like the name. A call said
      while the tags are up rises above them. */
body[data-project-picker="labels"] .pt-project-tags {
  left: 50%;
  right: auto;
  inset-block-start: auto;
  bottom: calc(100% + 13px + var(--viewer-name-rise, 27px));
  transform: translateX(-50%);
  max-inline-size: calc(var(--table-width) - 2 * (var(--ctl-right) + 2 * var(--pill-w) + var(--ctl-gap-x) + 8px));
  justify-content: center;
  flex-wrap: nowrap;
}
body[data-project-picker="labels"] .pt-project-tag-remove::before { inset: -11px -8px; }
.gm-mine:has(#projectTags:not([hidden])) #viewerIdentity > .pt-say {
  bottom: calc(100% + 12px + var(--viewer-name-rise, 0px) + 32px);
}
/* 3. A remote top seat's calls (auction and project alike) hang below its role
      labels, which stack under its name: one label ends 21px below the seat,
      two end 41px below, and the call starts 7px under the last one. With no
      label the call keeps its own place beside the seat. */
.gm-seat-top:has(.pt-seat-markers:not([hidden]) > :first-child) .pt-say { top: calc(100% + 28px); bottom: auto; }
.gm-seat-top:has(.pt-seat-markers:not([hidden]) > :nth-child(2)) .pt-say { top: calc(100% + 48px); }
/* 4. On the narrowest phones, while the viewer's own card is on the mat, a
      call the viewer makes sits beside the name (to its left, from the name's
      top down) instead of above it, where it would cover that card. */
@media (max-width: 350px) {
  body:has(#slotBottom > .card) #viewerIdentity > .pt-say {
    left: 6px;
    right: auto;
    transform: none;
    top: calc(50% - var(--viewer-name-rise, 0px) - 13.5px);
    bottom: auto;
    max-inline-size: calc(50% - 62px);
    font-size: 14px;
    padding: 5px 8px;
  }
}

/* ============================================================================
   52 · No instruction is written over the cards
   ============================================================================
   The rail sits over the hand, and its guidance lines («اضغط مرة ثانية للعب»,
   «بانتظار الطاولة») were painted across the card faces. With a hand on the
   table they are read to assistive technology only; ranks, suits, the calls
   above the seats and the refusal shake are unchanged. The waiting room has
   no hand, so its line stays visible there. */
body.gm:has(#hand > .card) .gm-act.is-empty .gm-hint {
  position: absolute; inline-size: 1px; block-size: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

/* ============================================================================
   53 · LOBBY TO TABLE · one walk from a press to the first deal
   ============================================================================
   A press on a way in used to grey every button - the chosen one with the
   others, so the choice read as refused - then cut to a dark, empty court, then
   switch the whole table on in one frame. Three beats replace that, and none of
   them can hold anything back: `lobby.js` navigates the moment the server
   answers, whatever is still moving.

   1. Departure (`body[data-entering]`, set by `table_entry.js` for the
      server's wait). The chosen way in keeps its own lit face, sunk as if still
      held, and its subtitle keeps §11's sweep; the stage deck comes up to full
      light and lifts card by card, the table being dealt; the rest of the room
      recedes. `filter: opacity()` rather than `opacity`, because the imported
      `.tkin` rise holds `opacity: 1` as its fill and would win over a plain
      declaration. A refusal removes the attribute in the task that reports it.
   2. Handoff. A same-origin cross-document view transition: the lobby eases
      forward and fades while the court settles in over it. Only the lobby and
      the table opt in (an inline head tag on each, where Chrome reliably reads
      it), so only navigations between them take part; the arriving page's
      styles own the pseudo-elements, so the forward beat is scoped to the
      table and the way back to the lobby keeps the plain crossfade. A browser
      without the feature navigates exactly as before.
   3. Arrival. `game/table_arrival.js` fades the names and score up at the
      first authoritative frame, and the existing deal carries the cards.

   Reduced motion keeps the departure's lit choice and receding room as still
   states, and takes no transition and no arrival. */
body[data-entering] .tkb-1.is-busy:disabled {
  background-image: var(--face-hot), var(--edge-hot);
  background-clip: padding-box, border-box;
  box-shadow: var(--sink-hot), var(--sh1), var(--bloom-sunk);
  color: var(--ink);
  cursor: progress;
}
body[data-entering] .tkb-1.is-busy .tkb-s {
  background-image: linear-gradient(90deg, var(--ink2) 0%, var(--ink) 50%, var(--ink2) 100%);
}
body[data-entering] .tkb-2.is-busy:disabled {
  background-image: var(--lamp-catch), var(--face-rise), var(--edge);
  background-clip: padding-box, padding-box, border-box;
  box-shadow: var(--sink), var(--sh1);
  color: var(--tx);
  cursor: progress;
}
body[data-entering] .tkb.tkb-q.is-busy:disabled {
  box-shadow: var(--sink), var(--sh0);
  color: var(--tx);
  cursor: progress;
}
body[data-entering] .pt-solo-resume.is-busy:disabled { opacity: 1; cursor: progress; }

body[data-entering] #lobbyPanel .panel .pin > :not(.is-busy):not(:has(.is-busy)),
body[data-entering] #lobbyPanel .pt-solo-controls > :not(.is-busy),
body[data-entering] #lobbyPanel > .ledger,
body[data-entering] #lobbyPanel > .pt-foot {
  filter: opacity(.42);
}
body[data-entering] #lobbyPanel .stage .tkfan { filter: brightness(1); }

@media (prefers-reduced-motion: no-preference) {
  #lobbyPanel .panel .pin > *,
  #lobbyPanel .pt-solo-controls > *,
  #lobbyPanel > .ledger,
  #lobbyPanel > .pt-foot,
  #lobbyPanel .stage .tkfan {
    transition: filter 280ms var(--ease-out);
  }
  /* `translate` composes with each card's imported `rotate()` transform, so
     the lift is straight up the screen whatever the card's lean. */
  body[data-entering] #lobbyPanel .stage .tkfan > .tkcardbody {
    animation: pt-deck-lift 1.4s var(--ease-out) infinite;
  }
  body[data-entering] #lobbyPanel .stage .tkfan > .tkcardbody:nth-child(2) { animation-delay: 140ms; }
  body[data-entering] #lobbyPanel .stage .tkfan > .tkcardbody:nth-child(3) { animation-delay: 280ms; }

  /* The opt-in itself is an inline head tag on both pages (see there). */
  :root:has(> body.gm)::view-transition-old(root),
  :root:has(> body.gm)::view-transition-new(root) {
    /* The default crossfade adds the two layers (plus-lighter); these two
       are not complementary, so they stack normally, table on top. */
    mix-blend-mode: normal;
  }
  /* The tokens live on `.tak` (the body) and these pseudo-elements hang off
     the root, so the two easings are tokens.css's --ease-in / --ease-out
     written out. */
  :root:has(> body.gm)::view-transition-old(root) {
    animation: pt-lobby-leave 360ms cubic-bezier(.4, 0, 1, 1) both;
  }
  :root:has(> body.gm)::view-transition-new(root) {
    animation: pt-table-settle 300ms cubic-bezier(.2, .7, .3, 1) both;
  }
}

@keyframes pt-deck-lift {
  0%, 44%, 100% { translate: 0 0; }
  16% { translate: 0 calc(var(--bw, 56px) * -.14); }
}
@keyframes pt-lobby-leave {
  to { opacity: 0; transform: scale(1.04); }
}
@keyframes pt-table-settle {
  from { opacity: 0; transform: scale(.985); }
}

/* ============================================================================
   54 · A suit off the card is the card's own pip
   ============================================================================
   Labels, announcements and the حكم ثاني rail used to draw suits as text or as
   hand-drawn shapes, and the small spade read as an arrow beside the real
   cards. `game/suits/*.svg` are the faces' own centre pips, shape and paint,
   cut out of `assets/cards-vector-v3.2/` by `tools/extract-suit-pips.py`
   (`--check` proves them unchanged); every one sits in the same square the pips
   share on the face, so the four keep the deck's relative size. So a spade is
   the card's black and a heart the card's own red wherever it appears - the
   pip paints itself, and no colour here is chosen for it.

   The حكم ثاني choices are the bidding buttons' own shape, size, type and gold
   law with one change the captain asked for (2026-09-23): a suit is only its
   true black or red on the card's paper, so the suit buttons wear the card
   face's paper (`--p-card` -> `--p-card-lo`, the lamp fall-off the faces use)
   with a dark hairline where the slate buttons have a light one. The whole
   button is the paper - no inner tile. The one gold primary keeps its gold;
   black and card red both hold on it. Pressed, focus and the pending/disabled
   filter are the rail's own rules, unchanged. */
.pt-suit-icon {
  display: inline-block; inline-size: 1em; block-size: 1em;
  flex: none; vertical-align: -.12em;
  background: var(--pt-suit-shape) center / contain no-repeat;
}
/* The dark header is the one place the design recolours a suit
   (game.css), so there the pip stays a mask in that colour. */
.gm-ct-suit[data-suit]:not([data-suit=""]) {
  display: inline-block; inline-size: 1em; block-size: 1em;
  flex: none; vertical-align: -.12em; background-color: currentColor;
  -webkit-mask: var(--pt-suit-shape) center / contain no-repeat;
  mask: var(--pt-suit-shape) center / contain no-repeat;
}
.pt-suit-icon[data-suit="s"], .gm-ct-suit[data-suit="s"] { --pt-suit-shape: url('../game/suits/s.svg'); }
.pt-suit-icon[data-suit="h"], .gm-ct-suit[data-suit="h"] { --pt-suit-shape: url('../game/suits/h.svg'); }
.pt-suit-icon[data-suit="d"], .gm-ct-suit[data-suit="d"] { --pt-suit-shape: url('../game/suits/d.svg'); }
.pt-suit-icon[data-suit="c"], .gm-ct-suit[data-suit="c"] { --pt-suit-shape: url('../game/suits/c.svg'); }

/* Sized to the Arabic letters beside them; the badge keeps its 12px line. */
.pt-say .pt-suit-icon { inline-size: 1em; block-size: 1em; margin-inline: .04em; vertical-align: -.2em; }
.pt-contract-suit.pt-suit-icon { inline-size: 1.2em; block-size: 1.2em; vertical-align: -.32em; }

.pt-suit-icon.pt-trump-icon {
  display: block; inline-size: 26px; block-size: 26px; margin-inline: auto;
  pointer-events: none;
}
.gm-act > .btn[data-trump]:not(.btn-1) {
  color: var(--ink);
  border-color: rgba(var(--sh-rgb), .28);
  background: linear-gradient(196deg, var(--p-card) 0%, var(--p-card-mid) 44%, var(--p-card-lo) 100%);
  box-shadow: var(--rim-paper), 0 5px 12px rgba(0, 0, 0, .3);
}
/* The one worded choice beside three icon-only suits (يقلب صن) keeps its words
   on one line; the suits share what is left, never under the 44px target. */
.gm-act[data-count="4"]:has(> .btn[data-trump]) > .btn:not([data-trump]) {
  flex: 0 0 auto; padding-inline: 14px; white-space: nowrap;
}

/* ============================================================================
   55 · The table's card stand: a lip IN FRONT of the fan, on the fan's curve
   ============================================================================
   It holds the cards by their bottom edge and covers no printed mark. The
   captain rejected a 92px stand and a 46px lip that covered the faces, a
   ledge behind the fan, a lip fitted to the fan's droop as "too curved", and
   a shallow arc that covered writing at 7% and still reached the rightmost
   card's upside-down rank at 4%. The captain's last word (2026-09-24): match
   the bottom fan curve and cover none of the writing.
   The frozen faces print nothing in their bottom 4.4% under the upside-down
   corner index and 7.7% under the court frames and pips, and nothing at all
   near a card's left edge. So the lip's edge is one circle parallel to the
   fan's own bottom, 1-5% up every card: it falls as far as the fan does, and
   clears every printed mark, the rightmost card's upside-down index closest,
   by 1.5px or more. The fan's
   outer cards always turn 6 degrees and drop 6px however many there are, so
   fewer cards bend the same drop over a shorter span: each count has its own
   radius, in card widths. The 8-card column below 375px lays its cards flat
   and steps them, so it takes a flatter circle.
   The lip is the hand's own pseudo-element on the fan's grid lines, centred
   on the fan. The body clips it at the table's edges and at the glass. The
   rail and every overlay stay above it; it takes no pointer. It wears the
   lobby's surface ladder and no brass, because brass is what a player presses. */
body.gm.pt-live .gm-hand:has(> .card)::after {
  --stand-r: calc(var(--card-w) * var(--stand-k) * var(--stand-raised, 1));
  --stand-shape: circle var(--stand-r) at 50% var(--stand-r);
  --stand-edge: radial-gradient(var(--stand-shape), black calc(var(--stand-r) - .5px), transparent var(--stand-r));
  /* Past the fan's outer cards the rim's shoulders fall into shadow, so the
     lit part of the stand is as wide as the hand it holds. */
  --stand-lit: calc(var(--card-w) * 1.85);
  --stand-dark: color-mix(in srgb, var(--s-void) 90%, transparent);
  content: "";
  position: absolute;
  z-index: 30; /* over the resting fan, under a raised card's own rise */
  pointer-events: none;
  grid-column: 1 / -1;
  grid-row: 1;
  inset-inline: calc(50% - var(--stand-r));
  top: calc(100% + var(--fan-rail-lift) - var(--hand-stand-seat));
  block-size: var(--table-height);
  -webkit-mask: var(--stand-edge);
  mask: var(--stand-edge);
  /* The lamp above the table on the rim's crown, the shoulders' shadow, and
     rings measured in px down from the edge, so they follow the curve at any
     radius: a warm catchlight, the lit rounded top, one crease, then a front
     face that falls into shadow. */
  background:
    radial-gradient(calc(var(--card-w) * 2.6) 14px at 50% 0, rgba(var(--tx-rgb), .1), transparent),
    linear-gradient(90deg, var(--stand-dark) calc(var(--stand-r) - var(--stand-lit) - var(--card-w) * .45), transparent calc(var(--stand-r) - var(--stand-lit)),
      transparent calc(var(--stand-r) + var(--stand-lit)), var(--stand-dark) calc(var(--stand-r) + var(--stand-lit) + var(--card-w) * .45)),
    radial-gradient(var(--stand-shape),
      var(--s-void) calc(var(--stand-r) - 70px),
      var(--s-well-lo) calc(var(--stand-r) - 46px),
      var(--s-well) calc(var(--stand-r) - 26px),
      var(--s-table) calc(var(--stand-r) - 12.5px),
      color-mix(in srgb, var(--s-void) 75%, black) calc(var(--stand-r) - 10.5px),
      var(--s-table) calc(var(--stand-r) - 9.5px),
      var(--s-table-lit) calc(var(--stand-r) - 7px),
      var(--s-panel) calc(var(--stand-r) - 3.5px),
      var(--s-lift) calc(var(--stand-r) - 1.4px),
      color-mix(in srgb, rgb(var(--tx-rgb)) 42%, var(--s-lift)) calc(var(--stand-r) - .6px));
}
/* Each count's radius and seat, fitted to the real fan at 360-1280px wide:
   the edge falls as far as the fan does to its outer corners (12-13px for 3-8
   cards, 4-5px for a pair), every visible card bottom sits at least 1.25px
   under it and every printed mark at least 1.5px above it
   (tests/hand_inset.mjs). The seat grows with the card, and a little faster,
   because a wider column spaces its tracks wider too. */
body.gm.pt-live .gm-hand:has(> .card)::after {
  --stand-k: 10.65;
  --hand-stand-seat: calc(var(--card-h) * .072 - 6.4px);
}
body.gm.pt-live .gm-hand[data-count="1"]:has(> .card)::after {
  --stand-k: 20; --stand-lit: calc(var(--card-w) * .65);
  --hand-stand-seat: calc(var(--card-h) * .03 - .5px);
}
body.gm.pt-live .gm-hand[data-count="2"]:has(> .card)::after {
  --stand-k: 7; --stand-lit: calc(var(--card-w) * 1.02);
  --hand-stand-seat: calc(var(--card-h) * .05 - 3.75px);
}
body.gm.pt-live .gm-hand[data-count="3"]:has(> .card)::after {
  --stand-k: 6.5; --stand-lit: calc(var(--card-w) * 1.4);
  --hand-stand-seat: calc(var(--card-h) * .06 - 4.65px);
}
body.gm.pt-live .gm-hand[data-count="4"]:has(> .card)::after {
  --stand-k: 8.6; --stand-lit: calc(var(--card-w) * 1.65);
  --hand-stand-seat: calc(var(--card-h) * .07 - 6.75px);
}
@media (max-width: 374px),
       (orientation: landscape) and (pointer: coarse) and (max-height: 374px) {
  body.gm.pt-live .gm-hand[data-count="8"]:has(> .card)::after {
    --stand-k: 20;
    --hand-stand-seat: calc(var(--card-h) * .04 - 2.6px);
  }
}
/* A raised card parts its neighbours 10px, so the end cards stand where the
   edge has fallen further: for 2 and 4-8 cards the circle flattens by 7%
   while a card is up. Three cards and the flat 8-card row keep their resting
   edge, which already holds both margins with any card up - except that
   raising the flat row's second card bares the first one's upside-down
   index, one 3px step lower than the edge was fitted to, while the edge still
   climbs across that card. No circle keeps 0.8px on both there; a flatter,
   lower one keeps about 0.4px on each, so neither a bottom nor a mark shows. */
body.gm.pt-live .gm-hand:has(> .card[data-sel="1"])::after { --stand-raised: 1.07; }
body.gm.pt-live .gm-hand[data-count="3"]:has(> .card[data-sel="1"])::after { --stand-raised: 1; }
@media (max-width: 374px),
       (orientation: landscape) and (pointer: coarse) and (max-height: 374px) {
  body.gm.pt-live .gm-hand[data-count="8"]:has(> .card[data-sel="1"])::after { --stand-raised: 1; }
  body.gm.pt-live .gm-hand[data-count="8"]:has(> .card ~ .card[data-sel="1"]):not(:has(> .card ~ .card ~ .card[data-sel="1"]))::after {
    --stand-k: 30;
    --hand-stand-seat: calc(var(--card-h) * .04 - 4.55px);
  }
}
/* On a wide screen the table is a phone-width column in open room: the stand
   fades out over its last 24px rather than ending in a square cut. */
@media (min-width: 520px) {
  body.gm.pt-live .gm-hand:has(> .card)::after {
    --stand-ends: linear-gradient(90deg, transparent calc(var(--stand-r) - var(--table-width) / 2),
      black calc(var(--stand-r) - var(--table-width) / 2 + 24px), black calc(var(--stand-r) + var(--table-width) / 2 - 24px),
      transparent calc(var(--stand-r) + var(--table-width) / 2));
    -webkit-mask: var(--stand-edge), var(--stand-ends);
    -webkit-mask-composite: source-in;
    mask: var(--stand-edge), var(--stand-ends);
    mask-composite: intersect;
  }
}
/* Below 360px the project controls span most of the column and the court is
   height-bound, so a side seat rises only half as far as a lifted hand. Main's
   own line already lets a control reach a two-label seat there, and every pixel
   of lift adds to it. The narrowest column keeps main's line, where the glass
   crops the far bottom edge and a lip would have no room. */
@media (max-width: 359px),
       (orientation: landscape) and (pointer: coarse) and (max-height: 359px) {
  body.gm { --hand-stand-lift: 0px; }
  body.gm.pt-live .gm-hand:has(> .card)::after { content: none; }
}

/* Account screens: sign-in, the players' board, the account panel and the
   name options share one sheet, one header row, one type scale and one button
   ladder - brass for the one main action, raised slate for the rest, a quiet
   text link for the alternative, and red text for the destructive one. Sign-in
   borrows the lobby ground and stage; the three sheets sit on a dim backdrop.
   Nothing here may scroll sideways: long or Latin names end in an ellipsis on
   their own side (`dir=auto`), records never wrap mid-figure. */
body:has(#labLogin[open]) #lobbyPanel > :not(.stage) {visibility:hidden}
#labLogin,#labBoard,#labAccount,#labModeration {--lab-line:rgba(242,236,221,.22);
  --lab-edge:rgba(242,236,221,.36);--lab-alert:color-mix(in srgb,var(--aqeeq-sun) 58%,var(--tx));color:var(--tx);font:400 1rem/1.5 var(--fu);text-align:start}
#labLogin *,#labBoard *,#labAccount *,#labModeration * {box-sizing:border-box}
#labLogin [hidden],#labBoard [hidden],#labAccount [hidden],#labModeration [hidden] {display:none!important}
#labLogin h2,#labBoard h2,#labAccount h2,#labModeration h2 {margin:0;min-width:0;color:var(--tx);
  font:850 1.4rem/1.25 var(--fu)}

/* The shared button ladder. */
#labLogin button,#labAccount .lab-scroll > button,#labModeration .lab-scroll > button,#labBoard .lab-wide,
#labAccountMethods > button,#labAccountTrial > button,#labLinkEmail button {
  display:flex;align-items:center;justify-content:center;gap:10px;width:100%;min-height:48px;
  margin:8px 0 0;padding:6px 14px;border:1px solid var(--lab-edge);border-radius:var(--r-btn-sm);
  background:linear-gradient(var(--s-rise-hi),var(--s-rise-lo));color:var(--tx);
  font:700 1rem/1.3 var(--fu);text-align:center;cursor:pointer}
#labLogin #labLoginProfile,#labLoginEmail button[type=submit] {border-color:var(--a);
  background:linear-gradient(var(--a-hi),var(--a-lo));color:var(--ink)}
#labLogin .lab-links button,#labAccount .lab-scroll > .lab-link,#labAccountMethods .lab-own-list button {
  width:auto;min-height:44px;margin:2px auto 0;
  padding:0 12px;border:0;background:transparent;color:var(--tx);opacity:.86;
  font:650 .92rem var(--fu);text-decoration:underline;text-underline-offset:4px;cursor:pointer}
#labLogin .lab-links {display:flex;justify-content:space-between;gap:8px;margin-top:2px}
#labLogin .lab-links button {margin:0;padding:0 4px}
#labLogin .is-email-flow .lab-links {justify-content:center}
#labLogin .lab-login-card.is-email-flow :is(#labLoginApple,#labLoginGoogle,.lab-or,#labLoginGuest) {display:none}
#labAccount .lab-scroll > .lab-link {display:block;margin-top:8px;font-size:.88rem;opacity:.75}
#labAccount .lab-scroll > .lab-danger {border-color:var(--lab-alert);background:transparent;color:var(--lab-alert)}
#labLogin button:focus-visible,#labLogin input:focus-visible,#labLogin a:focus-visible,
#labBoard button:focus-visible,#labBoard .lab-row:focus-visible,
#labAccount button:focus-visible,#labModeration button:focus-visible {outline:3px solid var(--a-hi);outline-offset:2px}

/* Sign-in. */
#labLogin {position:fixed;inset:0;width:100%;height:100dvh;max-width:none;max-height:none;
  margin:0;padding:0;border:0;background:transparent;overflow:auto}
#labLogin::backdrop {background:transparent}
#labLogin .lab-login-layout {width:min(100%,422px);min-height:100%;margin:auto;
  padding:clamp(132px,24vh,204px) 16px max(16px,env(safe-area-inset-bottom));
  display:flex;flex-direction:column;align-items:stretch;justify-content:flex-start}
#labLogin .lab-login-card {padding:20px 18px 12px;border:1px solid var(--lab-edge);
  border-radius:var(--r-sheet);background:linear-gradient(155deg,var(--s-panel-hi),var(--s-panel-lo));
  box-shadow:0 20px 60px rgba(4,7,16,.52),inset 0 1px rgba(255,250,236,.12)}
#labLogin h2 {margin-bottom:10px}
/* Provider marks are their own sign-in controls, not recoloured lobby buttons:
   the mark leads the label on the reading side. */
#labLogin #labLoginApple,#labLogin #labLoginGoogle,#labAccount #labLoginApple,#labAccount #labLoginGoogle {
  display:flex;align-items:center;justify-content:center;gap:10px;width:100%;min-height:48px;
  margin:8px 0 0;padding:6px 14px;border-radius:var(--r-btn-sm);font-size:1rem;line-height:1.3;cursor:pointer}
#labLoginApple::before,#labLoginGoogle::before {content:"";display:block;flex:none}
#labLoginApple,#labLoginGoogle {white-space:nowrap}
#labLogin #labLoginApple,#labAccount #labLoginApple {border:1px solid #fff;background:#000;color:#fff;
  font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text",var(--fu),sans-serif;font-weight:600}
#labLoginApple::before {width:17px;height:21px;margin-top:-3px;background:#fff;
  -webkit-mask:url('../assets/apple-mark-v1.svg') center/contain no-repeat;
  mask:url('../assets/apple-mark-v1.svg') center/contain no-repeat}
#labLogin #labLoginGoogle,#labAccount #labLoginGoogle {border:1px solid #dadce0;background:#fff;color:#1f1f1f;
  font-family:"Google Sans",Roboto,var(--fu),sans-serif;font-weight:500}
#labLoginGoogle::before {width:20px;height:20px;
  background:url('../assets/google-g-v1.svg') center/contain no-repeat}
#labLogin .lab-or {display:flex;align-items:center;gap:12px;margin:12px 0 2px;
  font-size:.88rem;opacity:.8}
#labLogin .lab-or::before,#labLogin .lab-or::after {content:"";flex:1;height:1px;background:var(--lab-line)}
#labLoginEmail {display:flex;flex-direction:column;gap:8px;margin-top:8px}
#labLoginEmail input {width:100%;min-height:48px;padding:8px 14px;
  border:1px solid var(--lab-edge);border-radius:var(--r-btn-sm);
  background:rgba(8,18,31,.35);color:var(--tx);font:500 1rem var(--fu)}
#labLoginEmail input::placeholder {color:var(--tx);opacity:.62}
#labLoginEmail input[type=email] {direction:ltr;text-align:right}
#labLoginEmail input[type=email]:placeholder-shown {direction:rtl}
#labLoginEmail button {margin:0}
#labLogin .lab-login-status {margin:2px 0 0;font-size:.92rem;text-align:center;color:var(--a-hi)}
#labLogin .lab-login-status.is-error {color:var(--lab-alert)}
#labLogin .lab-login-status:empty {display:none}
#labLogin .lab-legal {display:flex;justify-content:center;gap:20px;margin:12px 0 0}
#labLogin .lab-legal a {display:inline-flex;align-items:center;min-height:44px;color:var(--tx);
  opacity:.72;font-size:.86rem;text-underline-offset:4px}
#lobbyPanel #soloButton[hidden],#lobbyPanel #duoButton[hidden] {display:none}
/* accounts.js starts the sheet below the lobby's mark and deck; where a screen
   is too short for both it sets `lab-login-compact`, and the sheet takes the
   screen while the deck steps aside rather than showing through behind it. */
body.lab-login-compact:has(#labLogin[open]) #lobbyPanel .stage {visibility:hidden}
body.lab-login-compact #labLogin .lab-login-layout {justify-content:center;
  padding-block:max(12px,env(safe-area-inset-top)) max(12px,env(safe-area-inset-bottom))}
@media (max-height:700px) {
  #labLogin .lab-login-card {padding:14px 14px 6px}
  #labLogin h2 {margin-bottom:4px;font-size:1.28rem}
  #labLogin button,#labLogin #labLoginApple,#labLogin #labLoginGoogle,#labLoginEmail input {min-height:44px}
  #labLogin button,#labLogin #labLoginApple,#labLogin #labLoginGoogle {margin-top:6px}
  #labLoginEmail {gap:6px;margin-top:4px}
  #labLoginEmail button {margin:0}
  #labLogin .lab-links button {min-height:40px}
  #labLogin .lab-or {margin:4px 0 0}
  #labLogin .lab-legal {margin-top:0}
}
@media (max-width:359px) {#labLogin .lab-login-layout {padding-inline:10px}}

/* The lobby's own profile control and the notebook's tap target. */
#lobbyPanel .panel .tkfield:has(#labProfile) {gap:6px}
#lobbyPanel .panel .tkfield #labProfile {flex:0 0 44px;width:44px;min-height:max(44px,var(--lobby-field));
  padding:0 3px;border:1px solid rgba(242,236,221,.45);border-radius:var(--r-btn-sm);
  background:linear-gradient(var(--s-rise-hi),var(--s-rise-lo));color:var(--tx);
  font:700 .8rem var(--fu);white-space:nowrap;cursor:pointer}
#lobbyPanel .panel .tkfield #labProfile[aria-pressed="true"] {border-color:var(--a-hi)}
#labProfile:focus-visible,.tkledger #labBoardOpen:focus-visible {outline:3px solid var(--a-hi);outline-offset:3px}
.tkledger:has(#labBoardOpen) {position:relative}
.tkledger #labBoardOpen {position:absolute;z-index:2;inset:0;width:100%;height:100%;
  border:0;border-radius:var(--r-sheet);background:transparent;color:transparent;
  font-size:0;cursor:pointer}

/* The three sheets: a header row (title, then رجوع at the far end) above a
   body that alone scrolls. */
#labBoard,#labAccount,#labModeration {width:min(calc(100vw - 32px),390px);
  max-width:none;max-height:calc(100dvh - 32px);margin:auto;padding:0;overflow:hidden;
  border:1px solid var(--lab-edge);border-radius:var(--r-sheet);
  background:linear-gradient(155deg,var(--s-panel-hi),var(--s-panel-lo));
  box-shadow:0 20px 60px rgba(4,7,16,.52),inset 0 1px rgba(255,250,236,.12)}
#labBoard[open],#labAccount[open],#labModeration[open] {display:flex;flex-direction:column}
#labBoard::backdrop,#labAccount::backdrop,#labModeration::backdrop {background:rgba(7,9,17,.9)}
#labBoard .lab-top,#labAccount .lab-top,#labModeration .lab-top {display:flex;flex:none;align-items:center;
  justify-content:space-between;gap:12px;padding:16px 18px 12px;border-bottom:1px solid var(--lab-line)}
#labLogin h2:focus,#labBoard .lab-top h2:focus,#labAccount .lab-top h2:focus,#labModeration .lab-top h2:focus {outline:none}
#labBoard .lab-back,#labAccount .lab-back,#labModeration .lab-back {flex:none;min-width:72px;min-height:44px;
  margin:0;padding:0 14px;border:1px solid var(--lab-edge);border-radius:var(--r-btn-sm);
  background:linear-gradient(var(--s-rise-hi),var(--s-rise-lo));color:var(--tx);
  font:700 .95rem var(--fu);cursor:pointer}
#labBoard .lab-scroll,#labAccount .lab-scroll,#labModeration .lab-scroll {flex:1 1 auto;min-height:0;
  padding:6px 18px 22px;overflow:auto;overscroll-behavior:contain}
#labAccount .lab-scroll,#labModeration .lab-scroll {padding-top:14px}
#labBoard .lab-status,#labAccount .lab-status,#labModeration .lab-status {margin:8px 0 0;font-size:.92rem;text-align:center}
#labBoard .lab-status:empty,#labAccount .lab-status:empty,#labModeration .lab-status:empty {display:none}

/* The board. */
#labBoard .lab-note {margin:8px 0 0;color:var(--a-hi);font-size:.9rem;font-weight:700}
#labBoard .lab-rows {list-style:none;margin:0;padding:0}
#labBoard .lab-empty {padding:18px 6px;text-align:center;opacity:.75}
#labBoard .lab-row {display:flex;align-items:center;gap:10px;min-height:44px;padding:4px 6px;
  border-bottom:1px solid var(--lab-line)}
#labBoard .lab-row:last-child {border-bottom:0}
#labBoard .lab-row[tabindex] {cursor:pointer}
#labBoard .lab-name {flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap;text-align:start;font-size:.95rem;font-weight:700}
#labBoard .lab-rate {flex:none;min-width:3ch;color:var(--a-hi);font-size:1.05rem;font-weight:800;
  text-align:end;direction:ltr;unicode-bidi:isolate}
#labBoard .lab-group {padding:16px 6px 6px;border-bottom:1px solid var(--lab-line)}
#labBoard .lab-group h3 {margin:0;color:var(--a-hi);font:800 .92rem/1.3 var(--fu)}
#labBoard .lab-rank {flex:0 0 1.8em;opacity:.7;font-size:.86rem;font-weight:700;text-align:center}
#labBoard .lab-row.is-you {border-radius:10px;background:rgba(242,236,221,.09)}
#labBoard .lab-you {flex:none;padding:1px 8px;border:1px solid var(--a-hi);border-radius:999px;
  color:var(--a-hi);font-size:.74rem;font-weight:800}
#labBoard .lab-wide {margin-top:14px}

/* Agent versions stay separate from player ranks and name moderation. Only
   the active executor earns the tag; these are the server's player-scale numbers. */
#labBoard .lab-agents {margin:10px 0 14px;padding:10px 12px;border:1px solid var(--lab-line);
  border-radius:var(--r-btn-sm);background:rgba(242,236,221,.035)}
#labBoard .lab-agents h3 {margin:0 0 4px;color:var(--tx);font:800 .88rem/1.5 var(--fu)}
#labBoard .lab-agent {display:flex;align-items:center;gap:8px;min-height:44px}
#labBoard .lab-agent + .lab-agent {border-top:1px solid var(--lab-line)}
#labBoard .lab-agent-version {font:750 1rem/1.4 var(--fu);unicode-bidi:isolate}
#labBoard .lab-agent .lab-rate {margin-right:auto}
#labBoard .lab-current {flex:none;padding:1px 8px;border:1px solid var(--a-hi);border-radius:999px;
  color:var(--a-hi);font:800 .74rem/1.6 var(--fu)}
/* A non-modal one-line release note, above the lobby furniture, not over the
   notebook button. No focus capture, new local key or game-retirement action. */
.pt-agent-release {position:fixed;z-index:110;top:max(12px,env(safe-area-inset-top));left:50%;
  transform:translateX(-50%);display:flex;align-items:center;justify-content:center;gap:.3em;
  width:min(calc(100% - 32px),358px);min-height:48px;padding:10px 16px;
  border:1px solid var(--a-hi);border-radius:var(--r-btn-sm);
  background:linear-gradient(var(--s-panel-hi),var(--s-panel-lo));color:var(--tx);
  box-shadow:0 8px 30px rgba(4,7,16,.4);font:750 1rem/1.5 var(--fu);cursor:pointer}
.pt-agent-release bdi {unicode-bidi:isolate;direction:ltr}
.pt-agent-release[hidden] {display:none}
.pt-agent-release:focus-visible {outline:3px solid var(--a-hi);outline-offset:3px}

/* The account panel and its rating card. */
#labModeration .lab-subject {margin:0 0 4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  text-align:start;font-size:1.06rem;font-weight:700}
#labAccountRating {margin:0 0 6px;padding:14px 16px;border:1px solid var(--lab-line);
  border-radius:var(--r-btn-sm);background:rgba(242,236,221,.05)}
#labAccountRating p {margin:0}
#labAccountRating .lab-own-head {display:flex;flex-wrap:wrap;align-items:center;gap:6px 8px}
#labAccountRating .lab-own-title {opacity:.86;font-size:.92rem;font-weight:800}
#labAccountRating > .lab-own-title {margin-top:14px}
#labAccountRating .lab-tag {padding:1px 8px;border:1px solid var(--a-hi);border-radius:999px;
  color:var(--a-hi);font-size:.74rem;font-weight:800}
#labAccountRating .lab-own-rate {color:var(--a-hi);font:850 2.2rem/1.15 var(--fu)}
#labAccountRating .lab-own-rate.is-empty {font:700 1.6rem/1.3 var(--fu);opacity:.7}
#labAccountRating .lab-own-hint {font-size:.92rem;opacity:.86;text-wrap:balance}
#labAccountRating .lab-own-list {list-style:none;margin:10px 0 0;padding:0}
#labAccountRating .lab-own-pairs {margin-top:4px}
#labAccountRating .lab-own-list li {display:flex;align-items:center;justify-content:space-between;gap:12px;
  min-height:40px;border-top:1px solid var(--lab-line)}
#labAccountRating .lab-own-list span {min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  text-align:start;font-size:1rem;font-weight:700}
#labAccountRating .lab-own-list strong {flex:none;font-size:.92rem;font-weight:700;white-space:nowrap}
/* The account's sign-in methods (app/server/ACCOUNT-LINKING.md) in the same
   card language: a caption, one row per method with a quiet إزالة, then a
   button per method it can add; the add-email form uses the sign-in inputs. */
#labAccountMethods {margin:0 0 6px;padding:14px 16px;border:1px solid var(--lab-line);
  border-radius:var(--r-btn-sm);background:rgba(242,236,221,.05)}
#labAccountMethods h3 {margin:0;opacity:.86;font:800 .92rem var(--fu)}
#labAccountMethods .lab-own-list {list-style:none;margin:10px 0 0;padding:0}
#labAccountMethods .lab-own-list li {display:flex;align-items:center;justify-content:space-between;gap:12px;
  min-height:44px;border-top:1px solid var(--lab-line)}
#labAccountMethods .lab-own-list span {font-size:1rem;font-weight:700;unicode-bidi:isolate}
#labAccountMethods .lab-own-list button {min-width:44px;margin:0;padding:0 8px;font-size:.9rem}
#labAccountMethods #labLinkEmail button[type=submit] {border-color:var(--a);
  background:linear-gradient(var(--a-hi),var(--a-lo));color:var(--ink)}
#labAccountTrial:empty {display:none}
@media (max-width:359px) {
  #labAccountMethods,#labAccountRating {padding-inline:10px}
  #labAccountMethods #labLoginApple,#labAccountMethods #labLoginGoogle {padding-inline:8px;font-size:.94rem}
}
#labLinkEmail {display:flex;flex-direction:column;gap:8px;margin:8px 0 0}
#labLinkEmail input {width:100%;min-height:48px;padding:8px 14px;border:1px solid var(--lab-edge);
  border-radius:var(--r-btn-sm);background:rgba(8,18,31,.35);color:var(--tx);font:500 1rem var(--fu)}
#labLinkEmail input::placeholder {color:var(--tx);opacity:.62}
#labLinkEmail input:focus-visible {outline:3px solid var(--a-hi);outline-offset:2px}
#labLinkEmail button {margin:0}
/* Until the server answers, the sign-in sheet offers nothing to press. */
#labLogin .lab-login-card.is-pending > button {visibility:hidden}
/* The own card: one block per mode (SOLO, then DUO): its rating once placed,
   otherwise the placement count with a thin bar and one line, then wins and
   losses; DUO adds the three partners with the most games together. */
#labAccountRating .lab-own-mode + .lab-own-mode {margin-top:14px;padding-top:12px;border-top:1px solid var(--lab-line)}
#labAccountRating .lab-own-rate.is-placing {font-size:1.8rem}
#labAccountRating .lab-own-bar {display:block;height:6px;margin:6px 0 2px;border-radius:3px;
  background:linear-gradient(to left,var(--a-hi) var(--done),rgba(242,236,221,.14) var(--done))}
#labAccountRating .lab-own-hint {margin-top:4px;font-size:.9rem;font-weight:700;opacity:.9;text-wrap:balance}
#labAccountRating .lab-own-record {margin-top:6px;font-size:.95rem;font-weight:700}
#labAccountRating .lab-own-sub {margin-top:12px}
/* The lobby's notebook card for a signed-in player: their SOLO standing. */
.tkledger[data-account] > .lg-cells:not(.lab-book) {display:none}
.tkledger .lg-cells.lab-book {grid-template-columns:1fr}
.tkledger .lab-book .lg-cell .k {white-space:normal;text-align:center;text-wrap:balance}
.tkledger .lg-cells.lab-book[hidden] {display:none}

/* ============================================================================
   56 · Picks ahead of the turn (premoves, owner 2026-10-16)
   ============================================================================
   Before the viewer's own bidding turn the rail holds that turn's own bid
   buttons in their own slots, so nothing moves when the turn comes. They stay
   quiet until one is picked: a dashed outline and no gold - the gold is the
   turn's primary action alone - but opaque, so no label is ever drawn over a
   card face. The picked one wears a gold ring of the same border width (the
   rest drawn inside), so no neighbour moves. Like every rail line over the
   hand (§52), the line naming the pick is read to assistive technology only;
   on screen the button and the chip beside the viewer's name carry it. At the
   turn the picked button waits the reading pause with a draining underline
   (a still ring with Reduce Motion). */
.gm-act[data-premove="1"] > .btn.pt-premove {
  border: 1.5px dashed rgba(var(--tx-rgb), .42);
  box-shadow: none;
  color: var(--tx);
}
.gm-act[data-premove="1"] > .btn.pt-premove[aria-pressed="true"] {
  border: 1.5px solid var(--a-hi);
  background: rgba(var(--tx-rgb), .14);
  box-shadow: inset 0 0 0 1px var(--a-hi), 0 0 0 1px rgba(0, 0, 0, .35);
}
.gm-act[data-premove="1"] > .gm-hint.pt-premove-hint {
  position: absolute; inline-size: 1px; block-size: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.gm-act > .btn.is-premove-firing {
  position: relative;
  outline: 2px solid var(--a-hi);
  outline-offset: 2px;
}
@media (prefers-reduced-motion: no-preference) {
  .gm-act > .btn.is-premove-firing::after {
    content: "";
    position: absolute;
    inset-inline: 14%;
    inset-block-end: 5px;
    block-size: 3px;
    border-radius: 2px;
    background: currentColor;
    opacity: .7;
    transform-origin: center;
    animation: pt-premove-drain var(--premove-pause, 1200ms) linear forwards;
  }
}
@keyframes pt-premove-drain { from { transform: scaleX(1); } to { transform: scaleX(0); } }
/* The pick's chip beside the viewer's own name: a private note, so it is
   dashed like the pick-ahead buttons, never the solid chips of public facts. */
#viewerIdentity .pt-seat-markers [data-kind="premove"] {
  padding: 2px 7px;
  border: 1px dashed rgba(var(--tx-rgb), .55);
  border-radius: 6px;
  font-weight: 800;
  white-space: nowrap;
}
/* With the majlis layer the rail's buttons are slate glass (majlis-table);
   the pick-ahead ones keep the same lanes, quiet until picked. */
html[data-majlis~="system"] body.gm #act[data-premove="1"] > .btn.pt-premove {
  background: var(--mj-night-600);
  border: 1.5px dashed rgba(var(--mj-lit-rgb), .46);
  box-shadow: none;
  color: var(--mj-ivory);
}
html[data-majlis~="system"] body.gm #act[data-premove="1"] > .btn.pt-premove[aria-pressed="true"] {
  background: var(--mj-slate-glass);
  border: 1.5px solid var(--mj-gold-300);
  box-shadow: inset 0 0 0 1px var(--mj-gold-300), var(--mj-e2);
  color: var(--mj-gold-200);
}
html[data-majlis~="system"] body.gm #act > .btn.is-premove-firing {
  outline: 2px solid var(--mj-gold-300);
  outline-offset: 3px;
}
html[data-majlis~="system"] body.gm #viewerIdentity .pt-seat-markers [data-kind="premove"] {
  border-color: rgba(var(--mj-lit-rgb), .6);
  color: var(--mj-gold-200);
  background: rgba(8, 14, 26, .55);
}
/* While the last three cards fly (majlis-table's deal scene) a tap on the
   table skips the deal, so the labels before the turn wait for it to settle. */
html[data-mj-dealing] #projectLabels[data-ahead="1"],
html[data-mj-dealing] #projectTags[data-ahead="1"] { visibility: hidden; }
