/* ============================================================================
   layout.css — GEOMETRY ONLY (docs/moodygg-zones.md).

   Sizing, positioning and transforms for the zone table. NO colors, fonts, or
   backgrounds live here — those stay in duel.css (mood --c-<Color> tokens, card
   skin, info-face skin, stats), and no tokens either — those are in system.css,
   which is loaded first. Linked AFTER duel.css so these geometry rules win.
   Ported from the locked mockup app/sandbox/layout-sandbox.html with its
   placeholder skin (zone tints, --m-* mood fills, monospace) dropped.

   Everything here derives from u (system.css: min(svh/12, vw/24, 80px)): card
   size is svh-driven so all seats share one card size; the centre column
   absorbs the aspect ratio. 80px is the comfort ceiling = the 1920-wide board's
   1920/24, so the nominal 24u fits the letterboxed board exactly instead of
   squeezing its centre.
   ============================================================================ */

/* ---- macro: the board is a relative frame capped at 1920×1080 and centred by
   body's grid (letterboxed past that, with a hairline so the edge reads); the
   table is a 3-column grid [ side | center | side ]. The 90px --u ceiling is
   exactly 1080/12, so 12u never outgrows the capped height. The sidebar
   overlays it (below). ---- */
.board {
  position: relative;
  width: min(100vw, 1920px);
  height: min(100svh, 1080px);
  display: block;
  border: 1px solid #000;
  overflow: hidden;
}
.table {
  position: relative; /* containing block for the corner deck/discard */
  width: 100%;
  height: 100%;
  display: grid;
  grid-template-columns: var(--mod) 1fr var(--mod);
  overflow: hidden;
}
.seat {
  position: relative;
}
.seat-side {
  display: flex;
  align-items: center;
  justify-content: center;
}
/* center stack: rows [ module | choices(1fr) | module ] */
.center {
  display: grid;
  grid-template-columns: minmax(
    0,
    1fr
  ); /* cap so overflowing cards can't inflate it */
  grid-template-rows: var(--mod) 1fr var(--mod);
  min-width: 0;
}
.seat-top,
.seat-you {
  min-width: 0;
}

/* ---- horizontal module (top / you); side seats reuse it, rotated ---- */
.module {
  display: flex;
  flex-direction: column;
  gap: var(--mod-gap);
  width: 100%;
  height: 100%;
  min-width: 0;
}
/* top opp: the same module, rotated 180° as one rigid unit (hand-strip to the
   top edge, moods upside-down) — nothing per-piece counter-rotated. */
.seat-top .module {
  transform: rotate(180deg);
}
/* side seats: the same module rotated 90° into the column. A 10u×4.75u module
   rotates to 4.75u×10u = --mod wide × 10u tall (1u corner top+bottom).
   offsetWidth is pre-transform, so the packer still measures the true width. */
.seat-side .module {
  flex: 0 0 auto;
  width: calc(10 * var(--u));
  height: var(--mod);
}
.seat-left .module {
  transform: rotate(90deg);
}
.seat-right .module {
  transform: rotate(-90deg);
}
/* Only the seats this game has: render() stamps data-players on .table. The
   COLUMNS always stay — they're where #inspector parks and what the corner
   deck/discard centre on — so an unseated side hides its module, not its
   column. Left appears at 3 players, right only at 4 (docs/moodygg-zones.md). */
.table:not([data-players="3"]):not([data-players="4"]) .seat-left .module,
.table:not([data-players="4"]) .seat-right .module {
  display: none;
}

