/* Game Indicators for Health, Mana and Deck */

/* Health, Mana and Status Indicators */
.health-indicator,
.mana-indicator {
    width: 9.4vmin;
    height: 9.4vmin;
    border-radius: 50%;
    display: flex;
    justify-content: center;
    align-items: center;
    font-weight: bold;
    font-size: 4.6vmin;
    color: #fff;
    text-shadow: 0 0 5px rgba(0, 0, 0, 0.8);
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
    z-index: 20;
    position: relative;
    transition: background-color 0.3s ease, box-shadow 0.3s ease;
}

.health-indicator {
    background-color: rgba(180, 0, 0, 0.7);
    color: #fff;
}

.mana-indicator {
    background-color: rgba(0, 60, 180, 0.7);
    color: #eedbf1;
}

/* Portrait positioning */
/* Stylized summoner portrait. The asymmetric wooden silhouette is part of the
   original battle identity; the art itself remains on a separate stable paint
   layer so HUD animation never resamples the live raster. */
.player-portrait {
    /* THE DESIGNED WIDTH, and then what the board row can actually pay for.

       31vmin is the design and stays the design on every viewport with room for
       it. `--summoner-rail-fit` (game-board.css) is what the centred slot row
       leaves beside itself once the edge inset, the protruding disc and a
       clearance are paid — on a widescreen viewport it is far larger than the
       design and this `min()` returns 31vmin unchanged; as the aspect ratio
       squares up towards a tablet's it takes over and the rail stops exactly
       short of the row instead of walking onto it.

       The 96px floor is a degenerate-box guard, not a layout. Reaching it means
       the viewport is squarer than any shipping tablet, and a portrait is a
       drop target that must stay hittable even then. */
    --summoner-portrait-design: 31vmin;
    --summoner-portrait-width: max(
        96px,
        min(var(--summoner-portrait-design), var(--summoner-rail-fit))
    );
    /* Frame, radius and discs are all fractions of the width, so a capped
       portrait SHRINKS instead of distorting: the wooden silhouette is part of
       the battle's identity and must read the same at every size. Each fraction
       is the value it has always had at the full 31vmin, so none of this moves
       until the cap engages. */
    --portrait-frame-width: calc(var(--summoner-portrait-width) * 1.11 / 31);
    --portrait-radius:
        calc(var(--summoner-portrait-width) * 8.76 / 31)
        calc(var(--summoner-portrait-width) * 8.76 / 31)
        calc(var(--summoner-portrait-width) * 1.75 / 31)
        calc(var(--summoner-portrait-width) * 1.75 / 31);
    /* THE DISCS ARE SIZED HERE, not on `body` with the tier parameters, because
       they are a fraction of the width declared just above and a custom
       property's `var()`s are substituted on the element that DECLARES them. */
    --summoner-disc-size: calc(
        var(--summoner-portrait-width) * var(--summoner-disc-ratio)
    );
    --summoner-nameplate-height: clamp(28px, 3.2vmin, 38px);
    --summoner-nameplate-gap: clamp(3px, 0.55vmin, 8px);
    /* Resource discs sit half outside the lower art corners. The name crest
       straddles the portrait's bottom edge between those sockets, so wherever
       the discs already protrude the identity row costs zero extra space;
       only disc-less compact layouts reserve the crest's lower half.

       This is the BLOCK reach — how far a disc hangs below the frame — and it
       is not the same number as the inline reach the rail budget reserves: the
       tablet tier pulls its discs 14px in horizontally and leaves them at 5px
       vertically. */
    --summoner-resource-protrusion: calc(
        var(--summoner-disc-size) / 2 - var(--summoner-disc-inset-block)
    );
    --summoner-nameplate-reserve: max(
        var(--summoner-resource-protrusion),
        calc(var(--summoner-nameplate-height) / 2 + var(--summoner-nameplate-gap))
    );
    position: absolute;
    width: var(--summoner-portrait-width);
    height: calc(var(--summoner-portrait-width) * 23 / 31);
    border-radius: var(--portrait-radius);
    /* The wrapper owns interaction, framing, and indicators. Portrait pixels
       live on the isolated child below so animated UI never invalidates them. */
    background-image: none !important;
    background-color: #090612;
    filter: none;
    border: 0;
    z-index: 10;
    /* Enhanced outer and inner shadows for stronger 3D effect */
    box-shadow:
        0 0.37vmin 0.74vmin rgba(0, 0, 0, 0.55),
        0 0 0 1px rgba(234, 199, 97, 0.24);

    overflow: hidden;
    /* Keep the image within the rounded corners */
    transition: box-shadow 0.3s ease, border-color 0.3s ease;
    /* Ensure visibility */
    display: block !important;
    opacity: 1 !important;
    visibility: visible !important;
    margin: 0 !important;
    isolation: isolate;
}

.portrait-art {
    position: absolute;
    inset: 0;
    z-index: 0;
    display: block;
    overflow: hidden;
    border-radius: var(--portrait-radius);
    background-size: cover;
    background-position: center top;
    background-color: #090612;
    background-repeat: no-repeat;
    image-rendering: auto;
    contain: paint;
    transform: none;
    filter: none;
    pointer-events: none;
}

