/* hax-haxopoly.css — Haxopoly play surface, mobile-first per HaxopolyDesign/docs/design.md §10.
   Batch F (2026-06-03): live-state styling — owner badges, building markers, tokens, mortgage
   dim, centre HUD with viewer's Credits + piece + state lines, status chrome, action chrome,
   resign-confirm overlay. Static board layout from the thin slice carries through unchanged.

   iOS Safari guardrails per [[reference-ios-safari-css-traps]]:
   • box-sizing: border-box on every cell — padding + the sector band don't eat the grid gap
   • min-width: 0 + min-height: 0 on grid items — webkit otherwise defaults to min:auto and
     children can overflow the row
   • overflow: hidden on the board container so a single glyph mid-render can't burst the loop

   Mobile-first sizing: the board sits inside a container-query context. Cell padding + font
   sizes use `cqi` (container-query inline) units so the whole layout scales smoothly from a
   360px portrait iPhone up through desktop without media queries. */

/* ---------- Board container + grid ---------- */

.haxopoly-play {
    width: 100%;
    max-width: 560px;               /* desktop cap — board was overpowering wide viewports at 720 */
    margin: 0 auto;
    padding: 8px;
    box-sizing: border-box;
}

.haxopoly-board {
    /* The board fills the available width up to ~560px (see .haxopoly-play max-width);
       aspect-ratio: 1 keeps it square.
       Non-uniform 7×7 grid: outer rows/columns (which carry corners + property tiles + Wormhole
       transit + card spaces + Starship Maintenance) get 1.5fr each; inner rows/columns (which
       only contain the centre hub) get 1fr each. Total 8fr per axis, so outer cells are
       1.5/8 = 18.75% of board (≈ 105px on a 560px board) and inner cells are 1/8 = 12.5%
       (≈ 70px). That gives every property tile ≈ 50% more area than a uniform-fr layout — more
       room for the name + price + building markers without growing the board's footprint. The
       centre hub still spans the 5 inner cells. */
    container-type: inline-size;
    aspect-ratio: 1 / 1;
    display: grid;
    grid-template-rows: 1.5fr 1fr 1fr 1fr 1fr 1fr 1.5fr;
    grid-template-columns: 1.5fr 1fr 1fr 1fr 1fr 1fr 1.5fr;
    gap: 2px;
    background: #0B1437;            /* §14.2 deep-space navy frame */
    border: 2px solid #14181F;
    border-radius: 6px;
    overflow: hidden;
    box-sizing: border-box;
}

/* ---------- Cells (every grid item) ---------- */

.haxopoly-cell {
    background: #FCFCFD;            /* §14.2 card cream */
    color: #14181F;                 /* §14.2 ink */
    box-sizing: border-box;
    min-width: 0;                   /* iOS Safari grid-item overflow guard */
    min-height: 0;
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    justify-content: center;
    overflow: hidden;
    font-family: var(--mud-typography-default-family, sans-serif);
}

/* ---------- Corner tiles ---------- */

.haxopoly-cell--corner {
    background: #FAF6EC;            /* slightly warmer than the property cream */
    padding: 4cqi 2cqi;
    align-items: center;
    justify-content: center;
    gap: 1cqi;
}

.haxopoly-corner-icon {
    width: 60%;
    height: auto;
    max-height: 60%;
    object-fit: contain;
}

.haxopoly-corner-label {
    font-size: clamp(0.55rem, 2.4cqi, 0.85rem);
    font-weight: 600;
    text-align: center;
    line-height: 1.05;
    letter-spacing: 0.02em;
}

/* ---------- Edge property + transit + card-space tiles ---------- */

.haxopoly-cell--top,
.haxopoly-cell--right,
.haxopoly-cell--bottom,
.haxopoly-cell--left {
    /* Universal edge-cell padding — keeps the name + price + glyph clear of the cell borders at
       every viewport. Was 1.6cqi / 1.2cqi which resolved to ~6px / ~5px on mobile and looked
       cramped on the live render. Flat 6px is comfortable + the band-side padding overrides
       below carry the band's own clearance. */
    padding: 6px 6px;
    gap: 0.8cqi;
}

/* Sector colour band — a thin colored strip flush against the inside edge of each property
   tile (the side facing the centre hub). All four edge variants use position: absolute so the
   band butts directly against the cell border, no white-space gap between the band and the
   inside edge. The cell adds matching padding on that side so the body content doesn't overlap
   the band.
   12% of cell width/height = a clearly thin strip; body takes the remaining 88%.
   Earlier in-flex-flow approach (top/bottom only) floated the band in the middle of the cell
   because justify-content: center centered the band+body cluster — symptom the operator caught
   in the live render. Absolute positioning is consistent for all four edges and unambiguous. */
.haxopoly-cell--top,
.haxopoly-cell--bottom,
.haxopoly-cell--left,
.haxopoly-cell--right {
    position: relative;
    /* Breathing room between the glyph (Wormhole / Anomaly / Guild / Starship Maintenance) and
       the tile name + price stack below. Overrides the 0.8cqi value on the shared edge-cell
       rule above; 14px ≈ a blank line at the tile-name font size, visually distinct at every
       viewport. */
    gap: 14px;
}

.haxopoly-cell--top {
    padding-bottom: calc(12% + 4px);
}

.haxopoly-cell--bottom {
    padding-top: calc(12% + 4px);
}

.haxopoly-cell--right {
    padding-left: calc(12% + 4px);
}

.haxopoly-cell--left {
    padding-right: calc(12% + 4px);
}

.haxopoly-cell--top .haxopoly-sector-band {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;                      /* inside edge = the side facing the centre */
    height: 12%;
    min-height: 6px;
}

.haxopoly-cell--bottom .haxopoly-sector-band {
    position: absolute;
    left: 0;
    right: 0;
    top: 0;                         /* inside edge for bottom-row tiles */
    height: 12%;
    min-height: 6px;
}

.haxopoly-cell--right .haxopoly-sector-band {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;                        /* inside edge for right-column tiles */
    width: 12%;
    min-width: 6px;
}

.haxopoly-cell--left .haxopoly-sector-band {
    position: absolute;
    top: 0;
    bottom: 0;
    right: 0;                       /* inside edge for left-column tiles */
    width: 12%;
    min-width: 6px;
}

/* ---------- Tile body (name + price stack) ---------- */

.haxopoly-tile-body {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    /* Breathing room between the property name and the purchase-price line below it. Bumped
       to 12px ≈ a blank line at the tile-name font size for the side-column tiles which have
       the full cell height available (their sector band runs vertically along the inside
       edge). Top + bottom row tiles override below with a tighter gap because their band
       eats into cell height. */
    gap: 12px;
    min-width: 0;
    min-height: 0;
}

/* Top + bottom row property tiles have ~12% of cell height consumed by the sector band, and
   left + right column tiles use the inner 1fr rows (shorter than the 1.5fr outer rows) — both
   variants are vertically constrained relative to the side-band 1.5fr tiles, so the 12px
   universal body gap pushed the name + price stack over the edge for long names. Tighten the
   gap here so the wrapping case fits without bumping the cell borders. */
.haxopoly-cell--top .haxopoly-tile-body,
.haxopoly-cell--bottom .haxopoly-tile-body,
.haxopoly-cell--left .haxopoly-tile-body,
.haxopoly-cell--right .haxopoly-tile-body {
    gap: 5px;
}

.haxopoly-tile-name {
    font-size: clamp(0.50rem, 1.85cqi, 0.78rem);
    font-weight: 600;
    line-height: 1.05;
    letter-spacing: 0.01em;
    /* word-break: break-word is equivalent to "break anywhere" — it was wrapping mid-word
       ("Wormhol e Alpha", "Anomal y"). The proper modern pair: word-break: normal honours
       word boundaries; overflow-wrap: break-word only breaks INSIDE a word when the word
       itself can't fit at all, which is the polite-wrap behavior we want. */
    word-break: normal;
    overflow-wrap: break-word;
    hyphens: auto;
}

.haxopoly-tile-price {
    font-size: clamp(0.45rem, 1.6cqi, 0.7rem);
    color: #4B5363;
    font-weight: 500;
    letter-spacing: 0.02em;
}

/* Starship Maintenance tax label — same role as the price label (sub-text under the tile
   name), but coloured warm-red to telegraph "this space costs you" the way the Mtg overlay
   does. The percentage text itself carries the meaning so color is never the sole cue. */
.haxopoly-tile-tax {
    font-size: clamp(0.45rem, 1.6cqi, 0.7rem);
    color: #B91C1C;
    font-weight: 600;
    letter-spacing: 0.02em;
}

/* ---------- Narrow-board compact labels (operator 2026-10-03) ----------
   On a phone the side columns run on the board's short inner rows (~44px), and the glyph + a
   two-line name pushed the tax rate / move offset off the bottom of the cell. Below the width
   where that happens, those side cells drop the glyph and the stacked body for one label -
   "Maint. Tax - 25%", "Grav. Flux −4". Keyed to the BOARD's width (it is the container), not
   the device. 500px, not 560 (operator 2026-10-03): `.haxopoly-play` caps the desktop board at
   ~540px, so a 560 switch put desktop in compact mode too - desktop keeps the full tile. The
   compact div is aria-hidden; the full body stays in the DOM for screen readers. */
.haxopoly-tile-compact,
.haxopoly-tile-name--compact {
    display: none;
}

/* Property names with a word too long for a narrow inner tile ("Cassiopeia") swap to a short
   name below the same board width - see HaxopolyBoardGrid.CompactNames. The price stays. */
@container (max-width: 500px) {
    .haxopoly-tile-name--full {
        display: none;
    }

    .haxopoly-tile-name--compact {
        display: block;
    }
}

@container (max-width: 500px) {
    .haxopoly-cell--left .haxopoly-tile-compact,
    .haxopoly-cell--right .haxopoly-tile-compact {
        display: block;
        font-size: clamp(0.50rem, 1.85cqi, 0.78rem);
        font-weight: 600;
        line-height: 1.15;
        text-align: center;
    }

    .haxopoly-cell--left:has(.haxopoly-tile-compact) .haxopoly-space-icon,
    .haxopoly-cell--right:has(.haxopoly-tile-compact) .haxopoly-space-icon,
    .haxopoly-cell--left:has(.haxopoly-tile-compact) .haxopoly-tile-body,
    .haxopoly-cell--right:has(.haxopoly-tile-compact) .haxopoly-tile-body {
        display: none;
    }

    .haxopoly-tile-compact-num {
        color: #B91C1C;          /* the tax red the full tile uses; the number carries the meaning */
        white-space: nowrap;
    }

    /* The dash travels with its number, so a wrap reads "Maint. Tax" / "- 25%" rather than
       leaving the dash alone at the end of the first line (operator 2026-10-03). */
    .haxopoly-tile-compact-tail {
        white-space: nowrap;
    }
}

/* ---------- Space icons (Wormhole, Anomaly, Guild, Starship Maintenance) ---------- */

.haxopoly-space-icon {
    /* 50% width with height: auto keeps the icon square based on cell width without reserving
       50% of cell HEIGHT in the flex layout — the prior height: 50% rule ate vertical room
       proportional to cell height (74px on outer rows) and pushed the price line off the
       bottom of Wormhole tiles. height: auto lets the icon be just as tall as it is wide
       (square PNGs). */
    width: 50%;
    height: auto;
    object-fit: contain;
    margin: 0.4cqi auto;
    flex: 0 0 auto;
}

/* Left + right column icon-bearing tiles (Anomaly, Maintenance) use the shorter inner 1fr row
   height (~49px on iPhone XR vs ~74px for the 1.5fr outer rows feeding top + bottom icon
   tiles). The 50% icon was bumping the cell border on those sides; drop to 38% so the icon +
   name fit comfortably with the new 6px padding. Also tighten the cell flex gap (icon ↔ body)
   to 6px because the universal 14px gap eats more space than the short cells can spare. */
.haxopoly-cell--left .haxopoly-space-icon,
.haxopoly-cell--right .haxopoly-space-icon {
    width: 38%;
    height: 38%;
}