.mood-row {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0; /* the packer owns spacing */
  padding: 0 8px;
  min-height: 0;
  min-width: 0; /* measured width tracks the zone, not the card count */
}
.hand-strip {
  height: var(--strip);
  display: flex;
  align-items: stretch;
}
.pstats {
  --cut: calc(var(--die) / 12); /* kiss-cut margin, score sticker */
  /* …on the satellites. The floor is 1px, not 1.5: this margin is carried three
     times over in the pip column, so on a landscape phone a 1.5px floor was more
     than half the pip's own size and the column outgrew the strip on floors
     alone. 1px is still a visible kiss-cut. */
  --cut-s: max(1px, calc(var(--die) / 30));
  /* how far the dithered ink edge peeks out from under the sticker. Down and to
     the right, in the VIEWER's axes — see the per-seat block below. */
  --edge-x: calc(var(--die) / 40);
  --edge-y: calc(var(--die) / 26);
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  /* the kiss-cut margins now live OUTSIDE each tile, so the old --die/9 gap put
     two white margins where there used to be clear paper. This keeps the
     ink-to-ink air the cluster reads by. */
  gap: calc(var(--die) / 5);
  /* the cluster's bleeds start at the module's leading edge, which is the board
     edge — one structural gap (the same one #inspector keeps) so the sticker has
     paper margin instead of being cut off by the viewport. */
  margin-inline-start: var(--sp-2);
  overflow: visible; /* the sticker bleed + the corner strips spill out */
  position: relative;
  /* NO z-index here on purpose: any value makes the cluster a stacking context
     and traps the corner strips inside it, under the hand cards. Left auto, the
     die and pips still paint under the hand (the cards carry a positive --z and
     come later in the DOM) while .firstmark alone can climb out. */
}
/* p-stats internals (docs/moodygg-zones.md › P-STATS): all derived from --die,
   so the whole cluster still scales as one object. Colors/fills are skin
   (duel.css) and the sticker construction is shared (system.css › THE PRESS);
   this is geometry only. Every selector is .pstats-scoped on purpose: .pips/
   .pip/.score are generic enough that bare versions would style any future
   stats readout that reused the names, making it depend on this file by
   accident. */
.pstats .pips {
  display: flex;
  flex-direction: column;
  gap: calc(var(--die) / 14);
}
/* the pip's own kiss-cut margin is its border, so it adds to the box — and at the
   smallest board that margin is FLOORED, so it stops scaling while the core keeps
   shrinking. Three of these plus their gaps is what decides the cluster's height,
   so the core is a quarter of the die (not a third): that puts the column just
   inside the score sticker's own vertical extent, and the strip's air budget is
   then set by the sticker alone. */
.pstats .pip {
  width: calc(var(--die) / 4 + 2 * var(--cut-s));
  height: calc(var(--die) / 4 + 2 * var(--cut-s));
}
.pstats .score {
  position: relative;
  width: var(--die);
  height: var(--die);
  display: flex;
  align-items: center;
  justify-content: center;
  /* the ONE readout in the cluster, so it's sized off the board (--u) and not off
     the die: the die answers "does the sticker fit the hand strip", which is a
     different question. 0.45u is what 0.5 × the old die measured, so the numeral
     didn't move when the paper tightened — it just carries more of it now.
     duel.css steps it down at three digits. */
  font-size: calc(0.45 * var(--u));
  line-height: 1;
}
.pstats .score .num {
  display: inline-block;
}
/* the beats' ratchet scale-pulses .tick per digit step; transforms are ignored
   on inline boxes, so it must be inline-block or the jump silently no-ops. */
.pstats .score .tick {
  display: inline-block;
}
/* the top opp's module is flipped 180°; counter-rotate JUST the score digits
   back upright for the viewing player (the die, pips and mark stay flipped into
   play). The number nests in .num > .tick so the beats' ratchet scales .tick
   without clobbering this rotation. */
.seat-top .score .num {
  transform: rotate(180deg);
}
/* same exception for the top opp's mood value badges: a square centred on the
   card corner, so rotating the box in place moves nothing — only the digits
   come upright, matching the score readout.
   The beats scale this very element (badgePop, ratchet), and GSAP bakes
   whatever rotation it reads into an inline transform — which outlives this
   rule when a stolen mood moves to the other board, leaving the badge
   upside-down until a reload. The primitives clear their inline transform on
   completion (anim.js) so this rule is always the one in force. Don't reach for
   the `rotate` property to dodge that: GSAP folds independent transforms into
   its own and stamps `rotate: none` inline. */