.player-portrait:is(.portrait-art-retrying, .portrait-art-unavailable) .portrait-art {
    background-image: none !important;
    background:
        radial-gradient(circle at 50% 38%, rgba(116, 82, 158, 0.16), transparent 62%),
        #090612;
}

.portrait-source-probe {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
    pointer-events: none;
    overflow: hidden;
}

/* The authoritative localized name lives on a separate HUD plaque beneath the
   resource row. This preserves every portrait pixel and gives long names a
   deliberate identity strip instead of wedging text between circular sockets. */
.summoner-nameplate {
    position: absolute;
    left: 50%;
    top: calc(100% - var(--summoner-nameplate-height) / 2);
    bottom: auto;
    width: clamp(120px, 58%, 200px);
    min-height: var(--summoner-nameplate-height);
    height: var(--summoner-nameplate-height);
    box-sizing: border-box;
    transform: translateX(-50%);
    z-index: 24;
    display: flex;
    align-items: center;
    justify-content: center;
    /* Real vertical insets keep one- and two-line names optically centered
       instead of letting the fitted block sit flush against the crest top. */
    padding: clamp(3px, 0.5vmin, 7px) clamp(3px, 0.8vmin, 12px);
    overflow: hidden;
    border: 1px solid rgba(234, 199, 97, 0.85);
    border-radius: clamp(7px, 0.9vmin, 13px);
    background:
        linear-gradient(90deg, rgba(30, 13, 45, 0.97), rgba(13, 8, 24, 0.98) 22%, rgba(13, 8, 24, 0.98) 78%, rgba(30, 13, 45, 0.97));
    box-shadow:
        inset 0 1px 0 rgba(255, 246, 205, 0.18),
        inset 0 -1px 0 rgba(72, 36, 91, 0.72),
        0 clamp(2px, 0.35vmin, 5px) clamp(7px, 1vmin, 15px) rgba(0, 0, 0, 0.68),
        0 0 clamp(7px, 0.9vmin, 14px) rgba(185, 154, 220, 0.2);
    color: #fff1be;
    font-family: "Cinzel", Georgia, serif;
    font-size: clamp(10px, 1.45vmin, 20px);
    font-weight: 700;
    line-height: 1.05;
    letter-spacing: 0.01em;
    text-transform: uppercase;
    text-align: center;
    /* The crest sits in the narrow corridor between the resource discs, so
       long names balance onto two lines instead of shrinking past legibility. */
    white-space: normal;
    text-wrap: balance;
    text-shadow:
        0 1px 2px rgba(0, 0, 0, 0.96),
        0 0 clamp(5px, 0.7vmin, 10px) rgba(234, 199, 97, 0.28);
    pointer-events: none;
}

.summoner-nameplate[hidden] {
    display: none;
}

/* Generated names are fitted down to a readable floor. A pathological name
   that still cannot fit gets an explicit ellipsis (with the full text retained
   in title/accessibility text) instead of leaking or clipping arbitrary rows. */
.summoner-nameplate[data-fit="truncated"] {
    white-space: nowrap;
    text-overflow: ellipsis;
}

/* Tint overlay for damage/heal on portraits */
/* Removed portrait tint overlays (red/green flashes) */



/* Enlarged invisible hitbox for easier drag-and-drop */
.portrait-hitbox {
    position: fixed;
    background: transparent;
    pointer-events: auto;
    /* Must capture drag events */
}

/* Purple haze effect on hover */
.player-portrait::after {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: radial-gradient(circle, rgba(238, 219, 241, 0.2) 0%, rgba(238, 219, 241, 0) 70%);
    opacity: 0;
    transition: opacity 0.3s ease;
    pointer-events: none;
    border-radius: var(--portrait-radius);
    z-index: 3;
    /* Above the inner shadow */
}

/* Compatibility hover can stick after a touch. Keep the portrait haze on
   hardware that actually has a fine hover pointer so tapping its mana child
   cannot leave the surrounding portrait looking selected. */
@media (any-hover: hover) and (any-pointer: fine) {
    .player-portrait:not(.portrait--touch-hover-suppressed):hover {
        box-shadow:
            0 0.56vmin 1.11vmin rgba(238, 219, 241, 0.6),
            0 0 0 1px rgba(234, 199, 97, 0.35);
    }

    .player-portrait:not(.portrait--touch-hover-suppressed):hover::after {
        opacity: 1;
    }
}

/* Enhanced inner frame shadow overlay for stronger 3D effect */
.player-portrait::before {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: var(--portrait-radius);
    box-shadow:
        inset 0 0 0 var(--portrait-frame-width) #6f3516,
        inset 0 0 0 calc(var(--portrait-frame-width) + 1px) rgba(234, 199, 97, 0.5),
        inset 0.28vmin 0.28vmin 0.56vmin rgba(255, 255, 255, 0.22),
        inset -0.37vmin -0.37vmin 0.74vmin rgba(0, 0, 0, 0.62);
    pointer-events: none;
    z-index: 2;
}

