* { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html, body { height: 100%; }
body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  background: linear-gradient(180deg, #052e16 0%, #166534 55%, #052e16 100%);
  color: #f0fdf4; user-select: none; -webkit-user-select: none;
  touch-action: none; overflow: hidden;
}
#app { height: 100%; display: flex; flex-direction: column; max-width: min(100%, 860px); margin: 0 auto; }

#hdr { display: flex; align-items: center; gap: 8px; padding: 8px 12px; padding-top: max(8px, env(safe-area-inset-top)); }
#hdr .pill { background: rgba(0,0,0,0.22); border: 2px solid rgba(255,255,255,0.16); border-radius: 999px; padding: 6px 11px; font-weight: 800; font-size: 15px; min-width: 44px; min-height: 44px; display: flex; align-items: center; cursor: pointer; justify-content: center; color: inherit; font-family: inherit; }
#hdrHome { border: none; background: #4ade80; color: #052e16; font-size: 18px; }
#hdrStatus { flex: 1; min-width: 0; text-align: center; font-weight: 800; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #bbf7d0; }

.scr { flex: 1; display: none; flex-direction: column; overflow-y: auto; padding: 6px 14px 18px; }
/* The table gets the full width. 14px of padding either side costs the card
   size directly, and a fanned hand of thirteen divides whatever is left. */
#scr-play { padding: 6px 4px 6px; }
.scr.on { display: flex; }

#scr-menu { align-items: center; text-align: center; gap: 12px; justify-content: center; }
#scr-menu h1 { font-size: clamp(32px, 9vw, 44px); color: #ffffff; }
#scr-menu .sub { color: #bbf7d0; font-weight: 600; margin-bottom: 4px; }
#tierRow { display: flex; gap: 8px; width: 100%; }
.tierCard { flex: 1; background: rgba(0,0,0,0.2); border: 3px solid rgba(255,255,255,0.14); border-radius: 16px; padding: 10px 4px; font-weight: 800; font-size: 14px; line-height: 1.35; cursor: pointer; color: #f0fdf4; font-family: inherit; }
.tierCard small { display: block; color: #bbf7d0; font-weight: 700; font-size: 11.5px; }

/* Settings, behind a disclosure. Same shape as every other game so a player
   who has met one has met them all. */
.field { display: flex; align-items: center; gap: 10px; margin: 7px 0; }
.field .lbl { flex: 0 0 104px; font-size: 13.5px; font-weight: 800; color: #bbf7d0; }
.seg { flex: 1; display: flex; gap: 6px; }
.seg button { flex: 1; min-height: 44px; border-radius: 11px; border: 2px solid rgba(255,255,255,0.16); background: rgba(0,0,0,0.24); color: #f0fdf4; font-family: inherit; font-weight: 800; font-size: 13.5px; cursor: pointer; padding: 4px 2px; }
.seg button.on { background: #4ade80; border-color: #16a34a; color: #052e16; min-height: 44px;}

button.big { width: 100%; border-radius: 16px; min-height: 56px; font-size: 19px; font-weight: 800; background: rgba(0,0,0,0.2); color: #f0fdf4; border: 3px solid rgba(255,255,255,0.14); cursor: pointer; font-family: inherit; }
button.big:active { transform: translateY(2px); }
button.big.primary { background: #4ade80; border-color: #16a34a; color: #052e16; min-height: 44px;}

/* play */
#playTop { display: flex; align-items: center; justify-content: space-between; font-weight: 800; padding: 2px 4px 6px; font-size: 13px; gap: 8px; min-height: 22px; }
/* flex: none, AND THAT IS THE WHOLE BUG. This rule arrived by copy from a game
   where #msgOut sits INSIDE #playTop — a flex ROW, where `flex: 1` means "fill
   the width". Here it is a direct child of the screen's flex COLUMN, where the
   identical declaration means "take all the remaining height". It took 298px
   of a 298px canvas and the felt was drawn in a letterbox.
   check-stability catches an id that is styled and ABSENT. It cannot catch an
   id that is present under a different parent, because the selector resolves
   and the rule is real — only its MEANING changed. */
#msgOut { flex: none; min-height: 20px; text-align: center; color: #dcfce7;
          font-weight: 800; font-size: 13px; padding: 0 6px;
          white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#scoreOut { color: #4ade80; font-variant-numeric: tabular-nums; white-space: nowrap; }

#stageWrap { flex: 1; position: relative; min-height: 0; }
canvas#stage { display: block; touch-action: none; }

/* THE ROW IS ALWAYS HERE, EVEN WHEN IT IS EMPTY.
   It used to be display:none until there was something to offer, which meant
   showing it shrank #stageWrap, which re-fitted the canvas, which re-laid the
   whole table — cards resized and slid the instant you tapped one, and slid
   back when you chose. Reserving the height permanently removes the cause;
   toggling a class only ever chased the symptom.
   min-height is the 44px touch floor plus its own padding, so an empty row and
   a full one are exactly the same size. */
#actRow { flex: none; display: flex; flex-wrap: nowrap; gap: 6px; justify-content: center;
          align-items: center; min-height: 60px;
          padding: 8px 4px max(4px, env(safe-area-inset-bottom)); }
.actBtn { min-width: 44px; min-height: 44px; padding: 0 14px; border-radius: 12px; flex: 0 1 auto;
          white-space: nowrap; overflow: hidden;
          font-family: inherit; font-weight: 800; font-size: 15px; cursor: pointer;
          color: #f0fdf4; background: rgba(0,0,0,0.24);
          border: 2px solid rgba(255,255,255,0.16); }
.actBtn:active { transform: translateY(2px); }
.actBtn.go { border-color: #4ade80; color: #bbf7d0; }
.actBtn.cancel { border-color: rgba(255,255,255,0.2); }
/* The four suit buttons for an eight. Red suits are red on the button, because
   a player picking a suit is picking the thing they can see on their cards —
   not a word. */
.actBtn.suit { font-size: 26px; padding: 0 16px; line-height: 1; }
.actBtn.suit.red { color: #fca5a5; }

/* Centred, and it stays centred. It was moved to the bottom for one release,
   to keep it off the cards it was describing — but that was treating the
   symptom. The panel CARRIES the cards now (see the result rows in app.js), so
   there is nothing behind it that has to be read, and centre is where the most
   important message in the game belongs. A panel in a corner puts its buttons
   next to the home indicator and makes the eye travel for the one thing it
   most wants.
   overflow-y stays: a four-player summary is a tall panel on a short phone,
   and a message that runs off the top is worse than one that scrolls. */
.ovl { position: absolute; inset: 0; display: none; align-items: flex-start; justify-content: center; background: rgba(3,24,12,0.55); border-radius: 12px; padding: 16px; overflow-y: auto; }
.ovl.on { display: flex; }
.ovlCard { text-align: center; width: 100%; max-width: 320px; display: flex; flex-direction: column; gap: 10px; margin: auto;}
/* THE OVERLAY REPORTS ON THE TABLE, SO IT MUST NOT HIDE THE TABLE.
   It was a 90%-opaque curtain over the felt — and what it says is "Robin took
   three tricks", "the pot goes to Kit", "kings and fours". Every one of those
   sentences is about cards the player can no longer see. That is not a tuning
   nit; it is the overlay defeating its own purpose, the same shape as a word
   game refusing a word for being too hard.
   The fix is not just less alpha, which would leave white text on a busy felt.
   The scrim goes down to a dim, and the WORDS move onto a panel of their own,
   so the message is more legible than before and the table is still there
   around it. Overlays whose board is irrelevant once they appear — a game-over
   in an arcade game — are left opaque on purpose. */
.ovlCard { background: rgba(3,24,12,0.96); border: 2px solid rgba(255,255,255,0.16);
            border-radius: 18px; padding: 16px 14px;
            box-shadow: 0 10px 30px rgba(0,0,0,0.45); margin: auto;}

#ovlTitle { font-size: 26px; font-weight: 800; color: #ffffff; }
#ovlSub { color: #bbf7d0; font-weight: 700; min-height: 22px; line-height: 1.45; }

#scr-how .card { background: rgba(0,0,0,0.2); border: 3px solid rgba(255,255,255,0.13); border-radius: 16px; padding: 14px; margin-bottom: 10px; font-size: 15px; line-height: 1.55; font-weight: 600; }
#scr-how h2 { margin-bottom: 8px; color: #ffffff; }

/* The legend (docs/offline-games-pwa.md R-L1). One column of swatches at a
   fixed width so the eye can run the column instead of the text. Every mark is
   built from the same data the table paints with. */
.legend { display: flex; flex-direction: column; gap: 7px; }
.lgRow { display: flex; align-items: center; gap: 10px; }
.lgTxt { display: flex; flex-direction: column; min-width: 0; }
.lgTxt b { font-size: 14px; font-weight: 800; }
.lgTxt small { font-size: 12.5px; color: #bbf7d0; line-height: 1.3; font-weight: 600; }

/* THE LEGEND CARD IS ONE WIDTH (R-L1).
   It was `min-width`, so "10♦" came out four pixels wider than "A♠" and a row
   of example cards — a straight, a flush, a hand of five — stopped lining up
   at whichever card happened to be a ten. A legend whose job is comparison
   cannot have a column that wanders. Fixed width, and the glyph centres in
   it. */
.lgCard { display: inline-flex; align-items: center; justify-content: center; width: 30px; height: 40px;
          border-radius: 4px; background: #fff; color: #0f172a; font-weight: 800; font-size: 13px;
          border: 1px solid #94a3b8; }
.lgCard.pick { box-shadow: 0 0 0 3px #4ade80; }
.lgCard.red { color: #dc2626; }

/* The toast floats in the middle here, not at the bottom where the rest of the
   catalog puts it. This table has something at BOTH ends — the opponents and
   their books at the top, your hand at the bottom — and the toast is the
   running commentary on exactly those two things, so it must not sit on
   either. Two drafts got this wrong in opposite directions: bottom: 84px
   covered the cards, top: 108px covered the opponent it was talking about.
   40% lands in the band between the opponent block and the pond. */
#toast { position: fixed; left: 50%; top: 40%; transform: translate(-50%, -50%); background: #ffffff; color: #052e16; font-weight: 800; padding: 9px 16px; border-radius: 999px; opacity: 0; transition: opacity 0.25s; pointer-events: none; z-index: 40; max-width: calc(100vw - 32px); text-align: center; }
#toast.on { opacity: 1; }

/* docs/offline-pwa.md R-C15 — the PAGE must never scroll. The product is a
   fixed frame: header, a named scroll region, footer. */
html, body { overflow: hidden; overscroll-behavior: none; }
.scr { overscroll-behavior: contain; }

/* THE RESULT ROWS (docs/offline-games-pwa.md, "a result must carry its
   evidence"). One row per player or per team: who, the thing itself, what it
   was worth. Same class names as Poker's showdown, because it is the same idea
   and a second vocabulary for it would drift.
   `#ovlSub` becomes a column here rather than a paragraph, so the rows stack
   and their columns line up. */
#ovlSub { display: flex; flex-direction: column; gap: 5px; min-height: 0; }
.sdRow { display: flex; align-items: center; gap: 7px; padding: 5px 7px;
         border-radius: 10px; background: rgba(255,255,255,0.05);
         border: 2px solid transparent; text-align: left; }
.sdRow.you  { background: rgba(255,255,255,0.10); }
.sdRow.win  { border-color: #4ade80; background: rgba(74,222,128,0.14); }
.sdRow.out  { opacity: 0.55; }
.sdWho   { flex: 0 0 52px; font-size: 13px; font-weight: 800; }
.sdArt   { flex: 1 1 auto; display: flex; gap: 3px; flex-wrap: wrap; min-width: 0; }
.sdHand  { flex: 1 1 auto; font-size: 12px; font-weight: 700; line-height: 1.25;
           color: #bbf7d0; }
/* A rank or a card inside a result row. Fixed width for the same reason the
   legend's is: rows only form columns if a ten is as wide as an ace. */
.sdCard { display: inline-flex; align-items: center; justify-content: center;
          width: 27px; height: 34px; border-radius: 4px; background: #fff;
          color: #0f172a; font-weight: 800; font-size: 12px;
          border: 1px solid #94a3b8; }
.sdCard.red { color: #dc2626; }

/* The Settings panel's help line. It was in the markup with no rule behind it,
   so it rendered at body size in body colour where every other product shows
   it small and muted -- `just check-settings` found it once the guard started
   deriving the panel's OWN class vocabulary instead of demanding `.field` and
   `.seg` by name. Markup with no rules behind it is not a control. */
.blurb { margin: 6px 0 2px; font-size: 12.5px; line-height: 1.45; color: #bbf7d0; }

/* ---- Aggravation ------------------------------------------------------- */
/* The round chip and the contract, side by side above the felt. The contract
   is the single most-consulted thing on the screen — it is what you are trying
   to build — so it is never more than a glance away from the cards. */
#roundOut { color: #bbf7d0; white-space: nowrap; font-variant-numeric: tabular-nums; }
#contractOut { flex: 1; text-align: right; color: #fbbf24; font-weight: 800;
               white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#scoreOut { display: block; text-align: center; font-size: 11.5px; padding: 0 4px 4px; }

/* One tier button per rung, stacked so the age band and player count fit
   without truncation at 320px. */
.tier { display: block; width: 100%; text-align: left; margin: 4px 0;
        background: rgba(0,0,0,0.2); border: 3px solid rgba(255,255,255,0.14);
        border-radius: 16px; padding: 10px 14px; color: #f0fdf4;
        font-family: inherit; font-size: 16px; font-weight: 800; cursor: pointer;
        min-height: 44px; }
.tier.on { border-color: #4ade80; background: rgba(56,189,248,0.14); }
.tier span { display: block; font-size: 12px; font-weight: 700; color: #bbf7d0; }

/* Action buttons. `.off` is a control that is present but cannot be used right
   now — it is drawn dimmed rather than hidden ONLY where its absence would be
   more confusing than its presence, which is the buried-pile explainer. */
.act { flex: 0 1 auto; min-height: 44px; padding: 8px 12px; border-radius: 14px;
       border: 3px solid rgba(255,255,255,0.16); background: rgba(0,0,0,0.24);
       color: #f0fdf4; font-family: inherit; font-size: 14px; font-weight: 800;
       cursor: pointer; white-space: nowrap; }
.act.primary { border-color: #4ade80; background: rgba(74,222,128,0.18); }
.act.off { opacity: 0.55; cursor: default; }
.act.cancel { border-color: rgba(255,255,255,0.22); }
/* The row can hold three buttons on a 320px phone, and a long card name makes
   one of them too wide. Truncating inside the button beats pushing the last
   one off the screen, which is what `white-space: nowrap` alone does. */
.act { min-width: 44px; overflow: hidden; text-overflow: ellipsis; }

/* #219: THE GO DOWN BUTTON BLINKS, which is the owner's own words — "if the
   game detects I can make my melds... it blinks or does something". It blinks
   in two places and they mean two different things: on the entry button when
   your hand CAN make the contract, and on Lay It Down when the cards you have
   actually chosen do.
   prefers-reduced-motion is not decoration here. A blinking control is the
   single worst thing on a screen for someone who has asked for less movement,
   so the signal falls back to a solid border that says the same thing. */
@keyframes actPulse {
  0%, 100% { background: rgba(74,222,128,0.18); box-shadow: 0 0 0 0 rgba(74,222,128,0); }
  50%      { background: rgba(74,222,128,0.44); box-shadow: 0 0 0 5px rgba(74,222,128,0.26); }
}
.act.pulse { border-color: #4ade80; animation: actPulse 1.15s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
  .act.pulse { animation: none; background: rgba(74,222,128,0.34); }
}

/* #220: THE HAND-SORT CONTROL, ON THE PLAY SCREEN RATHER THAN IN SETTINGS.
   It is changed BETWEEN ROUNDS — round 1 is "2 sets of 3" and wants sets
   clustered, round 2 is "2 runs of 3" and wants the opposite — so filing it
   under one-time setup would be filing a per-round decision in the wrong
   drawer. It sits in the round/contract row because that row already answers
   "what am I building?", and this answers "how do I want to look at it?".
   44px, and #playTop grows to hold it: the floor is not negotiable for a
   control, and 22px of felt is what it costs. */
.sortBtn { flex: none; min-height: 44px; min-width: 44px; padding: 0 10px;
           border-radius: 12px; border: 2px solid rgba(255,255,255,0.16);
           background: rgba(0,0,0,0.24); color: #f0fdf4; font-family: inherit;
           font-weight: 800; font-size: 13px; cursor: pointer; white-space: nowrap; }
.sortBtn:active { transform: translateY(2px); }

/* LANDSCAPE ON A PHONE IS MOSTLY FURNITURE.
   At 740x360 the header, the round row, the message, the score line and the
   action row take about 200px and leave the felt 160 — under two cards tall,
   with three meld rows and a tray to fit in it. The felt is the game, so in a
   short viewport everything around it gives way first. What does NOT give way
   is the 44px touch floor: the text lines shrink, the controls do not. */
@media (orientation: landscape) and (max-height: 500px) {
  #hdr { padding: 4px 10px; padding-top: max(4px, env(safe-area-inset-top)); }
  #playTop { padding: 0 4px 2px; font-size: 12px; min-height: 0; }
  #msgOut { min-height: 15px; font-size: 11.5px; }
  #scoreOut { font-size: 10.5px; padding: 0 4px 2px; }
  #actRow { min-height: 52px; padding: 4px 4px max(2px, env(safe-area-inset-bottom)); }
}

/* The generated round list in the help. */
.rnd { display: flex; gap: 8px; padding: 3px 0; font-size: 14px; }
.rnd b { display: inline-block; min-width: 22px; color: #fbbf24; }

/* The glossary. A definition list, because that is what it is. */
.gloss { margin: 0; }
.gloss dt { font-weight: 800; color: #fbbf24; margin-top: 8px; }
.gloss dd { margin: 2px 0 0; padding-left: 0; font-size: 14px; line-height: 1.45; }

/* ---- ⚙️ Settings (docs/offline-pwa.md R-C21) ------------------------- */
#custom { width: 100%; margin: 2px 0; text-align: left; background: rgba(0,0,0,0.2); border: 3px solid rgba(255,255,255,0.13); border-radius: 16px; padding: 6px 12px 10px; }
#custom > summary { list-style: none; cursor: pointer; font-weight: 800; font-size: 16px; padding: 10px 2px; min-height: 44px; display: flex; align-items: center; gap: 8px; }
#custom > summary::-webkit-details-marker { display: none; }
#custom > summary::before { content: "▸"; transition: transform .12s; }
#custom[open] > summary::before { transform: rotate(90deg); }
.field { display: flex; align-items: center; gap: 10px; margin: 7px 0; }
.field .lbl { flex: 0 0 104px; font-size: 13.5px; font-weight: 800; color: #bbf7d0; }
.seg { flex: 1; display: flex; gap: 6px; }
.seg button { flex: 1; min-height: 44px; border-radius: 11px; border: 2px solid rgba(255,255,255,0.16); background: rgba(0,0,0,0.24); color: #f0fdf4; font-family: inherit; font-weight: 800; font-size: 13.5px; cursor: pointer; padding: 4px 2px; }
.seg button.on { background: #4ade80; border-color: #16a34a; color: #052e16; min-height: 44px;}
#optNote { font-size: 12.5px; line-height: 1.45; color: #bbf7d0; font-weight: 600; margin: 8px 2px 2px; }

/* #228: the landscape footer float moved into version.js, which owns the
   footer's layout for all 54 products. Nothing product-specific was left. */