.seat-top .badge.mood {
  transform: rotate(180deg);
}
/* same exception for the top opp's corner strips: the module is flipped 180°, so
   the labels alone come back upright. Third of four counter-rotations — the
   fourth is the seat-name label, which folds its 180° into its own tilt
   (duel.css › seat-name labels). */
.seat-top .firstmark span {
  rotate: 180deg;
}
/* the corner label strips: they sit ON the die's outer corners rather than
   beside them — each has to cross the cut line to read as a strip stuck over the
   sticker, not a second sticker. YOU's right edge is carried round to every
   other seat by that seat's module rotation.
   --fm is their cap size, and BOTH the size and the offsets hang off it rather
   off the die: the cap has a legibility floor (it answers a yes/no on a phone),
   so below that floor it stops shrinking while the die keeps going, and a
   die-proportional offset then walked the strip down over the numeral until it
   covered it. Offset by a fraction of the label instead and the strip always
   hangs the same fraction of ITSELF off the corner, at every board size. */
.pstats .firstmark {
  --fm: max(7px, calc(var(--u) * 0.14));
  /* the strips hang off the DIE's corners, but their containing block is the
     whole cluster — they have to stay out of .score, which is an isolate (see
     board.js › pstatsEl). The die is centred in the cluster, so its top and
     bottom edges are each 50% − die/2 in from the cluster's; the right edges
     already coincide, the die being the last item. */
  --fm-y: calc(50% - var(--die) / 2 - 0.9 * var(--fm));
  position: absolute;
  top: var(--fm-y);
  right: calc(-0.8 * var(--fm));
  display: grid;
  place-items: center;
  font-size: var(--fm);
  padding: calc(var(--die) / 34) calc(var(--die) / 9) calc(var(--die) / 20);
  /* over a lifted/inspected hand card (201): on a small board the strip's tail
     runs under the neighbouring card, and half a HURT label reads as none. */
  z-index: 202;
}
.pstats .firstmark.hurt {
  top: auto;
  bottom: var(--fm-y);
}
/* ---- one light, four seats. Each seat's module is rotated as one rigid unit,
   which carries the printed features around with it — correct, they're printed
   ON the sticker. The dithered ink edge is the exception: it's the system's only
   depth cue, so it states a direction, and a depth cue that points a different
   way per seat reads as four different tables. It stays down and to the right in
   the VIEWER's axes at every seat, by pre-rotating the offset by the inverse of
   that seat's rotation:
     top 180° → (-x, -y)    left 90° → ( y, -x)    right -90° → (-y, x)
   Side seats inherit this file's stance on text: nothing but the top seat
   counter-rotates its readouts, since a ±90° module reads as a card turned
   sideways on the table rather than as upside-down text. ---- */
.seat-top .pstats {
  --edge-x: calc(-1 * var(--die) / 40);
  --edge-y: calc(-1 * var(--die) / 26);
}
.seat-left .pstats {
  --edge-x: calc(var(--die) / 26);
  --edge-y: calc(-1 * var(--die) / 40);
}
.seat-right .pstats {
  --edge-x: calc(-1 * var(--die) / 26);
  --edge-y: calc(var(--die) / 40);
}
.fan {
  flex: 1;
  position: relative;
  min-width: 0;
}
.piles {
  flex: 0 0 auto;
  display: flex;
  align-items: flex-start;
  gap: 4px;
  padding: 0 8px;
}
/* hand fan: full-height cards tucked to the edge (viewport clips). top:0 at
   every seat — YOU/side hang off the bottom edge; the top opp's 180° flip lands
   the same top:0 on its top edge. overlap is set by the packer. */
.fan-inner {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
}

/* ---- cards in the new zones ----
   duel.css sizes .card off --card-w/--card-h (redefined above) and owns its
   skin. Here we only neutralise the old fan-arc transform and set the flex
   basis so packed cards don't stretch. */
.mood-row > .card,
.fan-inner > .card {
  flex: 0 0 auto;
  transform: none;
}