#player-portrait {
    /* Bottom right corner positioning */
    position: fixed;
    bottom: calc(
        var(--summoner-nameplate-reserve)
        + max(6px, env(safe-area-inset-bottom, 0px))
    );
    /* Spelled out rather than taken from `--summoner-rail-inset`, which holds
       the identical expression: `phoneCommandLaneSafeArea.test.js` derives the
       phone column's anchor from the FIRST `right: var(...)` among the portrait
       rules, so a token here silently becomes that oracle's answer and the
       phone-lane contract stops being checked. The two are kept in agreement by
       an assertion in tabletBandParity.test.js instead. */
    right: max(calc(4.7vmin + 6px), env(safe-area-inset-right, 0px));
    /* Ensure visibility */
    opacity: 1;
    visibility: visible;
    /* Important: Do NOT clip indicators */
    overflow: visible;
    z-index: 15;
    /* Higher z-index to ensure visibility */
}

#opponent-portrait {
    /* Top right corner positioning */
    position: fixed;
    top: max(calc(4.7vmin + 6px), env(safe-area-inset-top, 0px));
    right: max(calc(4.7vmin + 6px), env(safe-area-inset-right, 0px));
    /* Ensure visibility */
    opacity: 1;
    visibility: visible;
    /* Important: Do NOT clip indicators */
    overflow: visible;
    z-index: 15;
    /* Higher z-index to ensure visibility */
}

/* Player area positioning */
.player-area {
    position: relative;
    z-index: 10;
    overflow: visible !important;
    /* Ensure indicators can stick out */
}

/* Portrait container positioning - corners of the screen */
.portrait-container {
    position: fixed;
    z-index: 15;
    display: flex;
    justify-content: center;
    align-items: center;
}

#player-portrait-container {
    bottom: 20px;
    right: 20px;
}

#opponent-portrait-container {
    top: 20px;
    right: 20px;
}

/* Health and mana indicators base styling */
.health-indicator,
.mana-indicator {
    /* A FRACTION OF THE PORTRAIT, not of the viewport.

       `31vmin * 9.4/31` is exactly the 9.4vmin these discs have always been, so
       nothing moves on a viewport where the portrait keeps its designed width.
       It matters when the rail cap shrinks that portrait: two vmin-sized discs
       on a narrowed frame would meet in the middle of it and bury the summoner.
       The fallback keeps any disc rendered outside a portrait at its old size. */
    width: var(--summoner-disc-size, 9.4vmin) !important;
    height: var(--summoner-disc-size, 9.4vmin) !important;
    min-width: var(--summoner-disc-size, 9.4vmin) !important;
    min-height: var(--summoner-disc-size, 9.4vmin) !important;
    border-radius: 50%;
    border: none;
    /* Removed purple border */
    box-shadow: 0 0 1.11vmin rgba(0, 0, 0, 0.5), inset 0 0 0.56vmin rgba(255, 255, 255, 0.2);
    z-index: 20;
    /* Higher than portraits to be visible when sticking out */
    /* Ensure indicators are styled correctly */
    position: absolute !important;
    margin: 0 !important;
    display: flex !important;
    justify-content: center !important;
    align-items: center !important;
    font-weight: bold !important;
    color: white !important;
    text-shadow: 0 0 0.28vmin black, 0 0 0.46vmin rgba(0, 0, 0, 0.5) !important;
    /* The readout scales with its own disc, by the same fraction it always had
       of a full-size one (4.6vmin of a 9.4vmin disc). */
    font-size: calc(var(--summoner-disc-size, 9.4vmin) * 4.6 / 9.4) !important;
}

/* Health indicator specific styling */
.health-indicator {
    background-color: rgba(180, 0, 0, 0.7) !important;
    /* Red background for health */
}

/* Mana indicator specific styling */
.mana-indicator {
    background-color: rgba(0, 60, 180, 0.7) !important;
    transition: none;
    /* Blue background for mana */
}

/* Mana is a state display, not an event notification. It never pulses itself
   when a snapshot arrives or a value changes. The richer blue belongs only to
   deliberate inspection: real hover on desktop, or the duration of a finger
   press on touch hardware (never sticky compatibility hover). */
@media (any-hover: hover) and (any-pointer: fine) {
    .mana-indicator.mana-indicator--fine-hovered:not(.mana-indicator--touch-hover-suppressed) {
        background-color: rgba(18, 92, 224, 0.94) !important;
        box-shadow:
            0 0 1.65vmin rgba(91, 159, 255, 0.84),
            inset 0 0 0.7vmin rgba(255, 255, 255, 0.34);
    }
}

.mana-indicator.mana-indicator--pressed {
    background-color: rgba(18, 92, 224, 0.94) !important;
    box-shadow:
        0 0 1.65vmin rgba(91, 159, 255, 0.84),
        inset 0 0 0.7vmin rgba(255, 255, 255, 0.34);
}

/* Position for player health indicator - adjusted for larger size */
#player-health-indicator {
    bottom: var(--summoner-disc-inset-block);
    /* Adjusted for larger indicator */
    right: var(--summoner-disc-inset-inline);
    /* Adjusted for larger indicator */
    transform: translate(50%, 50%);
    /* Position half outside */
}

/* Position for opponent health indicator - adjusted for larger size */
#opponent-health-indicator {
    bottom: var(--summoner-disc-inset-block);
    /* Adjusted for larger indicator */
    right: var(--summoner-disc-inset-inline);
    /* Adjusted for larger indicator */
    transform: translate(50%, 50%);
    /* Position half outside */
}