.haxopoly-cell--left,
.haxopoly-cell--right,
.haxopoly-cell--top,
.haxopoly-cell--bottom {
    /* Cell flex gap between the icon and the body for icon-bearing tiles (Wormhole / Anomaly /
       Guild / Maintenance). 6px works for both axes: top + bottom Wormhole tiles need it
       because they carry icon + 2-line name + price in the inner 1fr column (~50px wide on
       small viewports); the universal 14px above was overflowing the price. Property tiles
       have a single body child, so flex gap doesn't apply — this rule only affects the
       icon-bearing variants. */
    gap: 6px;
}

/* ---------- Sector colour palette (§14.2) ---------- */

.haxopoly-sector--darkpurple { background: #6B3FA0; }
.haxopoly-sector--lightblue  { background: #8FD4F0; }
.haxopoly-sector--pink       { background: #E66FA0; }
.haxopoly-sector--orange     { background: #F0962D; }
.haxopoly-sector--red        { background: #D8352A; }
.haxopoly-sector--yellow     { background: #F4C430; }
.haxopoly-sector--green      { background: #2F9E52; }
.haxopoly-sector--darkblue   { background: #1F3A93; }

/* Odyssey upper-stage sectors (HaxopolyUpgrade/docs/design.md 5.2). Deliberately distinct from
   the lower eight: the two palettes are never seen side by side ON THE BOARD, since only one
   stage renders at a time, but they collide in the property-management list where a player's
   whole portfolio appears together. That list is the surface they were validated against.
   Reviewed live via HaxopolyUpgrade/odyssey-palette.htm. Colour is never the sole indicator —
   tiles carry name and price, and the title-deed names the sector in text. */

.haxopoly-sector--nebula      { background: #B5179E; }  /* deeper + redder than lower Pink */
.haxopoly-sector--pulsar      { background: #0E9594; }  /* no teal on the lower board */
.haxopoly-sector--quasar      { background: #8C6239; }  /* no brown on the lower board */
.haxopoly-sector--singularity { background: #7B1E3A; }  /* deep wine vs lower's bright Red */

/* ---------- Centre HUD (§10.2 lean centre) ---------- */

.haxopoly-centre {
    background: #0B1437;
    color: #FCFCFD;
    box-sizing: border-box;
    min-width: 0;
    min-height: 0;
    /* flex column with the stack growing to fill (its own content vertically centred via
       its own justify-content) and the recap docking at the bottom — keeps the player info
       on the eye line while the "what just happened" banner is anchored at the inner edge
       of the bottom row. */
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 4cqi 3cqi;
    /* position: relative lets the in-hub Roll button render in the centre flex flow while the
       Resign button absolutely-positions into the top-right corner (2026-06-04 — moved into
       the hub so both action affordances are visible without scrolling on default viewports). */
    position: relative;
}

.haxopoly-centre-stack {
    /* 2-column layout (2026-06-04): piece glyph LEFT, credits + roll/waiting + kept cards
       RIGHT. Frees vertical room above the docked recap banner. */
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
    flex: 1 1 auto;
    /* Widened 4cqi -> 8cqi (operator 2026-09-22) when the credits chip became a button: the
       piece and the chip were close enough to read as one object, and a tappable thing needs
       to look separable from the decoration next to it. Container units, so the breathing room
       scales with the board instead of collapsing on mobile. */
    gap: 8cqi;
    text-align: center;
}

.haxopoly-centre-info {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2cqi;
    min-width: 0;
}

.haxopoly-roll-hub {
    width: 18cqi;
    height: 18cqi;
    min-width: 64px;
    min-height: 64px;
    border-radius: 50%;
    background: linear-gradient(135deg, #A3E635 0%, #6FB91D 100%);
    color: #14181F;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
    border: 3px solid #FCFCFD;
}

.haxopoly-roll-hub-glyph {
    font-weight: 800;
    font-size: clamp(0.85rem, 3.8cqi, 1.4rem);
    line-height: 1;
}

.haxopoly-roll-hub-label {
    font-weight: 700;
    font-size: clamp(0.55rem, 2.2cqi, 0.9rem);
    margin-top: 2cqi;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

.haxopoly-centre-note {
    font-size: clamp(0.60rem, 2.0cqi, 0.85rem);
    color: #C7CFDC;
    max-width: 26ch;
    line-height: 1.3;
}

/* ---------- Owner badges (corner piece-glyph + accent ring per §10.2) ---------- */

.haxopoly-owner-badge {
    position: absolute;
    top: 0.4cqi;
    right: 0.4cqi;
    /* Ownership "tag" — visually distinct from a player token. Token is 6cqi (min 20px);
       badge is 4cqi (min 13px) so the eye reads them as marker vs piece, not as two pieces
       on the same tile. Tightened 2026-06-04 per operator. */
    width: 4cqi;
    height: 4cqi;
    min-width: 13px;
    min-height: 13px;
    border-radius: 50%;
    background: #FCFCFD;
    border: 1px solid #14181F;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
    z-index: 2;
}

.haxopoly-owner-badge--p1 {
    /* Challenger accent ring — the player's profile colour (set on the board root as --hax-p1),
       falling back to §14.2 challenger lime when unset or on a same-colour collision. */
    box-shadow: 0 0 0 1px var(--hax-p1, #A3E635), 0 1px 2px rgba(0, 0, 0, 0.18);
}

.haxopoly-owner-badge--p2 {
    /* Challengee accent ring — profile colour (--hax-p2), fallback §14.2 challengee coral. */
    box-shadow: 0 0 0 1px var(--hax-p2, #F4845F), 0 1px 2px rgba(0, 0, 0, 0.18);
}

.haxopoly-owner-badge-img {
    /* Tighter than the token's 78% so the smaller badge doesn't crowd its ring. */
    width: 85%;
    height: 85%;
    object-fit: contain;
}

/* Owner badge MUST sit on the OUTER perimeter of the tile (the edge facing AWAY from the
   centre), NEVER on the sector colour band — the band holds the §14.4 building markers and a
   badge over it would cover the colonies / starport. The base rule pins badges at
   top: right (correct for top-row + right-column tiles since their outer is the top/right
   corner). Bottom + left edges need an explicit override. 2026-06-04 operator request. */
.haxopoly-cell--bottom .haxopoly-owner-badge {
    top: auto;
    bottom: 0.4cqi;
    /* right: 0.4cqi from base — outer bottom-right corner */
}

.haxopoly-cell--left .haxopoly-owner-badge {
    right: auto;
    left: 0.4cqi;
    /* top: 0.4cqi from base — outer top-left corner */
}

/* The badge may overlap the first or last letter of a side cell's name - operator 2026-10-03,
   after a reserved-space fix pushed names off-centre: "it's ok if the player owned glyph is on
   top of the property card". Centred names across the board matter more; do not pad for it. */

/* ---------- Building markers — sit ON the sector band per §14.4 ---------- */

/* The band hosts the marker row. Top + bottom row bands lay out horizontally; left + right
   column bands lay out vertically. Markers shrink to fit the band's thin dimension. */
.haxopoly-sector-band {
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
}

.haxopoly-cell--top .haxopoly-sector-band,
.haxopoly-cell--bottom .haxopoly-sector-band {
    flex-direction: row;
    gap: 4px;
}

.haxopoly-cell--left .haxopoly-sector-band,
.haxopoly-cell--right .haxopoly-sector-band {
    flex-direction: column;
    gap: 4px;
}

.haxopoly-building-markers {
    display: contents;            /* let the marker children flex inside the band directly */
}

.haxopoly-building-marker {
    background: #FFF8E1;          /* cream fill */
    border: 1px solid #14181F;    /* ink outline */
    flex: 0 0 auto;
}

.haxopoly-building-marker--colony {
    /* Bumped 5 → 7px on 2026-06-04 — colonies were hard to spot on the band at default
       viewport. Still fits the band's 12% thickness on outer-row + inner-row tiles on
       desktop; mild overflow on the narrowest mobile inner-column cells but the shape
       reads clearly as a square. */
    width: 7px;
    height: 7px;
}

.haxopoly-building-marker--starport {
    /* Bumped 7 → 9px on 2026-06-04 — starport is a single marker per band so it has room
       to be a touch bolder than a colony. */
    width: 9px;
    height: 9px;
    border-radius: 50%;
}

/* ---------- Player tokens (current position on the loop) ---------- */

.haxopoly-token {
    position: absolute;
    bottom: 0.6cqi;
    width: 6cqi;
    height: 6cqi;
    min-width: 20px;
    min-height: 20px;
    border-radius: 50%;
    /* Solid player-colour fill per 2026-06-04 operator visual brief — the cream-fill +
       coloured-border combo read identically to the owner-tag badges + made it hard to
       tell "this is the player's CURRENT position" vs "this property is owned". The
       solid colour fill makes position-tokens pop. */
    border: 2px solid #14181F;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
    z-index: 3;
}

/* Two tokens on the same cell — separate left/right so they don't overlap. */
.haxopoly-token--p1 {
    left: 0.6cqi;
    background: var(--hax-p1, #A3E635);   /* profile colour; fallback §14.2 challenger lime */
}

.haxopoly-token--p2 {
    right: 0.6cqi;
    background: var(--hax-p2, #F4845F);   /* profile colour; fallback §14.2 challengee coral */
}

.haxopoly-token-img {
    width: 78%;
    height: 78%;
    object-fit: contain;
    /* The piece PNG is a dark silhouette on transparent — invert it so the glyph reads
       white on the solid coloured background. Same trick the Wormhole medallion uses
       (no new assets needed). */
    filter: brightness(0) invert(1);
}

/* ---------- Mortgaged plot dim ---------- */

.haxopoly-cell.is-mortgaged {
    filter: grayscale(0.55) brightness(0.78);
}

.haxopoly-cell.is-mortgaged::after {
    content: "Mtg";
    position: absolute;
    bottom: 0.4cqi;
    right: 0.4cqi;
    font-size: clamp(0.45rem, 1.4cqi, 0.65rem);
    color: #B91C1C;
    font-weight: 700;
    letter-spacing: 0.03em;
    background: #FCFCFD;
    padding: 0.2cqi 0.6cqi;
    border-radius: 2px;
    z-index: 1;
}

/* ---------- Centre HUD additions (Credits chip + piece label + state line) ---------- */

.haxopoly-centre-piece {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.8cqi;
}

.haxopoly-centre-piece-disc {
    /* Solid player-colour disc + dark ring + white silhouette glyph — same visual as the
       board tokens so the centre glyph reads as "your piece" on the loop. Background
       colour set by the --p1 / --p2 variants below. */
    width: 14cqi;
    height: 14cqi;
    min-width: 44px;
    min-height: 44px;
    border-radius: 50%;
    border: 2px solid #14181F;
    background: #A3E635;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}

.haxopoly-centre-piece-disc--p1 {
    background: var(--hax-p1, #A3E635);   /* profile colour; fallback §14.2 challenger lime */
}

.haxopoly-centre-piece-disc--p2 {
    background: var(--hax-p2, #F4845F);   /* profile colour; fallback §14.2 challengee coral */
}

.haxopoly-centre-piece-img {
    width: 78%;
    height: 78%;
    object-fit: contain;
    /* Dark silhouette PNG → white glyph on the coloured disc; same trick the board token
       + wormhole medallion use. */
    filter: brightness(0) invert(1);
    /* The centre player-piece glyph is a pure display element — no click handler. Harden it so
       it's fully inert: on mobile a bare <img> otherwise invites the long-press image callout
       ("Save Image") + drag-to-save gesture, which is undesirable on a game glyph. (It was also a
       suspect in a board-wedge a tester reported 2026-06-08, but that turned out non-reproducible
       — likely a transient session; this is defensive hardening, not a confirmed-bug fix.)
       Making a no-op glyph inert can't regress anything; the rest of the board is untouched.
         - pointer-events:none        → the glyph receives no taps (it does nothing anyway)
         - -webkit-touch-callout:none → suppress the iOS long-press image callout
         - -webkit-user-drag:none     → suppress the native image-drag */
    pointer-events: none;
    -webkit-touch-callout: none;
    -webkit-user-drag: none;
}

.haxopoly-centre-piece-label {
    font-size: clamp(0.55rem, 2.0cqi, 0.85rem);
    font-weight: 600;
    color: #C7CFDC;
    letter-spacing: 0.02em;
}

/* ---------- Resolved-state centre takeover (2026-06-06 operator request) ----------
   Replaces the single-viewer piece/credits HUD with stacked stats blocks — one per
   player — so each player can see who won and by how much in §8 net-worth terms. */

.haxopoly-resolved-stats {
    /* flex: 1 1 auto + justify-content: center mirrors .haxopoly-centre-stack — grows to fill
       the navy hub so the recap banner docks at the BOTTOM of the centre area instead of
       pinning directly under the stats. */
    display: flex;
    flex-direction: column;
    justify-content: center;
    flex: 1 1 auto;
    gap: 2.4cqi;
    width: 100%;
    padding: 1cqi 1cqi;
    box-sizing: border-box;
}

.haxopoly-resolved-card {
    display: flex;
    align-items: center;
    gap: 2.4cqi;
    padding: 1.4cqi 2cqi;
    border-radius: 8px;
    background: rgba(255, 255, 255, 0.04);
    border: 1px solid rgba(255, 255, 255, 0.12);
}

.haxopoly-resolved-card.is-winner {
    background: rgba(163, 230, 53, 0.10);          /* §14.2 lime tint */
    border-color: rgba(163, 230, 53, 0.45);
}

/* Shrink the piece disc inside the resolved card vs the live-play centre HUD — two cards
   stacked in the cramped mobile centre need a smaller glyph so the recap stays docked at
   the bottom instead of getting pushed into the property row below. */
.haxopoly-resolved-card .haxopoly-centre-piece-disc {
    width: 11cqi;
    height: 11cqi;
    min-width: 38px;
    min-height: 38px;
    flex-shrink: 0;
}

.haxopoly-resolved-card-info {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    gap: 0.6cqi;
    min-width: 0;                                  /* allow text shrink on narrow centre */
}

.haxopoly-resolved-card-name {
    display: flex;
    align-items: center;
    gap: 1.2cqi;
    font-size: clamp(0.55rem, 2.0cqi, 0.85rem);
    font-weight: 700;
    color: #FCFCFD;
    letter-spacing: 0.02em;
}

.haxopoly-resolved-card-winner-pill {
    font-size: clamp(0.40rem, 1.4cqi, 0.65rem);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: #14181F;
    background: #A3E635;
    padding: 0.2cqi 1cqi;
    border-radius: 999px;
}

.haxopoly-resolved-card-networth {
    display: flex;
    align-items: baseline;
    gap: 1.6cqi;
    flex-wrap: wrap;
}

.haxopoly-resolved-card-networth-label {
    font-size: clamp(0.48rem, 1.8cqi, 0.85rem);
    color: #A3E635;
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
}

.haxopoly-resolved-card-networth-value {
    /* Lower min so mobile shrinks more (the 7,696 Cr was floored too large on a 430px viewport
       and stretched the two cards past the navy hub's vertical budget, pushing the recap into
       the property row below). Desktop unchanged at the 2rem max. */
    font-size: clamp(0.90rem, 4.4cqi, 2rem);
    font-weight: 800;
    color: #FCFCFD;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}

.haxopoly-resolved-card-breakdown {
    font-size: clamp(0.52rem, 1.9cqi, 0.8rem);
    color: #C7CFDC;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.01em;
    line-height: 1.3;
}

.haxopoly-centre-credits {
    display: flex;
    align-items: baseline;
    gap: 1cqi;
    padding: 1.6cqi 3cqi;
    background: rgba(255, 255, 255, 0.08);
    border-radius: 6px;
    border: 1px solid rgba(255, 255, 255, 0.18);
}

.haxopoly-centre-credits-glyph {
    font-size: clamp(0.60rem, 2.2cqi, 0.95rem);
    color: #A3E635;
    font-weight: 700;
    letter-spacing: 0.04em;
}

.haxopoly-centre-credits-value {
    font-size: clamp(0.95rem, 4cqi, 1.6rem);
    font-weight: 800;
    color: #FCFCFD;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}

.haxopoly-centre-state {
    /* Top-left pin (2026-06-04) — mirrors the Resign button's top-right corner. Detention /
       skip-flag is a passive status indicator, not an action; corner placement keeps it
       visible without competing with the centred piece + credits stack. */
    position: absolute;
    top: 6px;
    left: 6px;
    z-index: 5;
    font-size: clamp(0.55rem, 1.7cqi, 0.75rem);
    color: #F4845F;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    text-align: left;
    padding: 4px 8px;
    background: rgba(244, 132, 95, 0.12);
    border: 1px solid rgba(244, 132, 95, 0.45);
    border-radius: 4px;
}

/* ---------- Header + status badges + recap line + action chrome ---------- */

.haxopoly-play-game {
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.haxopoly-play-header {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.haxopoly-play-status {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
}

.haxopoly-play-status-badge {
    font-weight: 700;
    font-size: 0.95rem;
    padding: 4px 12px;
    border-radius: 4px;
    letter-spacing: 0.02em;
}

.haxopoly-play-status-badge.turn-you {
    background: #A3E635;
    color: #14181F;
}

.haxopoly-play-status-badge.turn-opp {
    background: #2A3142;
    color: #FCFCFD;
}

.haxopoly-play-status-badge.win {
    background: #A3E635;
    color: #14181F;
}

.haxopoly-play-status-badge.loss {
    background: #B91C1C;
    color: #FCFCFD;
}

.haxopoly-play-meta {
    /* 0.85 -> 0.95rem (operator 2026-10-03, "one size larger" once Bank left the line): the
       badge's own size, so the two ends of the row read as a pair. */
    font-size: 0.95rem;
    font-weight: 700;             /* bold, like the badge (operator 2026-10-03) */
    /* #C7CFDC, not the #6B6B6B grey this carried until 2026-08-04. This readout sits on the
       dark-green card (--hax-surface-card #053D2E), where that grey measured 2.3:1 — under
       half the WCAG AA 4.5:1 floor, which is why players reported not being able to read the
       turn/bank line at all. #C7CFDC is 7.8:1 (AAA) and is already this file's secondary-text-
       on-dark colour (.haxopoly-centre-note, .haxopoly-centre-piece-label, the resolved-card
       breakdown), so the readout matches its siblings instead of inventing a hex. Deliberately
       NOT lime: lime is the site's winner/success signal (--hax-status-success) and would both
       borrow that meaning and out-shout the turn badge beside it. The old grey is fine on
       Haxopoly's CREAM surfaces (landing card, card overlay) and is left alone there. */
    color: #C7CFDC;
    font-variant-numeric: tabular-nums;
    /* Right-justified against the board's edge, opposite the badge (operator 2026-10-03) - the
       row's free space was empty. If a narrow phone wraps it under the badge, it stays right. */
    margin-left: auto;
    text-align: right;
}

.haxopoly-play-recap {
    /* In-hub recap docked at the BOTTOM of the centre navy area (2026-06-04 operator
       request, re-tightened after a too-big first cut). Same shape as the original
       below-board recap (ink-on-cream with the lime left-accent), just relocated. Sibling
       of .haxopoly-centre-stack so the flex layout pins it to the bottom while the stack
       grows to fill above. Font scales down on small viewports via clamp + cqi so mobile
       doesn't crowd the surrounding tiles. */
    flex: 0 0 auto;
    margin: 0;
    padding: 6px 10px;
    background: #FAF6EC;
    border-left: 3px solid #A3E635;
    border-radius: 0 4px 4px 0;
    /* Stepped down one notch 2026-09-22 (operator): the board got more interactive and the
       Credit Ledger now answers "where did my money go", so players lean on this line less than
       they did when it was the only account of the turn. All three clamp stops move together so
       it shrinks on mobile too, not only on desktop. */
    font-size: clamp(0.58rem, 2.1cqi, 0.8rem);
    line-height: 1.25;
    color: #14181F;
    text-align: left;
    font-weight: 500;
    width: 100%;
    box-sizing: border-box;
}

.haxopoly-play-recap--empty {
    /* "Fresh deck. Fresh dice. Fresh empire." flavour text — softer + italic so it doesn't
       compete with the live recap once the first roll lands. */
    background: transparent;
    border-left-color: #C7CFDC;
    color: #C7CFDC;
    font-style: italic;
    font-weight: 400;
}

.haxopoly-play-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
    padding-top: 4px;
}

.haxopoly-play-roll {
    /* Make the primary action visually distinct from secondary outlined controls. */
    min-width: 180px;
}

.haxopoly-play-submit-error {
    width: 100%;
}

.haxopoly-play-pregame,
.haxopoly-play-terminal {
    padding: 16px 8px;
    display: flex;
    flex-direction: column;
}

.haxopoly-play-pregame-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

/* ---------- Resign-confirm overlay ---------- */

.haxopoly-overlay {
    position: fixed;
    inset: 0;
    background: rgba(11, 20, 55, 0.65);
    display: flex;
    align-items: center;
    justify-content: center;
    /* Top padding clears the fixed MudAppBar, which sits ABOVE this overlay. The overlay scrolls
       so a card taller than the screen can always be reached (operator 2026-09-26, iPhone: a
       title deed with build actions grew past the viewport, its X hid under the app bar, and
       with no scroll the only way out was a sliver of scrim). */
    padding: calc(var(--mud-appbar-height, 64px) + 12px) 16px 16px;
    overflow-y: auto;
    overscroll-behavior: contain;
    z-index: 1000;
}

/* "Safe" centring: auto margins centre the card while it fits and pin it to the top edge once
   it does not - align-items: center alone pushes the top of a tall card off-screen, where
   scrolling cannot reach it. */
.haxopoly-overlay > .haxopoly-overlay-card {
    margin: auto;
}

.haxopoly-overlay-card {
    background: #FCFCFD;            /* §14.2 card cream */
    color: #14181F;                 /* §14.2 ink — forces dark text against the surrounding
                                       dark-theme play surface that otherwise leaks its light
                                       text colour into the popup. */
    border-radius: 8px;
    padding: 20px;
    max-width: 360px;
    width: 100%;
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
    display: flex;
    flex-direction: column;
    gap: 12px;
    /* position: relative establishes the positioning context for the absolute-positioned
       .haxopoly-overlay-close X button (added 2026-06-04 on informational popups). The
       resign-confirm + landing-card overlays don't render the X — both require an explicit
       decision — but the base-class context is set here once. */
    position: relative;
}

.haxopoly-overlay-close {
    position: absolute;
    top: 8px;
    right: 8px;
    z-index: 10;
    appearance: none;
    background: transparent;
    border: none;
    color: #14181F;
    font-size: 1.5rem;
    font-weight: 700;
    line-height: 1;
    padding: 2px 10px;
    cursor: pointer;
    border-radius: 4px;
    font-family: inherit;
    transition: background 120ms;
}

.haxopoly-overlay-close:hover {
    background: rgba(20, 24, 31, 0.08);
}

.haxopoly-overlay-close:focus-visible {
    outline: 2px solid #A3E635;
    outline-offset: 2px;
}

/* MudText components have their own colour rules tied to mud-palette CSS variables; inside
   the popup we want them to inherit the card's ink colour rather than the dark-theme's light
   foreground. Same shape Lexigon's overlay uses (via mud-palette vars) — this is just the
   inheritance variant since we hard-code the cream brand background. */
.haxopoly-overlay-card .mud-typography {
    color: inherit;
}

/* The same leak, for every other Mud component in the popup: re-point the palette at the cream
   card. Without it an OUTLINED button took the dark theme's near-white text and vanished, while
   a DISABLED one took the theme's deep-emerald disabled colour and read as a normal button - so
   on a built title deed "Sell one" and "Done" were invisible and the disabled Mortgage looked
   live (operator 2026-09-26, iPhone). Disabled now reads as a greyed-out control. */
.haxopoly-overlay-card {
    --mud-palette-text-primary: #14181F;
    --mud-palette-text-secondary: #5A6472;
    --mud-palette-action-default: #4A5361;
    --mud-palette-action-default-hover: rgba(20, 24, 31, 0.06);
    --mud-palette-action-disabled: rgba(20, 24, 31, 0.38);
    --mud-palette-action-disabled-background: rgba(20, 24, 31, 0.12);
}

.haxopoly-overlay-hint {
    color: #6B6B6B;
}

.haxopoly-overlay-actions {
    display: flex;
    gap: 8px;
    justify-content: flex-end;
    margin-top: 4px;
}

/* ---------- Kept cards display in centre HUD ---------- */

.haxopoly-centre-kept {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    justify-content: center;
    align-items: center;
    margin-top: 2cqi;
    max-width: 32ch;
}

.haxopoly-centre-kept-label {
    font-size: clamp(0.50rem, 1.6cqi, 0.70rem);
    color: #C7CFDC;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    margin-right: 4px;
}

.haxopoly-centre-kept-pill {
    background: #1F3A93;
    color: #FCFCFD;
    font-size: clamp(0.45rem, 1.5cqi, 0.65rem);
    padding: 2px 8px;
    border-radius: 12px;
    font-weight: 600;
    letter-spacing: 0.01em;
    border: 1px solid #FCFCFD;
}

/* ---------- In-hub action buttons (2026-06-04) ----------
   Operator request: Roll + Resign were below the board, off-screen on the default viewport on
   both mobile + desktop. Both moved into the navy centre hub so they're always visible without
   scrolling. Roll sits under Credits in the centre stack; Resign tucks into the top-right corner
   of the hub via absolute positioning. */

.haxopoly-centre-roll {
    appearance: none;
    background: #A3E635;            /* lime — §14.2 primary / challenger accent */
    color: #14181F;
    border: none;
    border-radius: 8px;
    padding: 8px 14px;
    font-family: inherit;
    font-weight: 800;
    /* Floor dropped 2026-06-04 so the button text shrinks on mobile viewports — was
       clamping at 0.7rem which read big inside the narrow centre hub on iPhone XR. */
    font-size: clamp(0.55rem, 2cqi, 0.95rem);
    letter-spacing: 0.04em;
    text-transform: uppercase;
    cursor: pointer;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
    transition: background 120ms, transform 80ms;
    margin-top: 0.4cqi;
    white-space: nowrap;
}

.haxopoly-centre-roll:hover:not(:disabled) {
    background: #B5F050;
}

.haxopoly-centre-roll:active:not(:disabled) {
    transform: translateY(1px);
}

.haxopoly-centre-roll:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

/* Immunity escape button — shown when detained and holding a kept immunity card. Shares the
   same shape/size as the Roll button but uses a golden accent to distinguish the special action. */
.haxopoly-centre-immunity {
    appearance: none;
    background: #F59E0B;            /* amber — "special power" accent */
    color: #14181F;
    border: none;
    border-radius: 8px;
    padding: 8px 14px;
    font-family: inherit;
    font-weight: 800;
    font-size: clamp(0.55rem, 2.2cqi, 0.8rem);
    letter-spacing: 0.03em;
    text-transform: uppercase;
    cursor: pointer;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
    transition: background 120ms, transform 80ms;
    margin-top: 0.4cqi;
    white-space: nowrap;
}

.haxopoly-centre-immunity:hover:not(:disabled) {
    background: #FBBF24;
}

.haxopoly-centre-immunity:active:not(:disabled) {
    transform: translateY(1px);
}

.haxopoly-centre-immunity:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

.haxopoly-centre-waiting {
    /* Renders in the same vertical slot the Roll button would occupy on the active player's
       view — keeps the hub from looking empty when it's the opponent's turn and gives the
       viewer a clear "this isn't my turn" cue beyond just the top status badge. */
    font-size: clamp(0.6rem, 2cqi, 0.85rem);
    color: #C7CFDC;                 /* muted on-navy text — same palette as kept-card labels */
    text-align: center;
    font-style: italic;
    letter-spacing: 0.02em;
    margin-top: 0.4cqi;
    padding: 8px 12px;
    max-width: 22ch;
    line-height: 1.3;
}

.haxopoly-centre-resign {
    position: absolute;
    top: 6px;
    right: 6px;
    z-index: 5;
    appearance: none;
    background: transparent;
    color: #F4845F;                 /* coral — §14.2 challengee / "destructive" accent */
    border: 1px solid rgba(244, 132, 95, 0.55);
    border-radius: 4px;
    font-family: inherit;
    font-size: clamp(0.55rem, 1.7cqi, 0.75rem);
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    padding: 4px 10px;
    cursor: pointer;
    transition: background 120ms, border-color 120ms;
}

.haxopoly-centre-resign:hover:not(:disabled) {
    background: rgba(244, 132, 95, 0.12);
    border-color: #F4845F;
}

.haxopoly-centre-resign:disabled {
    opacity: 0.4;
    cursor: not-allowed;
}

/* ---------- Resolved-view decisive callout ---------- */

.haxopoly-play-decisive {
    margin: 0;
    padding: 8px 12px;
    background: #FFF8E1;
    border-left: 3px solid #F4C430;
    font-size: 0.95rem;
    font-weight: 500;
    color: #14181F;
    border-radius: 0 4px 4px 0;
}

/* ---------- Clickable tile affordance ---------- */

.haxopoly-cell.is-clickable {
    cursor: pointer;
}

.haxopoly-cell.is-clickable:hover {
    filter: brightness(1.03);
}

/* ---------- Title-deed popup (§10.1 standardized single-surface card) ---------- */

.haxopoly-titledeed {
    max-width: 420px;
    padding: 0;
    overflow: hidden;
}

.haxopoly-titledeed-header {
    display: flex;
    align-items: stretch;
    background: #FAF6EC;
    border-bottom: 1px solid #E6E0CC;
}

.haxopoly-titledeed-sector {
    flex: 0 0 12px;
}

.haxopoly-titledeed-title {
    padding: 12px 16px;
    flex: 1;
    min-width: 0;
}

/* Viewer's cash-on-hand, right-aligned in the header (player request 2026-07-10). Padding-right
   clears the absolutely-positioned .haxopoly-overlay-close X. Distinct label + darker-lime "Cr"
   echo the centre-HUD Credits readout while staying legible on the cream card. */
.haxopoly-titledeed-wallet {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    justify-content: center;
    flex: 0 0 auto;
    /* Right padding clears the .haxopoly-overlay-close X (occupies the rightmost ~42px: right:8 +
       ~34px glyph box) with a comfortable gap so the credits don't crowd the button. */
    padding: 12px 58px 12px 8px;
    text-align: right;
    white-space: nowrap;
}

.haxopoly-titledeed-wallet-label {
    font-size: 0.62rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: #8A8266;
    line-height: 1;
}

.haxopoly-titledeed-wallet-value {
    margin-top: 2px;
    font-size: 1.05rem;
    font-weight: 800;
    color: #14181F;
    font-variant-numeric: tabular-nums;
    line-height: 1.1;
}

.haxopoly-titledeed-wallet-cr {
    margin-left: 3px;
    font-size: 0.72rem;
    font-weight: 700;
    color: #5F8C0B;
    letter-spacing: 0.02em;
}

.haxopoly-titledeed-body {
    padding: 16px;
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.haxopoly-titledeed-rent {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.85rem;
}

.haxopoly-titledeed-rent td {
    padding: 4px 8px;
    border-bottom: 1px solid #F0EBDF;
}

.haxopoly-titledeed-rent td:last-child {
    text-align: right;
    font-variant-numeric: tabular-nums;
    font-weight: 600;
}

.haxopoly-titledeed-rent tr.is-current {
    background: #FFF8E1;
    border-left: 3px solid #A3E635;
}

.haxopoly-titledeed-rent tr.is-current td {
    font-weight: 700;
}

.haxopoly-titledeed-row {
    display: flex;
    justify-content: space-between;
    font-size: 0.9rem;
    padding: 4px 0;
    border-bottom: 1px solid #F0EBDF;
}

.haxopoly-titledeed-row span:last-child {
    font-variant-numeric: tabular-nums;
    font-weight: 600;
}

.haxopoly-titledeed-status {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-top: 4px;
}

.haxopoly-titledeed-status-pill {
    font-size: 0.75rem;
    padding: 3px 8px;
    border-radius: 12px;
    font-weight: 600;
    letter-spacing: 0.02em;
}

.haxopoly-titledeed-status-pill.is-unowned {
    background: #E6E0CC;
    color: #14181F;
}

.haxopoly-titledeed-status-pill.is-yours {
    /* Viewer's own profile colour (--hax-yours), fallback §14.2 lime. Paired with the "Yours"
       text label, so colour is never the sole ownership signal. */
    background: var(--hax-yours, #A3E635);
    color: var(--hax-yours-ink, #14181F);   /* white on violet/indigo, dark on the rest - computed in HaxopolyBoardColors.InkOn */
}

.haxopoly-titledeed-status-pill.is-opp {
    background: var(--hax-opp, #F4845F);
    color: var(--hax-opp-ink, #14181F);
}

.haxopoly-titledeed-status-pill.is-mortgaged {
    background: #B91C1C;
    color: #FCFCFD;
}

.haxopoly-titledeed-status-pill.is-developed {
    background: #1F3A93;
    color: #FCFCFD;
}

.haxopoly-titledeed-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    padding: 0 16px 8px;
}

/* ---------- Staged-actions chip ---------- */

.haxopoly-staged-chip {
    display: flex;
    align-items: center;
    gap: 8px;
    background: #FFF8E1;
    border: 1px solid #A3E635;
    border-radius: 4px;
    padding: 4px 10px;
    font-size: 0.85rem;
    color: #14181F;
    width: 100%;
}

.haxopoly-staged-chip-count {
    background: #A3E635;
    color: #14181F;
    font-weight: 700;
    padding: 1px 8px;
    border-radius: 12px;
    font-variant-numeric: tabular-nums;
}

/* ---------- §14.1 Force End testing panel (Dev only) ---------- */

.haxopoly-force-end-panel {
    margin-top: 16px;
    padding: 12px 14px;
    border: 1px dashed #F4845F;          /* coral-dashed = visibly experimental */
    border-radius: 6px;
    background: rgba(244, 132, 95, 0.06);
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.haxopoly-force-end-header {
    display: flex;
    align-items: center;
    gap: 8px;
}

.haxopoly-force-end-pill {
    background: #F4845F;
    color: #FCFCFD;
    font-size: 0.65rem;
    padding: 2px 8px;
    border-radius: 10px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

/* Dev-only board setter on the title deed (operator 2026-09-26). Dashed like a scaffold so it
   never reads as part of the real card; the same coral pill as the Force End panel. */
.haxopoly-devboard {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 6px;
    margin: 4px 16px 0;
    padding: 8px 10px;
    border: 1px dashed #F4845F;
    border-radius: 6px;
}

.haxopoly-devboard-line {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px;
}

.haxopoly-devboard-label {
    flex: 0 0 64px;
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    opacity: 0.7;
}

.haxopoly-devboard .mud-button-root {
    min-width: 0;
    padding: 2px 8px;
}

.haxopoly-force-end-title {
    font-weight: 700;
    font-size: 0.9rem;
    color: #14181F;
}

.haxopoly-force-end-hint {
    margin: 0;
    font-size: 0.8rem;
    color: #4B5363;
    line-height: 1.4;
}

.haxopoly-force-end-section {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.haxopoly-force-end-section-label {
    font-size: 0.75rem;
    font-weight: 600;
    color: #14181F;
    letter-spacing: 0.02em;
}

.haxopoly-force-end-buttons {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}

.haxopoly-force-end-followup {
    margin: 4px 0 0 0;
    font-size: 0.72rem;
    color: #6B6B6B;
    line-height: 1.4;
    font-style: italic;
}

/* ---------- Stats cards CSS — used by HaxopolyStatsComponent (design.md §12) ---------- */

.haxopoly-stats {
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.haxopoly-stats-grid {
    display: flex;
    flex-direction: column;
    gap: 16px;
}

.haxopoly-stats-event-title {
    margin: 0 0 8px 0;
    font-size: 1.05rem;
    font-weight: 600;
}

.haxopoly-stats-stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
    gap: 8px;
    margin: 0 0 12px 0;
    padding: 0;
}

.haxopoly-stats-stats > div {
    background: var(--mud-palette-surface);
    border: 1px solid var(--mud-palette-divider);
    border-radius: 6px;
    padding: 8px 12px;
}

.haxopoly-stats-stats dt {
    margin: 0;
    font-size: 0.75rem;
    color: var(--mud-palette-text-secondary);
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

.haxopoly-stats-stats dd {
    margin: 2px 0 0 0;
    font-size: 1.3rem;
    font-weight: 700;
}

/* Headline four read as the at-a-glance identity — accent the values. */
.haxopoly-stats-headline dd {
    color: var(--mud-palette-primary);
}

/* Secondary qualifier (e.g. the property a highest-rent record was set on). */
.haxopoly-stats-sub {
    display: block;
    margin-top: 2px;
    font-size: 0.72rem;
    font-weight: 400;
    color: var(--mud-palette-text-secondary);
}

/* Native <details> disclosure for the deeper grids — no JS interop. */
.haxopoly-stats-details > summary {
    cursor: pointer;
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--mud-palette-text-secondary);
    margin-bottom: 8px;
    user-select: none;
}

.haxopoly-stats-group-title {
    margin: 4px 0 6px 0;
    font-size: 0.8rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--mud-palette-text-secondary);
}

.haxopoly-stats-empty {
    color: var(--mud-palette-text-secondary, #6b6b6b);
    margin: 12px 0;
    font-style: italic;
    text-align: center;
}

/* Light purple (operator 2026-10-04; CoreDesign/samples/carousel-card-panel.htm). */
.haxopoly-event-card-description {
    margin: 0;
    color: #c4b5fd;
}

/* ---------- Accept-time piece picker (challengee side) ---------- */

.haxopoly-pregame-yourpiece {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-top: 8px;
}

.haxopoly-pregame-yourpiece-img {
    width: 36px;
    height: 36px;
    object-fit: contain;
    background: #FCFCFD;
    border-radius: 50%;
    padding: 4px;
    border: 2px solid #A3E635;
}

.haxopoly-accept-pieces-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 8px;
    max-width: 480px;
}

@media (max-width: 420px) {
    .haxopoly-accept-pieces-grid {
        grid-template-columns: repeat(3, 1fr);
    }
}

.haxopoly-accept-piece {
    appearance: none;
    background: #FCFCFD;
    border: 2px solid #E6E0CC;
    border-radius: 8px;
    padding: 8px 4px 6px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    cursor: pointer;
    font-family: inherit;
    position: relative;
    transition: border-color 120ms, background 120ms, opacity 120ms;
}

.haxopoly-accept-piece:hover:not(:disabled) {
    border-color: #A3E635;
    background: #FAFFE7;
}

.haxopoly-accept-piece.is-selected {
    border-color: #A3E635;
    background: #FFF8E1;
    box-shadow: 0 0 0 2px #A3E635 inset;
}

.haxopoly-accept-piece.is-taken,
.haxopoly-accept-piece:disabled {
    cursor: not-allowed;
    opacity: 0.4;
    filter: grayscale(0.6);
}

.haxopoly-accept-piece-img {
    width: 44px;
    height: 44px;
    object-fit: contain;
}

.haxopoly-accept-piece-name {
    font-size: 0.72rem;
    font-weight: 600;
    text-align: center;
    line-height: 1.1;
    color: #14181F;
    word-break: keep-all;
}

.haxopoly-accept-piece-taken-pill {
    position: absolute;
    top: 4px;
    right: 4px;
    background: #B91C1C;
    color: #FCFCFD;
    font-size: 0.55rem;
    font-weight: 700;
    padding: 1px 5px;
    border-radius: 6px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

/* ---------- Throwdown options — piece picker (§17 8-piece roster) ---------- */

.haxopoly-throwdown-options {
    margin-top: 12px;
}

.haxopoly-throwdown-options-title {
    margin-bottom: 4px;
}

.haxopoly-throwdown-options-hint {
    display: block;
    margin-bottom: 10px;
    color: var(--mud-palette-text-secondary, #6b6b6b);
}

.haxopoly-throwdown-options-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 8px;
}

@media (max-width: 420px) {
    .haxopoly-throwdown-options-grid {
        grid-template-columns: repeat(3, 1fr);
    }
}

.haxopoly-throwdown-options-piece {
    appearance: none;
    background: #FCFCFD;
    border: 2px solid #E6E0CC;
    border-radius: 8px;
    padding: 8px 4px 6px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    cursor: pointer;
    font-family: inherit;
    transition: border-color 120ms, background 120ms, transform 120ms;
}

.haxopoly-throwdown-options-piece:hover {
    border-color: #A3E635;
    background: #FAFFE7;
}

.haxopoly-throwdown-options-piece.is-selected {
    border-color: #A3E635;
    background: #FFF8E1;
    box-shadow: 0 0 0 2px #A3E635 inset;
}

.haxopoly-throwdown-options-piece-img {
    width: 48px;
    height: 48px;
    object-fit: contain;
}

.haxopoly-throwdown-options-piece-name {
    font-size: 0.75rem;
    font-weight: 600;
    text-align: center;
    line-height: 1.1;
    color: #14181F;
    word-break: keep-all;
}

/* ---------- F.3.b §10.1 standardized landing card ---------- */
/* Reuses .haxopoly-overlay + .haxopoly-overlay-card from the resign overlay for the dark
   backdrop + cream card + colour inheritance. Adds the §10.1-spec horizontal header (medallion
   left, TITLE-DEED chip + property name stacked right), a body with the rent ladder + price,
   and the fixed bottom BUY/DECLINE row. */

.haxopoly-landing-card {
    max-width: 420px;
    padding: 0;
    overflow: hidden;
}

.haxopoly-landing-header {
    display: flex;
    align-items: stretch;
    background: #FAF6EC;
    border-bottom: 1px solid #E6E0CC;
}

.haxopoly-landing-medallion {
    flex: 0 0 64px;
    min-height: 64px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-right: 1px solid #E6E0CC;
}

.haxopoly-landing-medallion--wormhole {
    background: #0B1437;            /* deep-space navy frame for Wormholes */
}

.haxopoly-landing-medallion--wormhole img {
    width: 60%;
    height: auto;
    object-fit: contain;
    filter: invert(1);              /* white-on-navy */
}

.haxopoly-landing-title {
    padding: 10px 14px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 2px;
    flex: 1;
    min-width: 0;
}

.haxopoly-landing-chip {
    font-size: 0.65rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    color: #6B6B6B;
    text-transform: uppercase;
}

/* Cash-on-hand on the landing (buy/pass) card. Same readout as the title-deed inspector — reuses
   its -label/-value/-cr text styling — but this card has no close X, so it right-aligns flush with
   the header padding. */
.haxopoly-landing-wallet {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    justify-content: center;
    flex: 0 0 auto;
    padding: 10px 16px 10px 8px;
    text-align: right;
    white-space: nowrap;
}

.haxopoly-landing-name {
    font-size: 1.15rem;
    font-weight: 700;
    color: #14181F;
    line-height: 1.15;
    word-break: normal;
    overflow-wrap: break-word;
}

.haxopoly-landing-sub {
    font-size: 0.75rem;
    color: #6B6B6B;
    line-height: 1.2;
}

.haxopoly-landing-body {
    padding: 14px 16px;
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.haxopoly-landing-lead {
    margin: 0;
    font-size: 0.9rem;
    color: #14181F;
    line-height: 1.35;
}

.haxopoly-landing-price-row {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    padding: 8px 0;
    border-top: 1px solid #F0EBDF;
    border-bottom: 1px solid #F0EBDF;
}

.haxopoly-landing-price-label {
    font-size: 0.85rem;
    color: #4B5363;
    font-weight: 600;
}

.haxopoly-landing-price-value {
    font-size: 1.25rem;
    font-weight: 800;
    color: #14181F;
    font-variant-numeric: tabular-nums;
}

.haxopoly-landing-cant-afford {
    font-size: 0.8rem;
    color: #B91C1C;
    background: rgba(185, 28, 28, 0.08);
    border-left: 3px solid #B91C1C;
    padding: 6px 10px;
    border-radius: 0 4px 4px 0;
    font-weight: 500;
}

.haxopoly-landing-actions {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
    padding: 12px 16px 14px;
    border-top: 1px solid #E6E0CC;
    background: #FAF6EC;
}

.haxopoly-landing-buy,
.haxopoly-landing-decline {
    /* Plain HTML buttons instead of MudButton — MudBlazor's variant/color theming wasn't
       inheriting the cream-card context, which made the disabled BUY illegible and DECLINE
       nearly invisible (operator caught 2026-06-04). Same approach as the centre Roll +
       Resign buttons. */
    appearance: none;
    font-family: inherit;
    font-weight: 700;
    font-size: 0.85rem;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    padding: 8px 16px;
    border-radius: 6px;
    cursor: pointer;
    transition: background 120ms, border-color 120ms, opacity 120ms, transform 80ms;
}

.haxopoly-landing-buy {
    background: #A3E635;            /* lime — primary action */
    color: #14181F;
    border: none;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.18);
}

.haxopoly-landing-buy:hover:not(:disabled) {
    background: #B5F050;
}

.haxopoly-landing-buy:active:not(:disabled) {
    transform: translateY(1px);
}

.haxopoly-landing-buy:disabled {
    background: #E6E0CC;            /* warm gray on the cream card — clearly inert */
    color: #9A9385;
    box-shadow: none;
    cursor: not-allowed;
}

.haxopoly-landing-decline {
    background: transparent;
    color: #14181F;
    border: 1.5px solid #14181F;
}

.haxopoly-landing-decline:hover:not(:disabled) {
    background: rgba(20, 24, 31, 0.06);
}

.haxopoly-landing-decline:disabled {
    color: #9A9385;
    border-color: #C7C0A8;
    cursor: not-allowed;
}

/* ---------- Card inspector overlay (2026-06-05) ----------
   Operator request: recap line's drawn-card name + each tucked-card pill open a popup that
   shows the full card (deck label + name + flavor + effect). Reuses .haxopoly-overlay +
   .haxopoly-overlay-card shell from the title-deed popup for consistent chrome; the
   .haxopoly-card-overlay class layers card-specific layout on top, and the .is-anomaly /
   .is-guild variants tint the header rail per deck (anomaly = hazard orange, guild = reward
   teal). */

/* Inline card-name button inside .haxopoly-play-recap — looks like a link sitting in the
   cream recap strip, not a button, so the recap reads as prose. Borderless, no background,
   inherits font size from the parent recap clamp so it scales with viewport. */
.haxopoly-recap-card-link {
    appearance: none;
    background: none;
    border: none;
    padding: 0;
    margin: 0;
    font: inherit;
    line-height: inherit;            /* button defaults to its own line-height; inherit so the
                                        link sits on the recap's text baseline, not its own. */
    vertical-align: baseline;        /* button default is middle/baseline-ish per UA — explicit
                                        baseline keeps "Space-Time Fracture" level with the
                                        surrounding "drew ... , then rolled" prose. */
    color: #1F3A93;                  /* deep blue — readable on the cream recap background */
    font-weight: 700;
    text-decoration: underline;
    text-decoration-thickness: 1px;
    text-underline-offset: 2px;
    cursor: pointer;
}

.haxopoly-recap-card-link:hover,
.haxopoly-recap-card-link:focus-visible {
    color: #0B1437;
    text-decoration-thickness: 2px;
    outline: none;
}

/* Tucked-card pill (the "Tucked: Foo Bar" row in the centre hub) was a span before card click
   landed; promoting to a button means we reset the default button styling, keep the existing
   visual (navy fill, white border + text, small chip), and add a clear hover/focus cue. */
.haxopoly-centre-kept-pill {
    appearance: none;
    cursor: pointer;
    font-family: inherit;
    line-height: 1.2;
    transition: background-color 120ms ease, transform 120ms ease;
}

.haxopoly-centre-kept-pill:hover,
.haxopoly-centre-kept-pill:focus-visible {
    background: #2A4FB7;             /* one shade lighter than the resting navy */
    outline: none;
    transform: translateY(-1px);
}

/* Overlay card body — wider than the title-deed (more flavor text to breathe) but still
   capped on desktop so the line length stays readable. */
.haxopoly-card-overlay {
    width: min(420px, 92vw);
    padding: 22px 22px 18px;
    display: flex;
    flex-direction: column;
    gap: 16px;
}

.haxopoly-card-overlay-header {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding-bottom: 12px;
    border-bottom: 1px solid rgba(20, 24, 31, 0.12);
}

.haxopoly-card-overlay-deck {
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: #6B6B6B;
}

/* Deck tint — anomaly leans hazard (the F0962D used on the board's anomaly-icon orange);
   guild leans reward (the lime accent that runs through the rest of the cream surfaces). */
.haxopoly-card-overlay.is-anomaly .haxopoly-card-overlay-deck {
    color: #C25600;
}

.haxopoly-card-overlay.is-guild .haxopoly-card-overlay-deck {
    color: #2F7F3D;
}

.haxopoly-card-overlay-name {
    font-weight: 700;
    color: #14181F;
}

.haxopoly-card-overlay-body {
    display: flex;
    flex-direction: column;
    gap: 14px;
}

.haxopoly-card-overlay-flavor {
    margin: 0;
    font-style: italic;
    color: #4A4A4A;
    line-height: 1.4;
    font-size: 0.95rem;
}

.haxopoly-card-overlay-effect {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 10px 12px;
    background: #F4EFE2;             /* one shade darker than the cream surface — softly recessed */
    border-radius: 6px;
}

.haxopoly-card-overlay-effect-label {
    font-size: 0.65rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: #6B6B6B;
}

.haxopoly-card-overlay-effect-text {
    color: #14181F;
    font-weight: 600;
}

/* F.3.c card-resolution variant — the §10.1a card-acknowledgment popup. Reuses the deck-tinted
   card-overlay shell + the haxopoly-landing-actions action row. Parent is a flex container
   (justify-content: flex-end by default); override so the single contextual button stretches
   to span the row (no DECLINE partner to share with). */
.haxopoly-card-resolution-actions {
    justify-content: stretch;
}

.haxopoly-card-resolution-actions .haxopoly-landing-buy {
    flex: 1 1 auto;
}


/* ---------- Odyssey: stage toggle + off-stage token labels (design 10.2, 10.3) ---------- */

/* The Lower | Upper segmented control. VIEW-ONLY for the token: toggling never moves the player
   and never implies they are there. Property management stays fully interactive on whichever
   stage is shown, which is the point of the control — without it half a player's portfolio would
   be unreachable. The stage on screen always carries an explicit text label. */

.haxopoly-stage-toggle {
    display: flex;
    justify-content: center;
    gap: 6px;
    margin: 0 0 8px;
}

/* Un-picked is the board's own navy; picked is a lighter sky blue (operator 2026-09-24 - the old
   white-vs-navy pair read either way round). */
.haxopoly-stage-toggle-btn {
    font: inherit;
    font-size: 0.8125rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: #FCFCFD;
    background: #0B1437;
    border: 1px solid rgba(252, 252, 253, 0.28);
    border-radius: 999px;
    padding: 4px 14px;
    cursor: pointer;
    min-height: 32px;            /* stays tappable on mobile */
    transition: background 120ms ease, color 120ms ease;
}

.haxopoly-stage-toggle-btn:hover {
    background: #17224F;
}

.haxopoly-stage-toggle-btn.is-active {
    background: #8EC5FF;
    color: #0B1437;
    border-color: #8EC5FF;
}

/* "· You" / "· Them" / "· Both" marks which stage each token is on, so the control never relies
   on the active state alone to say where anyone is. This is the ONLY surface reporting token
   location: the centre-HUD .haxopoly-offstage label that used to carry the off-stage tokens was
   removed on 2026-09-07 (it duplicated the viewer's own position and overlapped Resign). */
.haxopoly-stage-toggle-here {
    opacity: 0.7;
    font-weight: 400;
    margin-left: 4px;
}

/* Event Horizon's corner icon in front of "Lower quadrant" / "Upper quadrant" (operator
   2026-09-17 — the bare words read bland; a 🌌 emoji and the discipline PNG were both tried and
   were unreadable at pill size). Sized to match the icon on its board cell on a phone (60% of the
   corner cell, ~32px) so the two read as the same mark. Decorative only: aria-hidden in markup, so
   the label still carries the name. */
.haxopoly-stage-toggle-glyph {
    display: inline-block;
    width: 32px;
    height: 32px;
    margin-right: 6px;
    vertical-align: middle;
    background: url('/icons/haxopoly/corner-eventhorizon.png') center / contain no-repeat;
}

/* Explanatory note under the count-scaled rent ladder — the Singularity quad's scaling is
   invisible from the numbers alone, so the card says what drives them. */
.haxopoly-titledeed-note {
    margin: 4px 0 8px;
    font-size: 0.6875rem;
    line-height: 1.35;
    opacity: 0.75;
}

/* ======================================================================================
   Turn replay (HaxopolyUpgrade/docs/presentation.md, phase 1: the player)
   The surface replays a turn's event log instead of jumping to the end state. Phase 1
   animates movement only; the dice scrim, beat cards and warp motion arrive in phases 2-3
   and their rules land here alongside these.
   ====================================================================================== */

/* A token lands with a pop. Each hop re-creates the token element inside the next cell, so
   the animation plays once per landing; a token that does not move keeps its element and
   stays still. No cross-cell glide is possible for an absolutely-positioned child (§5.2). */
@keyframes haxopoly-token-land {
    0%   { transform: scale(1.45) translateY(-18%); filter: brightness(1.25); }
    60%  { transform: scale(0.94) translateY(0);    filter: brightness(1); }
    100% { transform: scale(1)    translateY(0); }
}

.haxopoly-token.is-landing {
    animation: haxopoly-token-land 220ms cubic-bezier(0.2, 0.8, 0.3, 1) both;
}

/* While a replay runs the whole board is one tap target: tap to skip (P8). */
.haxopoly-board.is-replaying {
    cursor: pointer;
}

/* P1: the cold-open affordance, under the credits where Roll normally sits. */
.haxopoly-centre-replay {
    appearance: none;
    background: transparent;
    color: #A3E635;
    border: 1px solid rgba(163, 230, 53, 0.6);
    border-radius: 999px;
    padding: 0.8cqi 2.4cqi;
    margin-top: 1.2cqi;
    font-family: inherit;
    font-size: clamp(0.6rem, 2.4cqi, 0.8rem);
    font-weight: 700;
    letter-spacing: 0.02em;
    cursor: pointer;
    transition: background 120ms;
}

.haxopoly-centre-replay:hover {
    background: rgba(163, 230, 53, 0.12);
}

/* Bottom-left of the hub, the mirror of the Resign button's corner; only while replaying. */
.haxopoly-centre-skip-hint {
    position: absolute;
    bottom: 6px;
    left: 6px;
    font-size: clamp(0.55rem, 2cqi, 0.7rem);
    letter-spacing: 0.04em;
    color: #C7CFDC;
    opacity: 0.55;
    pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
    .haxopoly-token.is-landing {
        animation-duration: 1ms;
    }
}

/* ---------- Phase 2: the dice under the hub scrim (P9) ---------- */

/* The dice container IS the scrim: 82% navy plus a slight blur over the whole hub, the dice the
   only lit thing. It fades in on creation and out with .is-leaving; the hops start when it is
   gone. During a P6 hold it becomes the tap target (role=button) and never bubbles to the
   board's skip. */
.haxopoly-dice {
    position: absolute;
    inset: 0;
    z-index: 6;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 1.2cqi;
    background: rgba(11, 20, 55, 0.82);
    backdrop-filter: blur(1.5px);
    animation: haxopoly-scrim-in 160ms ease-out both;
}

.haxopoly-dice.is-leaving {
    animation: haxopoly-scrim-out 300ms ease-in both;
    pointer-events: none;
}

.haxopoly-dice.is-awaiting-tap {
    cursor: pointer;
}

@keyframes haxopoly-scrim-in  { from { opacity: 0; } to { opacity: 1; } }
@keyframes haxopoly-scrim-out { to { opacity: 0; } }

.haxopoly-dice-row {
    display: flex;
    gap: 3cqi;
}

/* An eight-sided die: octagon via clip-path, numeral face, the roller's profile colour (the same
   --hax-p variables the tokens and badges tint from). Ink numerals read on lime and coral. */
.haxopoly-die {
    position: relative;
    width: 11cqi;
    height: 11cqi;
    min-width: 38px;
    min-height: 38px;
    background: #FCFCFD;
    color: #14181F;
    clip-path: polygon(30% 0, 70% 0, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0 70%, 0 30%);
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 800;
    font-size: clamp(1rem, 5cqi, 1.9rem);
    font-variant-numeric: tabular-nums;
    box-shadow: 0 0 0 3px rgba(163, 230, 53, 0.35), 0 8px 24px rgba(0, 0, 0, 0.6);
}

.haxopoly-dice--p1 .haxopoly-die { background: var(--hax-p1, #A3E635); }
.haxopoly-dice--p2 .haxopoly-die { background: var(--hax-p2, #F4845F); }

/* The tumble: the die spins in while a stepped keyframe on a pseudo-element cycles the faces,
   so the numerals change without a single round trip to the server. The real face is hidden
   until the settle. */
@keyframes haxopoly-die-tumble {
    0%   { transform: translateY(-40%) rotate(0deg)   scale(0.7); }
    50%  { transform: translateY(6%)   rotate(200deg) scale(1.1); }
    100% { transform: translateY(0)    rotate(360deg) scale(1); }
}

@keyframes haxopoly-die-faces {
    0%    { content: "5"; }
    12.5% { content: "2"; }
    25%   { content: "7"; }
    37.5% { content: "3"; }
    50%   { content: "8"; }
    62.5% { content: "1"; }
    75%   { content: "6"; }
    87.5% { content: "4"; }
    100%  { content: "5"; }
}

.haxopoly-die.is-tumbling {
    animation: haxopoly-die-tumble 1100ms cubic-bezier(0.3, 0.7, 0.3, 1) both;
}

.haxopoly-die.is-tumbling .haxopoly-die-face {
    visibility: hidden;
}

.haxopoly-die.is-tumbling::after {
    content: "5";
    position: absolute;
    animation: haxopoly-die-faces 1100ms steps(1) both;
}

/* A trigger (doubles / an eight): a white ring and a pulse, not a fill, so it reads on any
   profile colour. */
@keyframes haxopoly-die-trigger {
    0%, 100% { transform: scale(1); }
    50%      { transform: scale(1.12); }
}

.haxopoly-die.is-trigger {
    box-shadow: 0 0 0 4px #FCFCFD, 0 0 0 7px rgba(252, 252, 253, 0.35), 0 8px 24px rgba(0, 0, 0, 0.6);
    animation: haxopoly-die-trigger 700ms ease-in-out 2;
}

.haxopoly-dice-caption {
    font-size: clamp(0.62rem, 2.6cqi, 0.9rem);
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: #A3E635;
    text-align: center;
    animation: haxopoly-caption-pop 200ms ease-out both;
}

@keyframes haxopoly-caption-pop {
    from { opacity: 0; transform: translateY(6px); }
    to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
    .haxopoly-die.is-tumbling,
    .haxopoly-die.is-tumbling::after,
    .haxopoly-die.is-trigger,
    .haxopoly-dice,
    .haxopoly-dice.is-leaving {
        animation-duration: 1ms;
    }
}

/* ---------- Phase 3: the beats - cards, warp, pulse, credit float, status flash ---------- */

/* The beat-card layer is absolutely positioned over the board, so the board is its context. */
.haxopoly-board {
    position: relative;
}

/* --- Warp (P7): the token shrinks and spins out where it was, then spins in where it lands.
       Cards, movement tiles, tunnel trips and the Event Horizon; never a dice move. --- */
@keyframes haxopoly-token-warp-out {
    to { transform: scale(0.2) rotate(180deg); opacity: 0; filter: blur(2px); }
}

@keyframes haxopoly-token-warp-in {
    from { transform: scale(2.2) rotate(-180deg); opacity: 0; filter: blur(2px); }
    to   { transform: scale(1) rotate(0);        opacity: 1; filter: blur(0); }
}

.haxopoly-token.is-warping-out { animation: haxopoly-token-warp-out 410ms ease-in both; }
.haxopoly-token.is-warping-in  { animation: haxopoly-token-warp-in 490ms cubic-bezier(0.2, 0.8, 0.3, 1) both; }

/* --- Cell pulse: an inset ring on the cell a payment (coral) or a gain / purchase (lime)
       just happened on. --- */
@keyframes haxopoly-cell-pulse {
    0%   { box-shadow: inset 0 0 0 0 rgba(244, 132, 95, 0); }
    30%  { box-shadow: inset 0 0 0 4px rgba(244, 132, 95, 0.85); }
    100% { box-shadow: inset 0 0 0 0 rgba(244, 132, 95, 0); }
}

@keyframes haxopoly-cell-pulse-gain {
    0%   { box-shadow: inset 0 0 0 0 rgba(163, 230, 53, 0); }
    30%  { box-shadow: inset 0 0 0 4px rgba(163, 230, 53, 0.9); }
    100% { box-shadow: inset 0 0 0 0 rgba(163, 230, 53, 0); }
}

.haxopoly-cell.is-pulsing {
    animation: haxopoly-cell-pulse 900ms ease-out both;
    z-index: 3;
}

.haxopoly-cell.is-pulsing--gain {
    animation-name: haxopoly-cell-pulse-gain;
}

/* --- The credits box: a floating +/- and a flash. The float was doubled 2026-09-12 and raised
       again 2026-09-16 (bigger, and held at full opacity for most of its life instead of fading
       from a fifth of the way in) - the operator wants the tally READ, not glimpsed. Its length
       must match ReplayTimings.CreditTickMs, which is when the delta is cleared. --- */
.haxopoly-centre-credits {
    position: relative;
}

@keyframes haxopoly-delta-float {
    0%   { opacity: 0; transform: translateY(6px); }
    12%  { opacity: 1; transform: translateY(0); }
    70%  { opacity: 1; transform: translateY(-4px); }
    100% { opacity: 0; transform: translateY(-18px); }
}

.haxopoly-credits-delta {
    position: absolute;
    right: 0;
    top: -1.35em;
    font-weight: 800;
    font-size: clamp(1.9rem, 8.5cqi, 2.75rem);
    font-variant-numeric: tabular-nums;
    text-shadow: 0 2px 6px rgba(0, 0, 0, 0.6);
    animation: haxopoly-delta-float 1600ms ease-out both;
    pointer-events: none;
}

.haxopoly-credits-delta--gain { color: #A3E635; }
.haxopoly-credits-delta--loss { color: #F4845F; }

@keyframes haxopoly-credits-flash {
    0%, 100% { background: rgba(255, 255, 255, 0.08); }
    40%      { background: rgba(163, 230, 53, 0.28); }
}

.haxopoly-centre-credits.is-flashing {
    animation: haxopoly-credits-flash 700ms ease-out both;
}

/* --- The status pin flashes on a transient status beat. --- */
@keyframes haxopoly-pin-flash {
    0%, 100% { transform: scale(1); }
    40%      { transform: scale(1.08); }
}

.haxopoly-centre-state.is-flashing {
    animation: haxopoly-pin-flash 700ms ease-out both;
}

/* --- Beat cards (P2, P3, P11): the auto-dismissing informational card. Over the BOARD, not
       the viewport, and never a gate: a tap on it is a board tap, which skips. --- */
.haxopoly-beat {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 6%;
    z-index: 20;
    background: rgba(11, 20, 55, 0.45);
}

.haxopoly-beat-card {
    background: #FCFCFD;
    color: #14181F;
    border-radius: 8px;
    padding: 14px 16px;
    max-width: 300px;
    width: 100%;
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.45);
    font-family: var(--mud-typography-default-family, sans-serif);
    animation: haxopoly-beat-in 220ms cubic-bezier(0.2, 0.8, 0.3, 1) both;
}

@keyframes haxopoly-beat-in  { from { opacity: 0; transform: translateY(14px) scale(0.96); } to { opacity: 1; transform: none; } }
@keyframes haxopoly-beat-out { to { opacity: 0; transform: translateY(-10px) scale(0.98); } }

.haxopoly-beat.is-leaving .haxopoly-beat-card {
    animation: haxopoly-beat-out 220ms ease-in both;
}

.haxopoly-beat-kicker {
    margin: 0 0 4px;
    font-size: 0.62rem;
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    opacity: 0.6;
}

.haxopoly-beat-title {
    margin: 0 0 4px;
    font-size: 1rem;
    font-weight: 800;
    line-height: 1.2;
}

.haxopoly-beat-body {
    margin: 0;
    font-size: 0.82rem;
    line-height: 1.4;
}

.haxopoly-beat-effect {
    margin: 6px 0 0;
    font-size: 0.78rem;
    font-weight: 600;
    line-height: 1.35;
}

.haxopoly-beat-amount {
    margin: 6px 0 0;
    font-size: 1.35rem;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
}

.haxopoly-beat-amount--loss { color: #D8352A; }
.haxopoly-beat-amount--gain { color: #2F9E52; }

.haxopoly-beat-card--payment  { border-top: 6px solid #F4845F; }
.haxopoly-beat-card--windfall { border-top: 6px solid #A3E635; }
.haxopoly-beat-card--anomaly  { border-top: 6px solid #B5179E; }
.haxopoly-beat-card--guild    { border-top: 6px solid #0E9594; }
.haxopoly-beat-card--detained { border-top: 6px solid #14181F; background: #FAF6EC; }
.haxopoly-beat-card--final    { border-top: 6px solid #0B1437; background: #FAF6EC; }

/* A drawn card flips in around its vertical axis. */
@keyframes haxopoly-card-flip {
    from { transform: rotateY(90deg); opacity: 0.4; }
    to   { transform: rotateY(0);     opacity: 1; }
}

.haxopoly-beat-card--anomaly,
.haxopoly-beat-card--guild {
    animation: haxopoly-card-flip 320ms cubic-bezier(0.2, 0.8, 0.3, 1) both;
}

/* The hold made visible: a bar that empties over the card's duration (set inline per beat). */
.haxopoly-beat-progress {
    height: 3px;
    margin: 10px 0 0;
    background: rgba(20, 24, 31, 0.12);
    border-radius: 2px;
    overflow: hidden;
}

.haxopoly-beat-progress > i {
    display: block;
    height: 100%;
    width: 100%;
    background: #14181F;
    transform-origin: left;
    animation: haxopoly-beat-progress linear both;
}

@keyframes haxopoly-beat-progress {
    from { transform: scaleX(1); }
    to   { transform: scaleX(0); }
}

@media (prefers-reduced-motion: reduce) {
    .haxopoly-token.is-warping-out,
    .haxopoly-token.is-warping-in,
    .haxopoly-cell.is-pulsing,
    .haxopoly-beat-card,
    .haxopoly-beat.is-leaving .haxopoly-beat-card,
    .haxopoly-centre-state.is-flashing {
        animation-duration: 1ms;
    }
}

/* ---------- Phase 4: the mute toggle (P4) ---------- */

/* Bottom-right of the hub, the mirror of the skip hint; present whenever the match is live so
   it is also the autoplay-policy unlock (tapping it is a gesture). Never a board skip. */
.haxopoly-centre-mute {
    position: absolute;
    bottom: 6px;
    right: 6px;
    z-index: 5;
    appearance: none;
    background: transparent;
    border: 1px solid rgba(255, 255, 255, 0.25);
    color: #FCFCFD;
    border-radius: 999px;
    width: 26px;
    height: 26px;
    padding: 0;
    font-size: 13px;
    line-height: 1;
    cursor: pointer;
    transition: opacity 120ms, border-color 120ms;
}

.haxopoly-centre-mute:hover {
    border-color: rgba(255, 255, 255, 0.6);
}

.haxopoly-centre-mute.is-muted {
    opacity: 0.5;
}

/* ===================================================================
   CREDIT LEDGER (2026-09-22)
   The credits chip opens a statement of every event that moved THIS
   player's credits. Money colours are the module's existing ones
   (.haxopoly-beat-amount--gain / --loss) so the ledger introduces no
   second green or red, and every amount carries an explicit sign -
   colour is never the only thing saying which way the money went.
   =================================================================== */

/* --- the chip, now a button ---------------------------------------- */
.haxopoly-centre-credits--btn {
    appearance: none;
    font-family: inherit;
    cursor: pointer;
    transition: background 120ms ease, border-color 120ms ease, transform 120ms ease;
}

.haxopoly-centre-credits--btn:hover {
    background: rgba(255, 255, 255, 0.15);
    border-color: rgba(163, 230, 53, 0.55);
}

.haxopoly-centre-credits--btn:active {
    transform: translateY(1px);
}

.haxopoly-centre-credits--btn:focus-visible {
    outline: 2px solid #A3E635;
    outline-offset: 2px;
}

/* Drawn in CSS, not typed as a character. The obvious glyph for this is U+25BE, which Roboto -
   the MudBlazor default family this surface inherits - does not carry, so it rendered as a
   missing-glyph box on the board (operator caught it 2026-09-22). Borders have no such
   dependency and scale with the chip on every platform. */
.haxopoly-centre-credits-caret {
    width: 0;
    height: 0;
    margin-left: 1.4cqi;
    align-self: center;
    border-left: clamp(0.17rem, 0.7cqi, 0.3rem) solid transparent;
    border-right: clamp(0.17rem, 0.7cqi, 0.3rem) solid transparent;
    border-top: clamp(0.19rem, 0.8cqi, 0.34rem) solid #A3E635;
    opacity: 0.85;
}

/* --- the card ------------------------------------------------------ */
.haxopoly-overlay-card.haxopoly-ledger {
    /* Overrides the base card's 20px padding: the header band runs edge to edge and the row
       list owns its own scroll gutter.
       The 10px at the BOTTOM is outside the scroller on purpose. Without it the list ended flush
       against the card's edge, so a part-scrolled row sat exactly on the border with no card
       beneath it and read as spilling out of the card (operator 2026-09-22). The gap gives the
       list somewhere to visibly stop. */
    padding: 0 0 10px;
    gap: 0;
    overflow: hidden;
    max-width: 360px;
}

.haxopoly-ledger-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 10px;
    /* Right padding clears the absolutely-positioned X. That button sits 8px in and is about
       34px wide (1.5rem glyph + 10px side padding), so it occupies the first ~42px; 56px clears
       it and leaves a real gap rather than tucking the balance under the close control
       (operator 2026-09-22). */
    padding: 12px 56px 10px 14px;
    background: #FAF6EC;
    border-bottom: 1px solid rgba(20, 24, 31, 0.12);
}

.haxopoly-ledger-title {
    margin: 0;
    font-size: 1rem;
    font-weight: 800;
    line-height: 1.15;
}

.haxopoly-ledger-onhand {
    text-align: right;
    line-height: 1.1;
    flex: 0 0 auto;
}

.haxopoly-ledger-onhand-label {
    display: block;
    margin-bottom: 1px;
    font-size: 0.58rem;
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    opacity: 0.6;
}

.haxopoly-ledger-onhand-value {
    /* Ink, not green (operator 2026-09-22): on-hand is a FACT, not a gain. Green would put it
       in the same family as the + rows and quietly imply "up". */
    font-size: 1.3rem;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    color: #14181F;
}

.haxopoly-ledger-section {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    /* Right padding = the rows' 14px + an 8px scrollbar gutter, so the TURN caption sits over
       the number column rather than over the scrollbar. */
    padding: 10px 22px 4px 14px;
    font-size: 0.62rem;
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    opacity: 0.55;
}

.haxopoly-ledger-section-turn {
    letter-spacing: 0.08em;
}

.haxopoly-ledger-rows {
    /* Raised from a flat 214px (operator 2026-09-22: the card sat noticeably shorter than a
       title-deed card, which is the size that feels right on this surface). Viewport-relative so
       a phone in landscape, or a short window, still gets a card that fits rather than one that
       runs off the screen - the px ceiling keeps it from becoming a wall of rows on a tall
       monitor. */
    max-height: min(44vh, 300px);
    overflow-y: auto;
    /* 6px of air under the caption - the list should not start on top of its own heading. */
    padding: 6px 14px 12px;
    scrollbar-width: thin;
    scrollbar-color: rgba(20, 24, 31, 0.28) transparent;
}

.haxopoly-ledger-rows::-webkit-scrollbar { width: 8px; }
.haxopoly-ledger-rows::-webkit-scrollbar-thumb { background: rgba(20, 24, 31, 0.25); border-radius: 4px; }
.haxopoly-ledger-rows::-webkit-scrollbar-track { background: transparent; }

/* --- a row --------------------------------------------------------- */
.haxopoly-ledger-row {
    display: flex;
    align-items: baseline;
    gap: 6px;
    padding: 3px 0;
    font-size: 0.82rem;
}

.haxopoly-ledger-row-label {
    flex: 0 1 auto;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Money that moved during the OPPONENT's turn says so, dimmed - it is still the viewer's own
   money, but a row they did not cause needs its origin legible. */
.haxopoly-ledger-row-from {
    opacity: 0.6;
    font-weight: 400;
}

.haxopoly-ledger-row-dots {
    flex: 1 1 auto;
    min-width: 12px;
    border-bottom: 1px dotted rgba(20, 24, 31, 0.34);
    transform: translateY(-3px);
}

.haxopoly-ledger-row-amt {
    flex: 0 0 auto;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.haxopoly-ledger-row-amt--gain { color: #2F9E52; }
.haxopoly-ledger-row-amt--loss { color: #D8352A; }

.haxopoly-ledger-row-turn {
    flex: 0 0 24px;
    text-align: right;
    font-size: 0.64rem;
    font-weight: 600;
    opacity: 0.42;
    font-variant-numeric: tabular-nums;
}

/* The opening balance closes the list - ruled off, because it is not a transaction. */
.haxopoly-ledger-row--opening {
    margin-top: 4px;
    padding-top: 6px;
    border-top: 1px solid rgba(20, 24, 31, 0.14);
}

.haxopoly-ledger-empty {
    padding: 2px 0 6px;
    font-size: 0.82rem;
    opacity: 0.7;
}

/* ===================================================================
   PROPERTY LEDGER (2026-09-23)
   The player's piece opens the whole board: what each space is right
   now, yours / theirs / open, grouped by stage and sector with the
   Wormholes gathered at the end. Sibling of the Credit Ledger and
   deliberately the same card size, type scale and scroll behaviour -
   holdings on the left of the hub, money on the right.
   =================================================================== */

/* --- the piece, now a button --------------------------------------- */
.haxopoly-centre-piece--btn {
    appearance: none;
    background: transparent;
    border: 0;
    padding: 2px;
    border-radius: 8px;
    font-family: inherit;
    color: inherit;
    cursor: pointer;
    transition: background 120ms ease, transform 120ms ease;
}

.haxopoly-centre-piece--btn:hover { background: rgba(255, 255, 255, 0.10); }
.haxopoly-centre-piece--btn:active { transform: translateY(1px); }

.haxopoly-centre-piece--btn:focus-visible {
    outline: 2px solid #A3E635;
    outline-offset: 2px;
}

/* --- the card ------------------------------------------------------ */
.haxopoly-overlay-card.haxopoly-pledger {
    /* Matches the Credit Ledger exactly, including the 10px below the scroller that gives the
       list somewhere to visibly stop. */
    padding: 0 0 10px;
    gap: 0;
    overflow: hidden;
    max-width: 360px;
}

.haxopoly-pledger-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 10px;
    padding: 12px 56px 10px 14px;   /* 56px clears the absolute X, as on the Credit Ledger */
    background: #FAF6EC;
    border-bottom: 1px solid rgba(20, 24, 31, 0.12);
}

.haxopoly-pledger-title {
    margin: 0;
    font-size: 1rem;
    font-weight: 800;
    line-height: 1.15;
}

.haxopoly-pledger-count {
    /* Centred, not right-aligned: the block is only as wide as "You hold", and a right-aligned
       single digit hung off the end of its own label (operator 2026-09-23). */
    text-align: center;
    line-height: 1.1;
    flex: 0 0 auto;
}

.haxopoly-pledger-count-label {
    display: block;
    margin-bottom: 1px;
    font-size: 0.58rem;
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    opacity: 0.6;
}

.haxopoly-pledger-count-value {
    font-size: 1.3rem;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    color: #14181F;
}

/* The Infrastructure caption is centred OVER its column, which is why the column width is
   repeated on both the caption and the row rather than being invented twice. */
.haxopoly-pledger-caption {
    display: flex;
    align-items: baseline;
    padding: 10px 22px 4px 14px;
    font-size: 0.58rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    opacity: 0.55;
}

.haxopoly-pledger-caption-board { flex: 1 1 auto; }
.haxopoly-pledger-caption-infra { flex: 0 0 104px; text-align: center; }

.haxopoly-pledger-rows {
    max-height: min(46vh, 330px);
    overflow-y: auto;
    padding: 2px 0 0;
    scrollbar-width: thin;
    scrollbar-color: rgba(20, 24, 31, 0.28) transparent;
}

.haxopoly-pledger-rows::-webkit-scrollbar { width: 8px; }
.haxopoly-pledger-rows::-webkit-scrollbar-thumb { background: rgba(20, 24, 31, 0.25); border-radius: 4px; }
.haxopoly-pledger-rows::-webkit-scrollbar-track { background: transparent; }

.haxopoly-pledger-group {
    padding: 9px 14px 3px;
    font-size: 0.6rem;
    font-weight: 800;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    opacity: 0.7;
}

/* --- a row --------------------------------------------------------- */
.haxopoly-pledger-row {
    display: flex;
    align-items: center;
    gap: 8px;
    /* 14px on the left so the sector swatch is inset rather than welded to the card edge
       (operator 2026-09-23, 8px; raised to 14 on 2026-09-26). It is the same 14px a set box
       spends on margin + border + inner air, so swatches line up whether or not a row is inside
       a completed set.
       6px top and bottom, raised from 3px the same day: the rows wanted air between them, and
       the ask was explicitly for height rather than a larger face. */
    padding: 6px 22px 6px 14px;
    font-size: 0.82rem;
}

.haxopoly-pledger-swatch {
    flex: 0 0 6px;
    align-self: stretch;
    min-height: 22px;
    border-radius: 0 2px 2px 0;
}

/* A Wormhole belongs to no sector, so it takes the surface's transit violet rather than
   borrowing a sector colour and implying a set it is not part of. */
.haxopoly-pledger-swatch--transit { background: #7C3AED; }

.haxopoly-pledger-badge-none {
    flex: 0 0 18px;
    height: 18px;
    margin-left: 6px;
    border-radius: 50%;
    border: 1px dashed rgba(20, 24, 31, 0.22);
}

/* The owner badge is the board tile's own renderer; it is absolutely positioned on a tile, so
   the ledger puts it back into normal flow. */
.haxopoly-pledger-row .haxopoly-owner-badge {
    position: static;
    flex: 0 0 18px;
    width: 18px;
    height: 18px;
    margin-left: 6px;
    inset: auto;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
}

/* In the ledger the owner wears their PIECE look - solid profile colour, white glyph - rather
   than the board's inverted cream badge (operator 2026-09-26: the glyph "has more color to
   identify the player"). The board inverts on purpose so an owned tile never reads as a token
   standing on it; the ledger has no tokens, so that risk does not exist here. Same renderer as
   the tile, restyled - the icon still cannot drift from the board's. */
.haxopoly-pledger-row .haxopoly-owner-badge--p1 { background: var(--hax-p1, #A3E635); }
.haxopoly-pledger-row .haxopoly-owner-badge--p2 { background: var(--hax-p2, #F4845F); }

.haxopoly-pledger-row .haxopoly-owner-badge-img {
    width: 78%;
    height: 78%;
    filter: brightness(0) invert(1);
}

.haxopoly-pledger-name {
    flex: 0 1 auto;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.haxopoly-pledger-dots {
    flex: 1 1 auto;
    min-width: 10px;
    border-bottom: 1px dotted rgba(20, 24, 31, 0.3);
    transform: translateY(-3px);
}

.haxopoly-pledger-infra {
    flex: 0 0 104px;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 3px;
    padding-right: 2px;
}

/* The building markers are the tiles' renderer too. On a tile they sit inside a thin sector
   band; here they need to read at row scale against a light background. */
.haxopoly-pledger-infra .haxopoly-building-markers {
    display: flex;
    align-items: center;
    gap: 3px;
    position: static;
}

/* Same shapes as the board - a Colony is a square, a Starport a circle (operator 2026-10-03; this
   had them as dots and diamonds). Only the size and ink fill change, for row scale. */
.haxopoly-pledger-infra .haxopoly-building-marker--colony {
    width: 9px;
    height: 9px;
    border-radius: 0;
    background: #14181F;
}

.haxopoly-pledger-infra .haxopoly-building-marker--starport {
    width: 11px;
    height: 11px;
    border-radius: 50%;
    background: #14181F;
}

/* --- unowned by anybody -------------------------------------------- */
.haxopoly-pledger-row.is-open { background: rgba(20, 24, 31, 0.055); }
/* The swatch is NOT faded: a faded pink read as a different sector from its full-strength twin
   (operator 2026-09-24, Europa Habitat next to Titan Station). The name carries "unowned". */
.haxopoly-pledger-row.is-open .haxopoly-pledger-name { opacity: 0.5; }
.haxopoly-pledger-row.is-open .haxopoly-pledger-dots { border-bottom-color: rgba(20, 24, 31, 0.16); }

/* --- mortgaged ------------------------------------------------------ */
.haxopoly-pledger-tag {
    font-size: 0.56rem;
    font-weight: 800;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    padding: 1px 5px;
    border-radius: 3px;
    white-space: nowrap;
}

.haxopoly-pledger-row.is-mortgaged { background: rgba(216, 53, 42, 0.10); }

.haxopoly-pledger-row.is-mortgaged .haxopoly-pledger-tag {
    background: rgba(216, 53, 42, 0.16);
    color: #A32A22;
}

/* --- a COMPLETED set ------------------------------------------------
   A box around the whole group, in the OWNER's profile colour - the viewer's own set in
   --hax-yours, the opponent's in --hax-opp (operator 2026-09-26: "stick with using player color
   for owned sets", so every colour on the card is identifiable from either seat; this retired
   the gold-for-you / slate-for-them pair of 09-24). The fallbacks are that old pair, for a
   context with no profile colours. The head text stays dark slate - most profile colours fail
   contrast as text on the cream card - and the head names the owner ("Complete" / "X's set"),
   so colour never carries the meaning alone.
   Geometry: margin (6) + border (2) + the rows' inner padding (6) = the outer rows' 14px, so a
   swatch inside a box and one outside it sit on the same vertical line; the right side pays back
   margin + border the same way for the Infrastructure column. A box that shifted its own
   contents would be worse than no box. */
.haxopoly-pledger-set {
    --hax-set: var(--hax-yours, #D4A017);
    margin: 5px 6px 7px 6px;
    border: 2px solid var(--hax-set);
    border-radius: 5px;
    background: rgba(20, 24, 31, 0.04);
    background: color-mix(in srgb, var(--hax-set) 8%, transparent);
    overflow: hidden;
}

.haxopoly-pledger-set.is-theirs { --hax-set: var(--hax-opp, #8A93A3); }

/* 6px of air between the border and the sector swatch (operator 2026-09-26 - the swatch sat
   welded to the border on a real phone). Right: the outer row's 22px minus margin + border. */
.haxopoly-pledger-set .haxopoly-pledger-row {
    padding-left: 6px;
    padding-right: 14px;
}

.haxopoly-pledger-set-head {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 4px 14px 3px 12px;
    font-size: 0.58rem;
    font-weight: 800;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: #4A5361;
}

.haxopoly-pledger-set-rule {
    flex: 1 1 auto;
    border-bottom: 1px solid rgba(74, 83, 97, 0.35);
    border-bottom-color: color-mix(in srgb, var(--hax-set) 55%, transparent);
    transform: translateY(-1px);
}

/* Opponent names are unbounded; the label gives way before the sector name does. */
.haxopoly-pledger-set-owner {
    flex: 0 1 auto;
    min-width: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* A plain (incomplete) sector. The gap between sectors is what tells a player which plots
   belong together - without it the list read as one run and a sector looked split. */
.haxopoly-pledger-sector + .haxopoly-pledger-sector { margin-top: 8px; }

/* --- tunnels -------------------------------------------------------- */
.haxopoly-pledger-tunnel {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 2px 22px 6px 36px;   /* 30 + the 6px rows gained on 2026-09-26 */
    font-size: 0.58rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

.haxopoly-pledger-tunnel-line {
    flex: 1 1 auto;
    border-bottom: 2px solid currentColor;
    transform: translateY(-2px);
    opacity: 0.45;
}

.haxopoly-pledger-tunnel.is-open { color: #2F9E52; }
.haxopoly-pledger-tunnel.is-closed { color: #5A6472; }
.haxopoly-pledger-tunnel.is-closed .haxopoly-pledger-tunnel-line { border-bottom-style: dashed; }

/* The "also taken back" line after an undo on the title deed (operator 2026-10-03) sits in the
   wrapping action row - give it the full width so it reads as a line, not a button-sized cell. */
.haxopoly-titledeed-actions > .haxopoly-titledeed-note {
    flex-basis: 100%;
    margin: 2px 0 0;
}