/* lift-on-focus, YOUR hand only: the tapped/hovered/paying/selected card rises
   out of the 1u tuck by exactly the tucked amount (card_h − strip) so the whole
   card clears the strip into play, frontmost. Scoped with .seat-you + #youHand
   so it beats duel.css's old -12px hand-lift rules.
   The :hover halves live in the (hover: hover) block below — see duel.css's
   "hover is the DESKTOP look channel" note: on touch :hover sticks to the
   last-tapped card, which stranded a deselected hand card up out of the tuck
   with nothing selected. */
.seat-you #youHand > .card.focused,
.seat-you #youHand > .card.lifted,
.seat-you #youHand > .card.selected {
  transform: translateY(calc(-1 * (var(--card-h) - var(--strip))));
  z-index: 1000;
}
/* .fan-inner's translateX makes it a stacking context that traps a lifted
   card's z inside the fan; raise the whole fan above the mood-row, but only
   while it actually holds a lifted card. */
.seat-you #youHand:has(> .card.focused),
.seat-you #youHand:has(> .card.lifted),
.seat-you #youHand:has(> .card.selected) {
  z-index: 1000;
}
@media (hover: hover) {
  .seat-you #youHand > .card:hover {
    transform: translateY(calc(-1 * (var(--card-h) - var(--strip))));
    z-index: 1000;
  }
  .seat-you #youHand:has(> .card:hover) {
    z-index: 1000;
  }
}
/* an opponent's hand — top, left or right: only ever lifts the revealed paying
   card (.lifted) or an inspected one (.focused). Each module's own rotation turns
   this same upward lift into a move into play on screen — by construction, like
   YOU. Scoped to .seat, so the fan is always the seat's own. */
.seat .fan-inner > .card.lifted,
.seat .fan-inner > .card.focused {
  transform: translateY(calc(-1 * (var(--card-h) - var(--strip))));
  z-index: 1000;
}
.seat .fan-inner:has(> .card.lifted),
.seat .fan-inner:has(> .card.focused) {
  z-index: 1000;
}
/* in-play mood focus: pure restack above its neighbours to show the full art —
   no positional move (unlike the hand lift). */
.mood-row > .card.focused {
  transform: none;
  z-index: 1000;
}
@media (hover: hover) {
  .mood-row > .card:hover {
    z-index: 1000;
  }
}

/* ---- shared deck + discard: table-level corners, tucked so ~1u peeks. .slot
   (duel.css) still sizes them; here we only dock + tuck. deck bottom-right by
   YOU's draw edge, discard bottom-left. ---- */
.table > .deck,
.table > .discard {
  position: absolute;
  bottom: calc(-1 * (var(--card-h) - var(--strip)));
  z-index: 4;
}
.table > .deck {
  right: calc(
    (var(--mod) - var(--card-w)) / 2
  ); /* centred in the right column */
}
.table > .discard {
  left: calc((var(--mod) - var(--card-w)) / 2); /* centred in the left column */
}
/* §4 discard browser zone: starts just right of the discard pile, ends at the
   deck's left edge — same corner math, so it can never run under either pile.
   Tucked with them; packZone (board.js) owns the overlap, flex-start owns the
   left alignment (cards hug the discard, they don't spread). */
.table > .discard-fan {
  position: absolute;
  bottom: calc(-1 * (var(--card-h) - var(--strip)));
  left: calc((var(--mod) - var(--card-w)) / 2 + var(--card-w) + var(--gap));
  right: calc((var(--mod) - var(--card-w)) / 2 + var(--card-w) + var(--gap));
  display: flex;
  z-index: 4;
}
/* open: the whole browser rises out of the tuck by exactly the tucked amount —
   the same move a hand card makes. #discard rides along so its empty slot (the
   close button) and the fanned cards read as one row. */
.table > .discard.open,
.table > .discard-fan.open {
  transform: translateY(calc(-1 * (var(--card-h) - var(--strip))));
  z-index: 500; /* over CHOICES and a resting hand, under #inspector */
}

/* ---- CHOICES: the centre row. Content scales to fit (a sentence + up to 7
   controls); the button skin stays in duel.css. ---- */
.choices {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: calc(0.28 * var(--u));
  padding: 0 12px;
  min-height: 0;
  overflow: hidden;
  text-align: center;
}