/* Position for player mana indicator - adjusted for larger size */
#player-mana-indicator {
    bottom: var(--summoner-disc-inset-block);
    /* Adjusted for larger indicator */
    left: var(--summoner-disc-inset-inline);
    /* Adjusted for larger indicator */
    transform: translate(-50%, 50%);
    /* Position half outside */
    position: absolute !important;
}

/* Position for opponent mana indicator - adjusted for larger size */
#opponent-mana-indicator {
    bottom: var(--summoner-disc-inset-block);
    /* Adjusted for larger indicator */
    left: var(--summoner-disc-inset-inline);
    /* Adjusted for larger indicator */
    transform: translate(-50%, 50%);
    /* Position half outside */
    position: absolute !important;
}

/* Deck Visualization */
.deck-visual,
#player-deck,
#opponent-deck {
    position: relative;
    width: var(--card-width);
    /* Match card width */
    height: var(--card-height);
    /* Match card height */
    background-image: var(--cardback-image, url('../assets/images/cardback.webp'));
    background-size: cover;
    background-position: center;
    border-radius: 0;
    box-shadow: 0 0 8px rgba(238, 219, 241, 0.5);
    cursor: pointer;
    transition: transform 0.2s ease, box-shadow 0.2s ease;
    border: 1px solid rgba(238, 219, 241, 0.3);
    z-index: 10;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    overflow: visible;
    margin: 0 20px;
}

/* Ensure parent containers don't clip content */
#player-area,
#opponent-area,
.player-area,
.opponent-area,
.board-container,
.hand-container,
#game-view {
    overflow: visible;
}

/* Stacked card effect with pseudo-elements */
.deck-visual::before,
#player-deck::before,
#opponent-deck::before {
    content: '';
    position: absolute;
    top: -2px;
    left: -2px;
    width: 100%;
    height: 100%;
    background-image: var(--cardback-image, url('../assets/images/cardback.webp'));
    background-size: cover;
    background-position: center;
    border-radius: 0;
    border: 1px solid rgba(238, 219, 241, 0.3);
    z-index: -1;
    /* Behind main element */
    box-sizing: border-box;
}

.deck-visual::after,
#player-deck::after,
#opponent-deck::after {
    content: '';
    position: absolute;
    top: -4px;
    left: -4px;
    width: 100%;
    height: 100%;
    background-image: var(--cardback-image, url('../assets/images/cardback.webp'));
    background-size: cover;
    background-position: center;
    border-radius: 0;
    border: 1px solid rgba(238, 219, 241, 0.3);
    z-index: -2;
    /* Behind ::before */
    box-sizing: border-box;
}

/* Hovering picks the whole pile up on devices with a real hover pointer. Touch
   browsers can leave :hover stuck after a tap, which would otherwise shift the
   deck into the compact phone HUD until the next interaction. */
@media (hover: hover) and (pointer: fine) {
    .deck-visual:hover,
    #player-deck:hover,
    #opponent-deck:hover {
        box-shadow: 0 0 12px rgba(238, 219, 241, 0.8);
        transform: rotate(-5deg) translateY(-3px);
    }
}

/* A phone has no hover, so the pile above simply never moved there. The owner's
   ask is parity, not a new effect: "it has its own 2d anim on desktop, it was
   perfect — just make [the] anim on desktop apply to mobile". So a press plays
   the IDENTICAL pose — same rotation, same lift, same glow, no 3D — driven by
   the wrapper, which is the element a finger actually lands on. The deck
   already carries `transition: transform 0.2s ease, box-shadow 0.2s ease`, so
   it eases in and out rather than snapping. */
@media (hover: none), (pointer: coarse) {
    body.game-active #player-deck-wrapper:is(:active, .mf-pressed) #player-deck,
    body.game-active #opponent-deck-wrapper:is(:active, .mf-pressed) #opponent-deck,
    body.game-active #player-deck-wrapper:is(:active, .mf-pressed) .deck-visual,
    body.game-active #opponent-deck-wrapper:is(:active, .mf-pressed) .deck-visual {
        box-shadow: 0 0 12px rgba(238, 219, 241, 0.8);
        transform: rotate(-5deg) translateY(-3px);
    }
}

@media (prefers-reduced-motion: reduce) {
    /* Keep the acknowledgement, drop the movement. */
    body.game-active #player-deck-wrapper:is(:active, .mf-pressed) #player-deck,
    body.game-active #opponent-deck-wrapper:is(:active, .mf-pressed) #opponent-deck,
    body.game-active #player-deck-wrapper:is(:active, .mf-pressed) .deck-visual,
    body.game-active #opponent-deck-wrapper:is(:active, .mf-pressed) .deck-visual {
        transform: none;
    }
}

.deck-count {
    position: absolute;
    bottom: -8px;
    right: -8px;
    background-color: rgba(0, 0, 0, 0.8);
    color: #eedbf1;
    border: 1px solid rgba(238, 219, 241, 0.5);
    border-radius: 50%;
    width: 6.74vmin;
    height: 6.74vmin;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 3.57vmin;
    font-weight: bold;
    z-index: 20;
    /* Increased z-index to ensure visibility */
}

/* Position player's deck count at the top so it's visible when deck is cut off */
#player-deck .deck-count {
    top: -1.59vmin;
    bottom: auto;
}

#player-deck,
#opponent-deck {
    /* Ensure decks are visually above hand containers for drop events */
    z-index: 15;
    position: relative;
    /* z-index requires a position other than static */
}