/* ---- seat-name label: it lives IN the seat, centred on that seat's cards in
   hand, so it is unambiguous about whose name it is without a single line of
   placement JS. Two facts of the fan do the work: .fan is already the strip's
   flexed remainder and .fan-inner centres itself inside it, so a label centred
   in .fan lands on the middle of the packed row at every hand size; and .fan is
   exactly --strip tall, which is the band of the hand that is visible at every
   seat (the cards tuck out of it), so "centred in .fan" is "centred on the
   backs you can see".
   Last child of .fan, so it paints over .fan-inner without claiming a z-index —
   which leaves a lifted card (z 1000, both the card and the fan) free to pass
   over it, correct: the card being played is the thing to read.
   The packer and the choreography both miss it by construction — packZone
   measures .fan's clientWidth, packAudit and Flip.getState walk .fan-inner's
   children, and the label is .fan-inner's sibling. ---- */
.pname {
  position: absolute;
  top: 50%;
  left: 50%;
  translate: -50% -50%;
  /* load-bearing, not cosmetic: the look channel resolves a tap through
     e.target.closest(".card") (board.js), so a label that could swallow one
     would dismiss the info-box instead of inspecting the back under it. */
  pointer-events: none;
  font-size: var(--fs-s);
  padding: 3px 7px 4px;
  white-space: nowrap;
  max-width: 90%;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* blank == empty, no visibility flag (the #inspector idiom): render() leaves
   your own seat and the slots this game doesn't seat blank, and they stop
   existing on screen. */
.pname:empty {
  display: none;
}

/* the one solid action button form factor — lobby + in-game menu share it
   (skin/bg/weight in duel.css). Padding-driven pills, u-scaled with px
   caps (side padding > top/bottom). Ported from the sandbox — the u-scaling
   replaces the old fixed 8px 14px + its mobile override, so no @media step is
   needed, and the whole app's buttons read as one form factor. */
.menu-acts {
  gap: min(calc(0.2 * var(--u)), var(--btn-gap));
}
.lobby-btn,
.menu-acts button:not(.chip),
.chip,
.text-input {
  padding: min(calc(0.18 * var(--u)), var(--btn-pad-y))
    min(calc(0.55 * var(--u)), var(--btn-pad-x));
  border-radius: min(calc(0.2 * var(--u)), var(--btn-radius));
  font-size: clamp(9px, calc(0.34 * var(--u)), var(--btn-fs));
}

/* ---- sidebar: a left drawer over the full-bleed table. Closed by default
   (slid off-screen); #navToggle flips .open to slide it in. z above lifted/
   flying cards (1000) but below the lobby screens (2100). ---- */
.board > .sidebar {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  width: min(
    280px,
    calc(8 * var(--u))
  ); /* caps at 280 on desktop; ~1/3 vw on a phone */
  z-index: 1100;
  padding: 8px;
  gap: 12px;
  align-items: stretch;
  overflow-y: auto;
  transform: translateX(-100%);
  transition: transform 0.2s ease;
}
.board > .sidebar.open {
  transform: none;
}
/* hamburger: fixed top-left, always reachable, above the drawer it toggles.
   Sized off --die so it never exceeds a p-stats die, then capped at a usual
   hamburger size so it doesn't balloon toward 1u on desktop. */
.nav-toggle {
  position: absolute; /* to the letterboxed .board, not the viewport */
  top: 8px;
  left: 8px;
  z-index: 1200;
  --ham: min(var(--die), 36px);
  width: var(--ham);
  height: var(--ham);
  font-size: calc(0.62 * var(--ham));
}

/* ---- info-box portal: one fixed slot inside the board, not a box glued to the
   inspected card (docs › Info-box). duel.css skins .info-face; all the geometry
   is here, so board.js only picks a side.

   Vertically: centred on the board, which IS the CHOICES centre (the centre grid
   is [ --mod | 1fr | --mod ], symmetric). Its 6.5u max height in a 12u board
   means a centred box can never clip — no clamp needed.
   Horizontally: centred in the side column — the dead strip between the prompt
   and the screen edge — so it clears CHOICES entirely whenever it fits there,
   while sitting as close to the prompt as the screen allows. max() keeps it
   inside the board on a narrow screen, where it creeps inward instead. The
   sticker bleeds --info-cut outside the face, so that floor has to clear the
   bleed too or the sticker overhangs the board edge on the narrow case.

   A fixed slot can park over a card (a narrow screen creeps it into the centre
   column). On hover devices that would be a flicker loop: the box covers the
   hovered card -> pointerout -> box clears -> pointerover -> reopens. Ignoring
   the pointer breaks it, and hover needs no scrolling — the wheel just falls
   through to the card underneath. Touch keeps its events: tapping the box must
   NOT dismiss it (the tap gate looks for .card, so a swallowed tap is an ignored
   tap) and a long effect has to be scrollable by drag. ---- */
#inspector {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  right: max(
    calc(var(--sp-2) + var(--info-cut)),
    calc((var(--mod) - var(--info-w)) / 2)
  );
  width: var(--info-w);
  z-index: 9999;
  text-align: left;
  pointer-events: none;
}
/* opposite side for a right-seat card, so its own moods stay readable */
#inspector.on-left {
  right: auto;
  left: max(
    calc(var(--sp-2) + var(--info-cut)),
    calc((var(--mod) - var(--info-w)) / 2)
  );
}
/* closed == empty: board.js only fills/clears innerHTML, no visibility flag */
#inspector:empty {
  display: none;
}
@media (hover: none) and (pointer: coarse) {
  #inspector {
    pointer-events: auto;
  }
}
/* the cut margin lives outside the face, so the face can't be the scroller any
   more: it keeps the height clamp but overflows visibly, and .fx becomes the only
   scroller under a pinned header (duel.css owns that). */