/* Player/Opponent deck wrapper positioning - balanced distance from hands */
#player-deck-wrapper {
    position: absolute;
    bottom: -8.91vmin;
    /* Aligned with the bottom edge, cut off like cards */
    right: 21%;
    /* Slightly closer to the right edge */
    z-index: 5;
    overflow: visible;
    /* Removed negative margin that was causing cutoff */
}

/* Completely remove wrapper height constraints and let content determine size */
#opponent-deck-wrapper {
    position: absolute;
    top: -8.91vmin;
    /* Aligned with the top edge, cut off like cards */
    right: 21%;
    /* Slightly closer to the right edge */
    z-index: 5;
    overflow: visible;
    /* Removed negative margin that was causing cutoff */
}
/* Number-tick pulse: a brief glow when health/mana/deck values change */
.count-pulse {
    animation: count-pulse-glow 450ms ease-out;
}

@keyframes count-pulse-glow {
    0% {
        filter: brightness(1);
    }

    35% {
        filter: brightness(1.7);
        text-shadow: 0 0 10px rgba(238, 219, 241, 0.8);
    }

    100% {
        filter: brightness(1);
    }
}

/* Stardust balances tick with a richer, on-theme (lunar-lavender) pop so
   earning currency feels rewarding — scoped so battle indicators keep the
   subtler glow above. */
.user-stardust.count-pulse,
.collection-stardust-value.count-pulse {
    animation: stardust-pulse-glow 600ms cubic-bezier(0.34, 1.56, 0.64, 1);
}

@keyframes stardust-pulse-glow {
    0% {
        transform: scale(1);
        filter: brightness(1);
    }

    30% {
        transform: scale(1.14);
        filter: brightness(1.6);
        text-shadow: 0 0 14px rgba(185, 154, 220, 0.9);
    }

    100% {
        transform: scale(1);
        filter: brightness(1);
    }
}

@media (prefers-reduced-motion: reduce) {
    .count-pulse,
    .user-stardust.count-pulse,
    .collection-stardust-value.count-pulse {
        animation: none;
    }
}

/* Phone-landscape battle: size the corner summoner portraits. VIEWPORT-driven
   (not pointer:coarse) so it also applies in a device emulator. The left-only
   feed frees the right edge, so the portraits can be generous; safe-area
   offsets clear the notch / home indicator. The full-size .portrait-hitbox
   keeps attack-drop targets generous. */
@media (orientation: landscape) and (max-height: 560px) {
    .player-portrait {
        --portrait-frame-width: 0.8vmin;
        /* TALL ENOUGH FOR TWO LINES. At 24px the plate could hold exactly one
           line, so `fitSummonerNameplate` exhausted its shrink range on a name
           like "ORION NIGHTWHISPER" and fell through to
           `data-fit="truncated"` — nowrap plus ellipsis, rendered as
           "ION NIGHTWHISF" with the discs covering both ends. A phone crest is
           ~80px wide; no single line of a real summoner name fits it at a
           legible size, so two lines is the only honest shape and the plate has
           to reserve the room for them. */
        --summoner-nameplate-height: clamp(24px, calc(8 * var(--mf-dvmin)), 32px);
        --summoner-nameplate-gap: clamp(2px, calc(0.7 * var(--mf-dvmin)), 4px);
        /* SHORT LANDSCAPE KEEPS ITS OWN EXPLICIT FRAME, and its disc stays the
           9.4vmin it has always been here rather than following that frame.
           This tier is entered by phones AND by short landscape tablets like
           1024x560, and its 36vmin crest is proportionally wider than the base
           31vmin — deriving the disc from it would grow every disc in the tier
           by 16% for no reason. Pinning both tokens makes the tier's geometry
           byte-identical to what it was before the rail cap existed. */
        --summoner-portrait-width: 36vmin;
        --summoner-disc-size: 9.4vmin;
        width: 36vmin;
        height: 26vmin;
    }

    .summoner-nameplate {
        /* CENTRED ON THE CREST. A previous pass anchored the plate's RIGHT edge
           to the crest so a long name could grow inward from a 56px crest
           without falling under the notch; with the crest back to a real size
           and the whole column stepped inboard of the exit gutter, that hack is
           both unnecessary and exactly what the owner saw as "their titles are
           unaligned" — every plate hung off to one side of the portrait it
           belongs to. Centred is aligned. */
        /* ITS OWN ROW BELOW THE CREST — reported: "u made the summoner nametag
           overlap its icons". Measured at 844x280: crest 0..80, plate 66..94,
           so 14px of the plate sat on top of the crest's lower edge and over
           the resource discs in its corners. The base rule deliberately
           STRADDLES that edge (desktop has protruding discs and a corridor
           between them); a phone keeps its discs inside the frame, so there is
           no corridor and the plate has to clear the crest entirely. */
        width: 100%;
        left: 50%;
        right: auto;
        transform: translateX(-50%);
        padding: 2px clamp(7px, 2vmin, 12px);
        font-size: clamp(9px, 2.4vmin, 12px);
        line-height: 1.05;
        letter-spacing: 0.01em;
        white-space: normal;
        text-wrap: balance;
    }

    #player-portrait {
        bottom: calc(
            var(--summoner-nameplate-reserve)
            + max(4px, env(safe-area-inset-bottom, 0px))
        );
        right: max(calc(4.7vmin + 6px), env(safe-area-inset-right, 0px));
    }

    #opponent-portrait {
        top: max(8px, env(safe-area-inset-top, 0px));
        right: max(calc(4.7vmin + 6px), env(safe-area-inset-right, 0px));
    }

}

/* Short landscape phones through 932px cannot afford the full portrait beside a
   centred five-card row. Preserve its stylized proportion in a compact command
   rail: portraits and resource sockets stay right, decks stay left, and
   neither changes board/hand centring. */
@media (orientation: landscape) and (max-height: 560px) and (max-width: 932px) {
    /* ITS OWN ROW BELOW THE CREST — reported: "u made the summoner nametag
       overlap its icons". Measured at 844x280: crest 0..80, plate 66..94, so
       14px of the plate sat on the crest's lower edge and over the resource
       discs in its corners. The base rule deliberately STRADDLES that edge
       (desktop has protruding discs and a corridor between them); a phone keeps
       its discs inside the frame, so there is no corridor and the plate must
       clear the crest entirely.

       PHONE-ONLY ON PURPOSE. This first went into the (max-height: 560px) block,
       which has NO max-width — so it also hit landscape TABLET boxes like
       1024x560 and 960x600, which do have the straddle corridor, and broke 30
       summoner-status assertions there. */
    .summoner-nameplate {
        top: calc(100% + var(--summoner-nameplate-gap));
    }

    .player-portrait {
        /* COMPOSITION: discs INSIDE the frame, name crest in its own row below.

           The desktop composition — discs half outside the lower corners, name
           straddling the bottom edge between them — was tried here and reverted
           on measurement. At phone scale the crest is anchored flush to the
           right safe inset, so a disc half outside hangs 7px past that inset,
           and on a box with no insets (iPhone SE) 7px of the player's HEALTH
           goes off the screen entirely. Insetting the crest to make room drags
           the summoner status rail, which is anchored to the crest's right edge,
           into the board — two whole failure families. Moving the cluster inward
           instead costs the command seam the Volume+End Turn stack needs.

           There is no version of the half-outside disc that a 275-344px page box
           can pay for. What IS affordable, and is kept, is the SCALE: the crest
           is 92-103px here rather than the 56px a previous pass shrank it to.

           Worth retrying as a dedicated round if the status rail is first
           decoupled from the crest's right edge. */
        --summoner-resource-protrusion: 0px;
        --summoner-nameplate-reserve: calc(
            var(--summoner-nameplate-height) + var(--summoner-nameplate-gap)
        );
        width: var(--phone-summoner-portrait-width);
        height: calc(var(--phone-summoner-portrait-width) * 23 / 31);
    }

    /* THE CREST STOPS AT THE CORNER LANE, not at the safe inset.

       A pass inset it by its own disc protrusion, to keep the outer disc out of
       the right safe inset. That fixed the wrong axis and cost far more than it
       bought: the summoner status rail is anchored to the crest's RIGHT EDGE, so
       moving the crest sideways drags the entire rail — and everything
       overflowing from it — that much further into the board. It produced two
       whole failure families in mobile-battle.spec.js. The defect actually
       being chased was the discs entering the BOTTOM gesture band, which is a
       vertical problem and is solved on #player-portrait below.

       `--phone-summoner-column-right` is that lesson kept: it is the safe inset
       on every box where the inset already clears the owner's corner controls
       (47px on the reporting device, 59px on every notched profile), so the
       crest does NOT move there and neither does the rail. It only steps in on
       a phone with no right inset, where the exit X and the volume trigger
       would otherwise be painted on the crest — over the opponent's health
       readout at 915x336, measured. The deck anchor below uses the same base,
       so the whole column moves as one piece. */
    #opponent-portrait {
        top: 0;
        right: var(--phone-summoner-column-right);
    }

    #player-portrait {
        right: var(--phone-summoner-column-right);
        /* Mirror the opponent cluster's exact top-edge anchor. The decorative,
           non-interactive nameplate may meet the bottom gesture band; keeping
           the two summoners symmetric is more important than an empty strip. */
        bottom: var(--summoner-nameplate-reserve);
    }

    .health-indicator,
    .mana-indicator {
        width: clamp(24px, calc(7.4 * var(--mf-dvmin)), 32px) !important;
        height: clamp(24px, calc(7.4 * var(--mf-dvmin)), 32px) !important;
        min-width: clamp(24px, calc(7.4 * var(--mf-dvmin)), 32px) !important;
        min-height: clamp(24px, calc(7.4 * var(--mf-dvmin)), 32px) !important;
        font-size: clamp(10px, calc(3.6 * var(--mf-dvmin)), 15px) !important;
    }

    /* Discs sit INSIDE the frame's lower corners — see the composition note
       above for why half-outside is unaffordable at this scale. */
    #player-health-indicator,
    #opponent-health-indicator {
        top: auto;
        right: 4px;
        bottom: 4px;
        left: auto;
        transform: none;
    }

    #player-mana-indicator,
    #opponent-mana-indicator {
        top: auto;
        right: auto;
        bottom: 4px;
        left: 4px;
        transform: none;
    }


    /* Decks keep desktop's horizontal placement — inset from the RIGHT edge —
       rather than stacking into the left rail. That rail is the narration lane,
       and two decks parked in it forced the StoryFeed to start halfway down the
       screen to dodge them. A landscape phone has width to spare and no height
       to spare, so the decks spend width. `fixed` still anchors them to the
       viewport edge (the board here is pinned to art guides, so `absolute`
       would land them next to the slot rows instead of the screen edge). */
    #player-deck-wrapper,
    #opponent-deck-wrapper {
        position: fixed;
        left: auto;
        /* Reserve the stable status lane on every phone. The 8px allowance
           leaves two painted pixels after the count badge's 6px overflow.
           Anchored to the summoner column's own right edge, so the deck keeps
           its measured seam from the status plate whether or not the corner
           lane had to be reserved. */
        right: calc(var(--phone-summoner-column-right) + var(--phone-status-rail-width) + 8px);
    }

    /* Cut off at the edge, exactly as desktop does with its -8.91vmin offset
       (40% of the cardback's height at every size it was measured). This
       override had flattened both decks flush against the edge, so a landscape
       phone showed two complete cardbacks — they read as playable cards rather
       than off-stage stacks, and they ate the left edge on the tightest screen
       we have. Percentage translate reproduces the desktop proportion at any
       deck size. The count badges already sit on the edge that stays visible
       (opponent bottom, player top) precisely for this treatment. */
    /* 40% of the cardback, matching desktop's -8.91vmin of a 22.2vmin card,
       applied per wrapper just below. Measured from the true viewport edge:
       anchoring to the safe inset first left the player's deck only 5-13%
       hidden, because the home-indicator inset pushed it back inboard. The
       hidden part is decorative — the count badge sits on the edge that stays
       visible — so it may run under that band exactly as it runs off the screen
       edge on desktop. A percentage translate is measured against the element's
       OWN height, so it lands on exactly 40% whatever the cardback ends up
       being; the equivalent calc had to guess that height and overshot to
       ~50%. */
    #opponent-deck-wrapper {
        top: 0;
        bottom: auto;
        transform: translateY(-40%);
    }

    #player-deck-wrapper {
        top: auto;
        bottom: 0;
        transform: translateY(40%);
    }

    /* PUBLISHED, because the deck's width is the inner edge of the command lane
       and something outside this file has to be able to stay clear of it. The
       mulligan's centred action row is the first such consumer: it is capped
       against `column-right + rail + 8px + this`, mirrored, so a long translated
       label grows until it reaches the deck and then wraps instead of printing
       over the stack.

       THE DECK ITSELF STILL WRITES THE LITERAL, in both this gate and the
       `max-height: 320px` override below, and the token restates it. That is a
       deliberate duplication, not an oversight: the override's gate has no
       `max-width`, so a landscape tablet can match it and a `var(--phone-*)`
       read there would resolve to nothing and drop the deck to its intrinsic
       width. Keeping the pattern identical in both gates is what makes that
       readable. `mulliganActionRowCentre.test.js` asserts the token and the
       width are the same string in each gate, so they cannot drift apart. */
    :root {
        --phone-deck-width: max(44px, calc(8.8488 * var(--card-unit)));
    }

    #player-deck,
    #opponent-deck {
        /* Size the physical cardback instead of scaling its raster layer. The
           0.72 factors are folded into the card-unit coefficients so browsers
           paint at the final pixel size on every frame. */
        width: max(44px, calc(8.8488 * var(--card-unit)));
        height: max(79.54px, calc(15.984 * var(--card-unit)));
        margin: 0;
    }

    #player-deck .deck-count,
    #opponent-deck .deck-count {
        right: -6px;
        bottom: -6px;
        width: clamp(22px, calc(4.8528 * var(--mf-dvmin)), 28px);
        height: clamp(22px, calc(4.8528 * var(--mf-dvmin)), 28px);
        font-size: clamp(10px, calc(2.5704 * var(--mf-dvmin)), 15px);
    }

    #player-deck .deck-count {
        top: -6px;
        bottom: auto;
    }
}