#inspector .info-face {
  max-height: var(--info-max);
  overflow: visible;
  display: flex;
  flex-direction: column;
}

/* ---- page graphic: the sticker sheet the rotate gate and the 404 are printed
   on (duel.css owns its skin). The ONE measurement everything else derives from
   is --pg, the sticker's PAPER size — the padded viewBox means dice-friend
   himself occupies ~78% of it.

   Bounded on BOTH axes, unlike every other component here: this is the one
   graphic that shows up in portrait (the gate) and in a 320px-tall landscape
   phone (a 404 mid-game), and a width-only clamp overflows the short case —
   ~220px of sticker stacked over an --fs-l bubble does not fit in 320px. svh
   rather than vh so a mobile toolbar sliding in can't push it off. ---- */
.page-graphic {
  --pg: clamp(120px, min(44vw, 30svh), 246px);
  /* content-sized and self-centring rather than inheriting the host's width. The
     satellites are hung off THIS box's edges in percentages, so a full-width
     parent would throw them a proportion of the VIEWPORT outward instead of a
     proportion of the sticker sheet, and push them off screen. All three current
     hosts centre in a grid and so already shrink-wrap it; owning the width here
     is what keeps that from being a thing each host has to remember. */
  width: fit-content;
  margin-inline: auto;
}

/* the leftovers, hung off the sticker they orbit and sized off --pg so they
   travel with it */
.pg-s1 {
  top: -9%;
  left: -17%;
  rotate: -16deg;
  width: calc(var(--pg) * 0.19);
}
.pg-s2 {
  top: 46%;
  right: -19%;
  rotate: 13deg;
  width: calc(var(--pg) * 0.23);
}
.pg-s3 {
  bottom: 4%;
  left: -21%;
  rotate: 9deg;
  width: calc(var(--pg) * 0.15);
}

/* on a phone the sheet runs out of paper: the bubble is already most of the
   viewport, so a satellite hung a fifth of it further out hangs off the screen.
   Pull them in — a mark half past the edge reads as a bug, not as a sticker
   sheet continuing. (The overlay also clips, so this is the look, not the
   safety net.) */
@media (max-width: 480px) {
  .pg-s1 {
    left: -9%;
  }
  .pg-s2 {
    right: -11%;
  }
  .pg-s3 {
    left: -12%;
  }
}