/* The 667px page box grows vertically when Safari collapses its bars, but its
   five-slot board keeps the same horizontal edge. Reveal a little less of each
   decorative stack in that one constrained mode so the painted cardback
   (including its offset duplicate) cannot sit over slot five. */
@media (orientation: landscape) and (min-height: 301px) and (max-height: 560px) and (max-width: 700px) {
    #opponent-deck-wrapper {
        transform: translateY(-58%);
    }

    #player-deck-wrapper {
        transform: translateY(58%);
    }
}

/* On the 275-320px-tall phone page boxes the painted stack extends four pixels
   beyond the cardback itself. Reduce only that decorative pile so its complete
   painted footprint clears the rightmost board slot and the status rail. */
@media (orientation: landscape) and (max-height: 320px) {
    /* The token restates the override for the same reason the gate above states
       it: the mulligan action row is capped against the deck lane and cannot
       read this file's rules. The literal stays because THIS gate has no
       `max-width` — a landscape tablet can match it, where no `--phone-*` token
       exists, and a `var()` that resolves to nothing invalidates the whole
       declaration (`phoneCommandLaneSafeArea.test.js` enforces that and caught
       exactly this). Token and literal must stay equal; that is asserted. */
    :root {
        --phone-deck-width: 38px;
    }

    #player-deck,
    #opponent-deck {
        width: 38px;
        height: 69px;
    }

}

/* Keep the portrait cluster in a predictable right-side combat rail on
   landscape tablets, while the deck occupies the left utility rail beside the
   battle log. Board slots and hands retain their established true centering. */
@media (orientation: landscape) and (min-width: 960px) and (min-height: 561px) and (max-width: 1560px) and (max-aspect-ratio: 17/10) {
    body {
        /* THE TABLET TIER RE-PARAMETERISES THE SHARED RAIL BUDGET rather than
           hand-writing its own sizes. Every one of these feeds the same closed
           form in game-board.css, so the tier gets its own proportions AND the
           guarantee that the rail stops short of the board row — which is what
           the previous hand-written clamps could not give: they cleared by
           2.9px at 1368x822 and by 3.8px at 1024x768, and a device outside the
           band's width/aspect box got no help at all. */
        --summoner-rail-inset: clamp(29px, 3.7vmin, 44px);
        /* Discs sit further inside the tablet frame: with the base 5px inset
           the mana disc painted under the rightmost centred board slot (2.3px
           at 960x600, 18.3px at 1024x768 before the narrower portrait). */
        --summoner-disc-inset-inline: 14px;
        /* A third of the frame, the proportion the tier's own 8vmin disc had
           against its 24vmin portrait. */
        --summoner-disc-ratio: calc(1 / 3);
    }

    :root {
        --tablet-battle-deck-left: calc(
            env(safe-area-inset-left, 0px)
            + var(--story-lane-w, 72px)
            + 12px
        );
    }

    .player-portrait {
        --portrait-frame-width: clamp(8px, 0.85vmin, 10px);
        --portrait-radius:
            clamp(48px, 7vmin, 72px)
            clamp(48px, 7vmin, 72px)
            14px
            14px;
        /* A tablet reads from further away than a phone and closer than a
           desktop, so the frame is deliberately smaller than the desktop
           31vmin. The 200px floor keeps the summoner legible on the smallest
           tablet; the shared cap still overrides it when the board row
           genuinely cannot pay, because a collision is worse than a small
           crest. */
        --summoner-portrait-design: clamp(200px, 24vmin, 270px);
        height: calc(var(--summoner-portrait-width) * 18 / 24);
    }

    #player-mana-indicator,
    #opponent-mana-indicator {
        left: var(--summoner-disc-inset-inline);
    }

    #player-health-indicator,
    #opponent-health-indicator {
        right: var(--summoner-disc-inset-inline);
    }

    #player-portrait {
        right: var(--summoner-rail-inset);
        bottom: calc(
            var(--summoner-nameplate-reserve)
            + max(8px, env(safe-area-inset-bottom, 0px))
        );
    }

    #opponent-portrait {
        right: var(--summoner-rail-inset);
        top: var(--summoner-rail-inset);
    }

    #player-deck-wrapper,
    #opponent-deck-wrapper {
        right: auto;
        left: var(--tablet-battle-deck-left);
    }
}

/* ============================================================================
   battle tap feedback — summoner portraits and deck stacks.

   Reported twice as "tapping summoner icons or decks has no effect on mobile".
   The first attempt styled `.db-character-card` and `.deck-list-item`, which are
   the DECK BUILDER's summoner cards and saved-deck rows — not these elements at
   all, so the reported surface never changed.

   Scoped to `body.game-active` because unscoped rules written here and in
   game-board.css have leaked into the deck builder, collection, foundry and
   mulligan twice before. Transform/filter only, so the press is compositor work
   and cannot reflow the board. Reduced motion keeps the acknowledgement and
   drops the movement.
   ========================================================================== */
body.game-active #player-portrait,
body.game-active #opponent-portrait,
body.game-active #player-deck-wrapper,
body.game-active #opponent-deck-wrapper {
    -webkit-tap-highlight-color: transparent;
    transition: transform 0.12s cubic-bezier(0.2, 0.9, 0.3, 1), filter 0.12s linear;
}

/* The DECK wrappers are deliberately absent here: a deck answers a press with
   the desktop hover pose (rotate + lift + glow, above), and a second competing
   scale on the wrapper would change that pose rather than reproduce it. They
   keep the transition and tap-highlight reset declared above; the portraits,
   which have no hover pose of their own, keep the press-in. */
body.game-active #player-portrait:is(:active, .mf-pressed),
body.game-active #opponent-portrait:is(:active, .mf-pressed) {
    transform: scale(0.955);
    filter: brightness(1.12);
    transition-duration: 0.06s;
}

body.game-active #player-deck-wrapper:is(:active, .mf-pressed),
body.game-active #opponent-deck-wrapper:is(:active, .mf-pressed) {
    filter: brightness(1.12);
    transition-duration: 0.06s;
}

@media (prefers-reduced-motion: reduce) {
    body.game-active #player-portrait:is(:active, .mf-pressed),
    body.game-active #opponent-portrait:is(:active, .mf-pressed),
    body.game-active #player-deck-wrapper:is(:active, .mf-pressed),
    body.game-active #opponent-deck-wrapper:is(:active, .mf-pressed) {
        transform: none;
        /* The press must still read without motion. */
        filter: brightness(1.2);
    }
}
