/* Deck Builder Styles */

.deck-builder-modal {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.85);
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
    z-index: 10000;
    justify-content: center;
    align-items: center;
    padding: 20px;
}

.collection-modal {
    padding: 12px;
}

.collection-visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

.collection-modal :is(
    .deck-tab,
    .deck-builder-search,
    .deck-filter,
    .deck-btn,
    .select-cardback-btn
):focus-visible {
    outline: 2px solid #f6e27a;
    outline-offset: 2px;
}

/* The spell's summoner, seated on the rim the card frame draws between its art
   and its description. `.card-desc` owns that boundary at 72.5% of card height
   (cards.css); the chip's BOTTOM rests on it, so it covers neither the title it
   used to sit on nor a line of description. Left-aligned with the Stellar chip
   so the tile reads as one column of chrome rather than four loose stickers. */
.collection-card-owner-badge {
    position: absolute;
    top: 72.5%;
    left: 2px;
    transform: translateY(-100%);
    max-width: calc(100% - 4px);
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    padding: 2px 6px;
    border-radius: 4px;
    font-size: 0.6rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.35px;
    color: #f4f8ff;
    background: linear-gradient(135deg, rgba(90, 110, 170, 0.95), rgba(130, 170, 230, 0.95));
    border: 1px solid rgba(200, 220, 255, 0.4);
    z-index: 12;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35);
}

.deck-builder-container {
    background: linear-gradient(145deg, rgba(30, 25, 50, 0.98), rgba(20, 15, 35, 0.98));
    border: 1px solid rgba(238, 219, 241, 0.3);
    border-radius: 16px;
    width: 100%;
    max-width: 1200px;
    /* dvh, not svh: `svh` is frozen at the bars-SHOWING height, so once a
       phone hides its URL bar — which is what opening the keyboard does — this
       panel stops ~100px short of the page box and leaves a dead band it can
       never scroll back. dvh is the height the page actually has. */
    max-height: 90dvh;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}

/* Collection Modal Overrides */
.collection-modal .deck-builder-container {
    max-width: 1300px;
    max-height: 96dvh;
}

.collection-container {
    display: flex;
    flex-direction: column;
    height: min(96dvh, 920px);
    overflow: hidden;
    /* Rebind physical dimensions at this inheritance boundary. The root values
       are computed against root --card-unit before a high-resolution tier can
       counter-scale it, which would otherwise make zoom grow cards twice. */
    --card-width: calc(12.29 * var(--card-unit));
    --card-height: calc(22.2 * var(--card-unit));
}

/* The Collection is authored as a 1300x920 desktop workspace. Large desktop
   canvases previously kept those exact pixels, turning the whole modal into a
   thumbnail at QHD/4K. Layout zoom scales borders, type, controls, and hit
   targets as one crisp physical surface. Card anatomy already follows vmin,
   so each tier counter-scales --card-unit to avoid applying viewport growth a
   second time. Requiring both dimensions deliberately excludes 2560x1080 and
   other short ultrawides whose 920px workspace is already height-bound. */
@media (min-width: 2560px) and (min-height: 1440px) {
    .collection-modal > .collection-container {
        zoom: 133.3333%;
        --card-unit: 0.75vmin;
    }
}

@media (min-width: 3200px) and (min-height: 1800px) {
    .collection-modal > .collection-container {
        zoom: 166.6667%;
        --card-unit: 0.6vmin;
    }
}

@media (min-width: 3840px) and (min-height: 2160px) {
    .collection-modal > .collection-container {
        zoom: 200%;
        --card-unit: 0.5vmin;
    }
}

@media (min-width: 5120px) and (min-height: 2880px) {
    .collection-modal > .collection-container {
        zoom: 266.6667%;
        --card-unit: 0.375vmin;
    }
}

@media (min-width: 7680px) and (min-height: 4320px) {
    .collection-modal > .collection-container {
        zoom: 400%;
        --card-unit: 0.25vmin;
    }
}

.collection-container > .deck-builder-header {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    gap: 12px;
}

.collection-modal.collection-packs-active .deck-builder-container {
    overflow: hidden;
}

.collection-modal.collection-packs-active .collection-content {
    overflow: hidden;
}

.collection-modal.collection-packs-active .collection-section-packs {
    overflow: hidden;
}

.collection-subtitle {
    margin: 6px 0 0;
    color: rgba(238, 219, 241, 0.7);
    font-size: 0.9rem;
}

.collection-header-actions {
    display: flex;
    align-items: center;
    gap: 12px;
}

.collection-toast-host {
    position: fixed;
    right: 24px;
    bottom: 24px;
    display: flex;
    flex-direction: column;
    gap: 10px;
    z-index: 10001;
    pointer-events: none;
}

.collection-toast {
    min-width: 260px;
    max-width: 360px;
    padding: 12px 14px;
    border-radius: 12px;
    background: linear-gradient(135deg, rgba(28, 24, 44, 0.98), rgba(38, 28, 60, 0.98));
    border: 1px solid rgba(255, 240, 200, 0.35);
    color: #f7f3ff;
    font-size: 0.88rem;
    line-height: 1.35;
    box-shadow: 0 12px 30px rgba(0, 0, 0, 0.45);
    animation: collectionToastIn 0.34s cubic-bezier(0.22, 1.18, 0.32, 1);
}

.collection-toast-success {
    border-color: rgba(160, 255, 214, 0.6);
    box-shadow: 0 12px 30px rgba(50, 200, 140, 0.25);
}

.collection-toast-info {
    border-color: rgba(180, 210, 255, 0.6);
    box-shadow: 0 12px 30px rgba(80, 120, 200, 0.25);
}

.collection-toast-error {
    border-color: rgba(255, 140, 140, 0.6);
    box-shadow: 0 12px 30px rgba(210, 70, 90, 0.25);
}

.collection-toast-exit {
    animation: collectionToastOut 0.35s ease-in forwards;
}

@keyframes collectionToastIn {
    from {
        opacity: 0;
        transform: translateY(12px) scale(0.98);
    }

    to {
        opacity: 1;
        transform: translateY(0) scale(1);
    }
}

@keyframes collectionToastOut {
    from {
        opacity: 1;
        transform: translateY(0) scale(1);
    }

    to {
        opacity: 0;
        transform: translateY(8px) scale(0.98);
    }
}

.collection-stardust {
    -webkit-appearance: none;
    appearance: none;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    padding: 6px 12px;
    border-radius: 999px;
    border: 1px solid rgba(238, 219, 241, 0.3);
    background: rgba(255, 255, 255, 0.08);
    min-width: 90px;
    margin: 0;
    color: inherit;
    font: inherit;
    cursor: pointer;
    transition: background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
}

.collection-stardust:hover:not([disabled]) {
    background: rgba(238, 219, 241, 0.14);
    border-color: rgba(238, 219, 241, 0.58);
    box-shadow: 0 0 14px rgba(172, 119, 220, 0.18);
}

.collection-stardust:focus-visible {
    outline: 2px solid #f0ddff;
    outline-offset: 2px;
}

.collection-stardust-label {
    font-size: 0.65rem;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: rgba(238, 219, 241, 0.6);
}

.collection-stardust-value {
    font-size: 0.95rem;
    font-weight: 600;
    color: #f4e6ff;
    /* Flex item (blockified) — already transformable for the count-pulse.
       tabular-nums keeps the width steady while the value ticks. */
    font-variant-numeric: tabular-nums;
}

.collection-tabs {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 2px;
    min-height: 0;
    padding: 0 8px;
    overflow-y: hidden;
    overflow-x: auto;
    overscroll-behavior-inline: contain;
    scrollbar-width: thin;
    border-inline: 1px solid rgba(238, 219, 241, 0.14);
    background: transparent;
}

.collection-tabs .deck-tab {
    flex: 0 0 auto;
    width: auto;
    min-height: 34px;
    margin: 0;
    padding: 5px 10px;
    justify-content: center;
    text-align: center;
    white-space: nowrap;
}

.collection-content {
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: 8px 12px 6px;
    flex: 1;
    min-height: 0;
    overflow: hidden;
}

.collection-modal.collection-cardbacks-active .collection-content {
    overflow: hidden;
}

.collection-modal.collection-cardbacks-active .deck-builder-container {
    overflow: hidden;
}

.collection-controls-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    flex-wrap: nowrap;
}

.collection-card-tabs {
    margin-bottom: 0;
    flex-shrink: 0;
}

.collection-controls {
    margin-bottom: 0;
    flex: 1;
    display: flex;
    justify-content: flex-end;
    min-width: 0;
    margin-left: auto;
}

.collection-controls-row .deck-builder-controls {
    flex-wrap: nowrap;
    justify-content: flex-end;
    gap: 4px;
}

.collection-controls-row .deck-builder-search {
    width: clamp(150px, 20vw, 280px);
    min-width: 120px;
    flex: 1 1 auto;
}

.collection-controls-row .deck-filter {
    min-width: 74px;
    padding: 6px 8px;
}

/* Desktop keeps the existing inline toolbar. The phone layer turns this
   wrapper into a surfaced filter drawer without duplicating any controls. */
.collection-mobile-filters-toggle {
    display: none;
}

.collection-filter-popover {
    display: contents;
}

#collection-filter-collection {
    min-width: 104px;
}

#collection-filter-mana,
#collection-sort-cards {
    min-width: 74px;
}

.collection-modal .deck-builder-cards {
    max-height: none;
    min-height: 0;
    padding-bottom: 12px;
    overflow: hidden;
    scroll-padding-bottom: 0;
}

.collection-modal .collection-cards-grid {
    --catalog-card-scale: var(--collection-fit-card-scale, 1.05);
}

.collection-card-paint-stage {
    position: absolute;
    inset: 0;
    display: grid;
    grid-template-columns: inherit;
    grid-auto-rows: inherit;
    column-gap: inherit;
    row-gap: inherit;
    align-content: inherit;
    justify-content: inherit;
    justify-items: center;
    visibility: hidden;
    pointer-events: none;
}

.collection-modal .collection-cards-grid.collection-cards-grid-balanced {
    grid-template-columns: repeat(
        var(--collection-column-count),
        calc(var(--card-width) * var(--catalog-card-scale))
    );
    justify-content: space-evenly;
    align-content: center;
}

.collection-section-cards {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
    overflow: hidden;
}

#collection-cards-browser {
    display: grid;
    grid-template-rows: auto minmax(0, 1fr) auto;
    flex: 1;
    min-height: 0;
    overflow: hidden;
}

.collection-modal .deck-card-item.unowned {
    cursor: pointer;
}

.collection-card-detail .card {
    margin: 0;
}

.collection-card-detail-status .deck-btn {
    flex: none;
}

/* The craft spark burst moved to the shared UI.FX.burst primitive (fx.css). */

@keyframes craftButtonPulse {
    0% {
        transform: scale(1);
    }

    40% {
        transform: scale(1.08);
    }

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

@keyframes craftButtonGlow {
    0% {
        opacity: 0;
        transform: scale(0.5);
    }

    50% {
        opacity: 0.8;
        transform: scale(1);
    }

    100% {
        opacity: 0;
        transform: scale(1.2);
    }
}

/* responsive card detail handled in redesign section below */

.collection-section-cardbacks {
    display: flex;
    flex-direction: column;
    gap: 10px;
    flex: 1;
    min-height: 0;
    overflow: hidden;
    padding-right: 0;
    padding-bottom: 0;
}

.collection-section-deckbuilder {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
    overflow: hidden;
    padding-right: 2px;
    scrollbar-gutter: stable both-edges;
}

.collection-cardbacks-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 12px;
}

.collection-cardbacks-header h3 {
    margin: 0;
    color: #eedbf1;
    font-size: 1.2rem;
}

.collection-cardbacks-subtitle {
    margin: 6px 0 0;
    color: rgba(238, 219, 241, 0.7);
    font-size: 0.85rem;
}

.collection-controls #collection-filter-spell-owner {
    min-width: 120px;
}

.collection-cardbacks-count {
    color: rgba(238, 219, 241, 0.8);
    font-size: 0.85rem;
    padding: 4px 10px;
    border-radius: 999px;
    border: 1px solid rgba(238, 219, 241, 0.2);
    background: rgba(255, 255, 255, 0.08);
}

.collection-pagination,
.deck-builder-pagination,
.cardback-pagination {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding-top: 6px;
    flex-shrink: 0;
}

:is(.collection-pagination, .deck-builder-pagination, .cardback-pagination) .deck-btn {
    min-height: 34px;
    padding: 6px 10px;
}

@media (pointer: coarse) {
    :is(.collection-pagination, .deck-builder-pagination, .cardback-pagination) .deck-btn {
        min-height: 44px;
        padding: 6px 10px;
    }
}

/* Author display rules outrank the browser's default [hidden] rule. Keep the
   semantic visibility state and the pixels in lockstep on every breakpoint. */
.deck-saved-pagination[hidden] {
    display: none;
}

.collection-page-info,
.deck-builder-page-info,
.cardback-page-info {
    font-size: 0.85rem;
    color: rgba(238, 219, 241, 0.7);
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

.cardback-grid {
    display: grid;
    grid-template-columns: repeat(5, minmax(0, 1fr));
    gap: 16px;
    justify-items: center;
}

.collection-section-cardbacks .cardback-grid {
    flex: 1;
    min-height: 0;
    overflow: hidden;
}

.cardback-item {
    background: transparent;
    border-radius: 8px;
    padding: 8px;
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    border: 1px solid transparent;
    transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
    width: 100%;
    max-width: min(100%, calc((96svh - 380px) * 9 / 16));
    color: inherit;
    font: inherit;
    appearance: none;
    -webkit-appearance: none;
}

button.cardback-item {
    cursor: pointer;
}

button.cardback-item:disabled {
    cursor: default !important;
    opacity: 1;
    background: transparent;
    border-color: transparent;
}

button.cardback-item:focus-visible {
    outline: 3px solid #f6e27a;
    outline-offset: -1px;
}

.cardback-item:hover {
    background: transparent;
}

.cardback-item.selected {
    border-color: transparent;
    background: transparent;
}

.cardback-item.locked .cardback-image {
    opacity: 0.4;
    filter: grayscale(100%);
}

.cardback-image-wrapper {
    width: 100%;
    aspect-ratio: 9/16;
    position: relative;
    border-radius: 6px;
    overflow: hidden;
    margin-bottom: 6px;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}

.cardback-item.selected .cardback-image-wrapper {
    box-shadow: 0 0 15px rgba(255, 215, 0, 0.4), 0 4px 6px rgba(0, 0, 0, 0.3);
    outline: 2px solid #ffd700;
    outline-offset: -2px;
}

.cardback-item .select-cardback-btn {
    flex: 0 0 auto;
    width: 7em;
    text-align: center;
    margin-top: auto;
    pointer-events: none;
}

.select-cardback-btn.equipped {
    opacity: 0.5;
    background: #444;
    cursor: default;
}

.cardback-image {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.cardback-image-unavailable {
    visibility: hidden;
}

/* Cohort hold (docs/asset-loading.md law 2, the group form): a freshly
   presented page of backs withholds its ART WELLS — never its tile chrome —
   until every back has decoded or collection.js's bounded reveal cap releases
   them, so the page deals in left to right instead of trickling in network
   order. !important so no later rule can leak a held back early. */
.cardback-image.cardback-image-cohort-hold {
    visibility: hidden !important;
}

/* The neutral well behind a back that is still on its way (text-first render:
   the name, description and Equip never wait). The flat colour cross-fades
   with the arriving art; the sweep starts only after the shared art grace,
   so a warm load still appears atomically complete. The wrapper's own
   background paints beneath its <img>, so a settled back simply covers it.
   The sweep is a background-position animation, never a transform on a
   pseudo-element: Chromium lets a composited animation inside this modal's
   zoomed container (2560x1440 and up) escape its overflow clip, the trap the
   hover popup's art well documents in cards.css. The cross-fade itself is
   declared with the tilt host's transition below, which owns the wrapper's
   `transition` shorthand. */
.cardback-image-wrapper:has(> .cardback-image[data-retrying-image-state="loading"]),
.cardback-image-wrapper:has(> .cardback-image[data-retrying-image-state="retrying"]),
.cardback-image-wrapper:has(> .cardback-image-cohort-hold) {
    background-color: rgba(31, 26, 52, 0.95);
    background-image: linear-gradient(110deg,
        transparent 0%,
        rgba(255, 255, 255, 0.04) 32%,
        rgba(238, 219, 241, 0.16) 50%,
        rgba(255, 255, 255, 0.04) 68%,
        transparent 100%);
    background-size: 200% 100%;
    background-repeat: no-repeat;
    /* Parked off the box until the sweep starts. */
    background-position: 200% 0;
    animation: cardback-well-sweep 1.8s ease-in-out var(--card-art-grace, 450ms) infinite;
}

@keyframes cardback-well-sweep {
    from {
        background-position: 200% 0;
    }

    to {
        background-position: -100% 0;
    }
}

@media (prefers-reduced-motion: reduce) {
    /* Same selectors as the sweep, so this out-ranks it: the flat well stays,
       the motion goes. */
    .cardback-image-wrapper:has(> .cardback-image[data-retrying-image-state="loading"]),
    .cardback-image-wrapper:has(> .cardback-image[data-retrying-image-state="retrying"]),
    .cardback-image-wrapper:has(> .cardback-image-cohort-hold) {
        animation: none;
    }
}

.cardback-lock-overlay {
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    color: white;
    padding: 10px;
}

.cardback-selected-badge {
    position: absolute;
    top: 10px;
    right: 10px;
    background: #ffd700;
    color: black;
    padding: 4px 8px;
    border-radius: 4px;
    font-weight: bold;
    font-size: 0.75em;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}

.cardback-name {
    font-weight: bold;
    margin-bottom: 2px;
    color: #fff;
    font-size: 0.95em;
}

.cardback-desc {
    font-size: 0.72em;
    color: #aaa;
    margin-bottom: 6px;
    min-height: 0;
}

.lock-icon {
    font-size: 2em;
    margin-bottom: 5px;
}

.lock-hint {
    font-size: 0.85em;
    color: #ddd;
    font-style: italic;
}

.cardback-lock-copy {
    display: none;
}

.cardback-modal-overlay {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.9);
    z-index: 20000;
    display: flex;
    justify-content: center;
    align-items: flex-start;
    overflow-y: auto;
    padding: 20px 14px;
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
    animation: cardbackFadeIn 0.2s ease-out;
}

.cardback-modal-content {
    max-width: 980px;
    width: min(92vw, 980px);
    max-height: none;
    overflow: visible;
    margin: 0 auto;
    background: linear-gradient(145deg, rgba(26, 22, 42, 0.96), rgba(18, 16, 34, 0.98));
    color: #fff;
    border: 1px solid rgba(238, 219, 241, 0.2);
    border-radius: 16px;
    padding: 24px;
    box-shadow: 0 24px 60px rgba(0, 0, 0, 0.6);
}

.cardback-modal-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 12px;
    margin-bottom: 18px;
    border-bottom: 1px solid rgba(238, 219, 241, 0.2);
    padding-bottom: 10px;
}

.cardback-modal-title {
    margin: 0;
    color: #eedbf1;
    font-size: 1.4rem;
}

/* 3D tilt for cardback image wrappers — reuses card-tilt system */
.cardback-tiltable {
    --card-tilt-max: 12;
    --card-tilt-perspective: 500px;
    --card-tilt-scale: 1;
    --card-tilt-glare-intensity: 0.7;
    --card-tilt-glare-hue: 270;
    --card-tilt-x: 0.5;
    --card-tilt-y: 0.5;
    --card-tilt-rotate-x: 0deg;
    --card-tilt-rotate-y: 0deg;
    --card-tilt-opacity: 0;
    transform-style: flat;
    will-change: auto;
    /* background-color: the loading well above cross-fades with arriving art. */
    transition: box-shadow 0.25s ease, background-color 0.3s ease;
}

.cardback-tiltable.card-tilt-active {
    transform:
        perspective(var(--card-tilt-perspective))
        rotateX(var(--card-tilt-rotate-x))
        rotateY(var(--card-tilt-rotate-y))
        scale(var(--card-tilt-scale));
    transform-style: preserve-3d;
    will-change: transform;
    box-shadow: 0 8px 20px rgba(0, 0, 0, 0.5);
}

.cardback-modal-close {
    background: none;
    border: none;
    font-size: 2rem;
    color: rgba(238, 219, 241, 0.7);
    cursor: pointer;
    line-height: 1;
    transition: color 0.2s;
}

.cardback-modal-close:hover {
    color: #fff;
}

@keyframes cardbackFadeIn {
    from {
        opacity: 0;
    }

    to {
        opacity: 1;
    }
}

.collection-section-exchange {
    display: flex;
    flex-direction: column;
    gap: 16px;
    flex: 1;
    min-height: 0;
    overflow: hidden;
    padding-bottom: 24px;
}

.moon-exchange-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 16px;
}

.moon-exchange-subtitle {
    margin: 6px 0 0;
    color: rgba(238, 219, 241, 0.7);
    font-size: 0.9rem;
}

.moon-exchange-actions {
    display: flex;
    align-items: center;
    gap: 10px;
}

.moon-exchange-view-switcher {
    display: none;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 6px;
    flex: 0 0 auto;
}

.moon-exchange-view-btn {
    min-height: 34px;
}

.moon-exchange-view-btn.is-active {
    border-color: rgba(238, 219, 241, 0.6);
}

.moon-exchange-grid {
    display: grid;
    grid-template-columns: minmax(260px, 1fr) minmax(320px, 1.4fr);
    grid-template-rows: minmax(0, 1fr);
    gap: 18px;
    align-items: stretch;
    flex: 1;
    min-height: 0;
}

.moon-exchange-panel {
    background: rgba(12, 10, 24, 0.6);
    border: 1px solid rgba(238, 219, 241, 0.2);
    border-radius: 14px;
    padding: 16px;
    box-shadow: 0 16px 32px rgba(0, 0, 0, 0.25);
    display: flex;
    flex-direction: column;
    min-width: 0;
    min-height: 0;
    height: 100%;
    overflow: hidden;
}

.moon-exchange-panel-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 12px;
    gap: 10px;
    flex-wrap: wrap;
}

.moon-exchange-panel-header h4 {
    margin: 0;
    color: #eedbf1;
    font-size: 1rem;
}

.moon-exchange-summary-actions {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    justify-content: flex-end;
}

.moon-exchange-market .moon-exchange-panel-header {
    margin-bottom: 12px;
}

.moon-exchange-market .moon-exchange-summary-actions {
    justify-content: flex-end;
}

.moon-exchange-market-controls {
    position: relative;
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    align-items: center;
    margin-bottom: 12px;
}

.moon-exchange-market-controls .deck-builder-search {
    flex: 1;
    min-width: 220px;
}

/* Global form/button margins are useful in the lobby, but they make grid items
   exceed their Exchange tracks (especially Firefox) and consume an entire
   listing row at short desktop heights. The command bar owns spacing via gap. */
.moon-exchange-market-controls :is(
    .deck-builder-search,
    #moon-exchange-owner-filter,
    #moon-exchange-sort,
    .moon-exchange-refresh
) {
    margin: 0;
}

#moon-exchange-owner-filter,
#moon-exchange-sort {
    width: 100%;
    min-width: 0;
}

.moon-exchange-filters-toggle {
    display: none;
    min-width: 104px;
    min-height: 44px;
}

.moon-exchange-filter-menu {
    display: contents;
}

.moon-exchange-filter-menu[hidden] {
    display: none;
}

.moon-exchange-form {
    display: flex;
    flex-direction: column;
    gap: 12px;
}

/* The sell panel swaps the form for the empty state via the `hidden` attribute
   (renderSellOptions). Both carry a `display:` rule that would otherwise beat the
   UA `[hidden]{display:none}`, so guard them explicitly — same pattern as
   manga-overlay/story-feed. Without this the dead form and the "no cards yet"
   message both render at once. */
.moon-exchange-form[hidden],
.moon-exchange-sell-empty[hidden] {
    display: none;
}

.moon-exchange-label {
    font-size: 0.8rem;
    color: rgba(238, 219, 241, 0.75);
    text-transform: uppercase;
    letter-spacing: 0.08em;
}

.moon-exchange-input {
    width: 100%;
    box-sizing: border-box;
    min-width: 0;
}

.moon-exchange-availability {
    font-size: 0.8rem;
    color: rgba(238, 219, 241, 0.7);
}

.moon-exchange-sell-row {
    display: grid;
    grid-template-columns: 120px minmax(0, 1fr);
    gap: 12px;
    align-items: start;
}

.moon-exchange-sell-fields {
    display: grid;
    gap: 10px;
}

.moon-exchange-field {
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-width: 0;
}

.moon-exchange-sell-preview {
    width: 120px;
    height: 170px;
    border: 1px dashed rgba(238, 219, 241, 0.3);
    border-radius: 10px;
    background: rgba(6, 6, 12, 0.55);
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    cursor: pointer;
    position: relative;
    /* Capture the ambient unit for the fitted card to multiply — a custom
       property cannot reference itself, so the capture sits one level up. */
    --exchange-card-unit: var(--card-unit);
}

/* PHYSICALLY fitted, never composited. `fitCardInsidePreview` is allowed to
   UPSCALE (capped at 1.5, so a small card is not a lonely island in a 120x170
   box) and on the 1400x640 QA viewport it lands on 1.17x — the same stretched
   raster as the collection grid had. The shrink direction mattered too: the art
   canvas is built for the card's VISUAL box, so a transform shrink resampled a
   24px-worth canvas up to the 37px layout box and back down. Sizing by the unit
   resamples zero times in either direction, and is dimensionally identical
   because every card metric is a multiple of the unit. */
.moon-exchange-sell-preview .deck-builder-card {
    /* Pre-JS default. `fitCardInsidePreview` sets the real factor as an inline
       style, which outranks this. */
    --moon-exchange-card-scale: 0.68;
    --card-unit: calc(var(--exchange-card-unit, 1vmin) * var(--moon-exchange-card-scale));
    --card-width: calc(12.29 * var(--card-unit));
    --card-height: calc(22.2 * var(--card-unit));
    /* Hairline and shadow are the only card metrics NOT expressed in
       --card-unit, so the transform used to shrink them with everything else. */
    border-width: calc(1px * var(--moon-exchange-card-scale));
    box-shadow: 0 0 calc(8px * var(--moon-exchange-card-scale)) rgba(238, 219, 241, 0.5);
    --card-base-transform: scale(1);
    /* The card is a real flex item now, and a flex item that shrinks changes
       the card's SHAPE rather than its size. It is fitted to this box by
       measurement; it must not be renegotiated by the layout. */
    flex: 0 0 auto;
    margin: 0;
    pointer-events: none;
    transform-origin: center;
}

.moon-exchange-sell-preview .moon-exchange-preview-staging {
    position: absolute;
    visibility: hidden;
    pointer-events: none;
}

.moon-exchange-sell-placeholder {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    font-size: 0.7rem;
    color: rgba(238, 219, 241, 0.6);
    text-align: center;
    padding: 8px;
}

/* Empty state shown when there is nothing to sell — a friendly message that
   replaces the dead form + black card-preview box. */
/* The locked sell gate shares the empty state's composition: it is the same
   kind of object — a panel explaining why there is no form here yet. */
.moon-exchange-sell-locked {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 28px 20px;
    text-align: center;
    color: rgba(238, 219, 241, 0.75);
}

.moon-exchange-sell-locked[hidden] {
    display: none;
}

.moon-exchange-sell-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 28px 20px;
    text-align: center;
    color: rgba(238, 219, 241, 0.75);
}

.moon-exchange-sell-empty-icon {
    font-size: 2rem;
    opacity: 0.85;
}

.moon-exchange-sell-empty-title {
    margin: 0;
    font-size: 1rem;
    font-weight: 600;
    color: #eedbf1;
}

.moon-exchange-sell-empty-hint {
    margin: 0;
    font-size: 0.85rem;
    line-height: 1.45;
    max-width: 28ch;
}

.moon-exchange-create-btn {
    width: 100%;
    min-width: 0;
    max-width: 100%;
    box-sizing: border-box;
}

.moon-exchange-status {
    min-height: 18px;
    font-size: 0.8rem;
    color: rgba(238, 219, 241, 0.8);
}

.moon-exchange-status.success {
    color: rgba(160, 255, 214, 0.9);
}

.moon-exchange-status.error {
    color: rgba(255, 150, 150, 0.95);
}

.moon-exchange-proceeds {
    display: grid;
    gap: 6px;
    padding: 10px;
    border: 1px solid rgba(238, 219, 241, 0.16);
    border-radius: 10px;
    background: rgba(7, 6, 15, 0.42);
    font-size: 0.78rem;
    color: rgba(238, 219, 241, 0.74);
}

.moon-exchange-proceeds-row {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 10px;
}

.moon-exchange-proceeds-row strong {
    color: #f4e9f6;
    white-space: nowrap;
}

.moon-exchange-fee-policy {
    font-size: 0.72rem;
}

.moon-exchange-sell-error,
.moon-exchange-market-error {
    display: grid;
    place-items: center;
    align-content: center;
    gap: 12px;
    min-height: 0;
    flex: 1 1 auto;
    padding: 20px;
    text-align: center;
    color: rgba(255, 178, 178, 0.96);
}

.moon-exchange-sell-error[hidden] {
    display: none;
}

.moon-exchange-sell-error p,
.moon-exchange-market-error p {
    margin: 0;
}

/* One-shot reward pop when a trade completes (buy/sell/list). */
.moon-exchange-status--celebrate {
    animation: moon-exchange-celebrate 620ms cubic-bezier(0.34, 1.56, 0.64, 1);
    transform-origin: left center;
}

@keyframes moon-exchange-celebrate {
    0% { transform: scale(1); text-shadow: none; }
    35% {
        transform: scale(1.08);
        text-shadow: 0 0 12px rgba(160, 255, 214, 0.75);
    }
    100% { transform: scale(1); text-shadow: none; }
}

@media (prefers-reduced-motion: reduce) {
    .moon-exchange-status--celebrate {
        animation: none;
    }
}

.moon-exchange-listings {
    display: flex;
    flex-direction: column;
    gap: 6px;
    flex: 1;
    min-height: 0;
    overflow-y: hidden;
    padding-right: 6px;
    position: relative;
}

.moon-exchange-paint-stage {
    position: absolute;
    inset: 0 6px 0 0;
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-width: 0;
    min-height: 0;
    visibility: hidden;
    pointer-events: none;
}

.moon-exchange-pagination {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-top: 12px;
    gap: 12px;
}

.moon-exchange-page-info {
    font-size: 0.8rem;
    color: rgba(238, 219, 241, 0.75);
}

.moon-exchange-empty {
    text-align: center;
    padding: 30px 10px;
    color: rgba(238, 219, 241, 0.6);
    font-style: italic;
}

/* Skeleton loading */
.moon-exchange-skeleton {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    gap: 8px;
    padding: 4px 8px;
    border-radius: 12px;
    background: rgba(8, 6, 20, 0.6);
    border: 1px solid rgba(238, 219, 241, 0.1);
    animation: me-skeleton-fade-in 0.2s ease-out;
}

.moon-exchange-skeleton-card {
    display: flex;
    align-items: center;
    gap: 12px;
    flex: 1;
    min-width: 0;
}

.moon-exchange-skeleton-thumb {
    width: 66px;
    height: 118px;
    flex: 0 0 auto;
    border-radius: 8px;
    background: rgba(238, 219, 241, 0.06);
    position: relative;
    overflow: hidden;
}

.moon-exchange-skeleton-lines {
    display: flex;
    flex-direction: column;
    gap: 8px;
    flex: 1;
    min-width: 0;
}

.moon-exchange-skeleton-line {
    height: 12px;
    border-radius: 6px;
    background: rgba(238, 219, 241, 0.06);
    position: relative;
    overflow: hidden;
}

.moon-exchange-skeleton-line:nth-child(1) { width: 55%; height: 14px; }
.moon-exchange-skeleton-line:nth-child(2) { width: 75%; }
.moon-exchange-skeleton-line:nth-child(3) { width: 40%; }

.moon-exchange-skeleton-action {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 6px;
}

.moon-exchange-skeleton-btn {
    width: 80px;
    height: 32px;
    border-radius: 8px;
    background: rgba(238, 219, 241, 0.06);
    position: relative;
    overflow: hidden;
}

.moon-exchange-skeleton-price {
    width: 60px;
    height: 10px;
    border-radius: 6px;
    background: rgba(238, 219, 241, 0.04);
}

/* Shimmer sweep on skeleton elements */
.moon-exchange-skeleton-thumb::after,
.moon-exchange-skeleton-line::after,
.moon-exchange-skeleton-btn::after {
    content: '';
    position: absolute;
    top: 0;
    left: -100%;
    width: 100%;
    height: 100%;
    background: linear-gradient(
        90deg,
        transparent 0%,
        rgba(238, 219, 241, 0.06) 40%,
        rgba(238, 219, 241, 0.1) 50%,
        rgba(238, 219, 241, 0.06) 60%,
        transparent 100%
    );
    animation: me-shimmer 1.8s ease-in-out infinite;
}

.moon-exchange-skeleton:nth-child(2) .moon-exchange-skeleton-thumb::after,
.moon-exchange-skeleton:nth-child(2) .moon-exchange-skeleton-line::after,
.moon-exchange-skeleton:nth-child(2) .moon-exchange-skeleton-btn::after {
    animation-delay: 0.15s;
}
.moon-exchange-skeleton:nth-child(3) .moon-exchange-skeleton-thumb::after,
.moon-exchange-skeleton:nth-child(3) .moon-exchange-skeleton-line::after,
.moon-exchange-skeleton:nth-child(3) .moon-exchange-skeleton-btn::after {
    animation-delay: 0.3s;
}
.moon-exchange-skeleton:nth-child(4) .moon-exchange-skeleton-thumb::after,
.moon-exchange-skeleton:nth-child(4) .moon-exchange-skeleton-line::after,
.moon-exchange-skeleton:nth-child(4) .moon-exchange-skeleton-btn::after {
    animation-delay: 0.45s;
}

@keyframes me-shimmer {
    0% { left: -100%; }
    100% { left: 100%; }
}

@keyframes me-skeleton-fade-in {
    from { opacity: 0; }
    to { opacity: 1; }
}

/* Smooth transition when real listings replace skeletons */
.moon-exchange-listings.me-loaded {
    animation: me-listings-reveal 0.3s ease-out;
}

@keyframes me-listings-reveal {
    from { opacity: 0; }
    to { opacity: 1; }
}

.moon-exchange-listing {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    gap: 8px;
    padding: 4px 8px;
    border-radius: 12px;
    background: rgba(8, 6, 20, 0.6);
    border: 1px solid rgba(238, 219, 241, 0.2);
    overflow: visible;
}

.moon-exchange-listing-mine {
    border-style: dashed;
    border-color: rgba(255, 215, 130, 0.45);
    background: rgba(34, 25, 21, 0.58);
}

/* Personal listings need only their total and cancellation action. */
.moon-exchange-listing-mine .moon-exchange-buy {
    /* Your own listing has no quantity field, so the three areas simply stack.
       Wider than the buy row's clamp because "Cancel listing" and "Share
       listing" are both full labels here and several languages are longer. */
    grid-template-areas:
        "total"
        "action"
        "share";
    grid-template-columns: minmax(0, 1fr);
    width: clamp(120px, 10vw, 140px);
}

.moon-exchange-card {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
    flex: 1;
}

.moon-exchange-card-preview {
    width: 66px;
    height: 118px;
    flex: 0 0 auto;
    display: grid;
    place-items: center;
    position: relative;
    overflow: hidden;
    cursor: pointer;
    /* Capture the ambient unit — see the sell preview above. */
    --exchange-card-unit: var(--card-unit);
}

.moon-exchange-card-preview .deck-builder-card {
    --moon-exchange-card-scale: 0.5;
    --card-unit: calc(var(--exchange-card-unit, 1vmin) * var(--moon-exchange-card-scale));
    --card-width: calc(12.29 * var(--card-unit));
    --card-height: calc(22.2 * var(--card-unit));
    border-width: calc(1px * var(--moon-exchange-card-scale));
    box-shadow: 0 0 calc(8px * var(--moon-exchange-card-scale)) rgba(238, 219, 241, 0.5);
    /* Centering stays a transform; only the SIZE stopped being one. */
    --card-base-transform: translate(-50%, -50%);
    position: absolute;
    left: 50%;
    top: 50%;
    margin: 0;
    pointer-events: none;
    transform-origin: center;
}

.moon-exchange-card-image {
    width: 56px;
    height: 76px;
    object-fit: cover;
    border-radius: 8px;
    border: 1px solid rgba(255, 255, 255, 0.15);
    box-shadow: 0 8px 18px rgba(0, 0, 0, 0.35);
}

.moon-exchange-card-info {
    min-width: 0;
    overflow: visible;
}

.moon-exchange-card-name {
    font-size: 0.95rem;
    font-weight: 600;
    color: #f4e6ff;
    margin-bottom: 4px;
    white-space: normal;
    overflow-wrap: anywhere;
}

.moon-exchange-card-meta,
.moon-exchange-card-seller {
    font-size: 0.78rem;
    color: rgba(238, 219, 241, 0.7);
    white-space: normal;
    overflow-wrap: anywhere;
}

/* Real-money equivalent. Its own line so a longer currency string cannot push
   the price and remaining-count apart, and empty (rate not loaded) collapses to
   nothing rather than reserving a gap. */
.moon-exchange-card-money {
    font-size: 0.72rem;
    color: rgba(238, 219, 241, 0.52);
    white-space: normal;
    overflow-wrap: anywhere;
}

.moon-exchange-card-money:empty {
    display: none;
}

.collection-craft-money-note {
    flex-basis: 100%;
    margin: 2px 0 0;
    font-size: 0.72rem;
    color: rgba(238, 219, 241, 0.55);
    text-align: center;
}

.moon-exchange-buy {
    display: grid;
    /* Share has its OWN CELL. `.moon-exchange-buy .btn` assigns
       `grid-area: action`, so a second button with no cell of its own painted
       ON TOP of Buy — reported as "share listing button overlaps other button".
       It is a ROW rather than a third column: a third column takes its width
       out of the card side, whose name/meta/seller lines then wrap and make the
       row taller, while a row costs nothing at all here because the 118px card
       preview already sets the row height and qty+action+share only reach
       ~108px. The actions column is narrower than the three-column version was,
       so the card side gained width rather than losing it. */
    grid-template-areas:
        "total total"
        "qty action"
        "share share";
    grid-template-columns: 54px minmax(72px, auto);
    align-items: end;
    gap: 3px 6px;
    min-width: 0;
}

/* BUY, BUT NOT YET.
   A locked account browses the Exchange on purpose — seeing what it is, is how
   a player learns why the matches are worth finishing. So the row renders in
   full and only the action is closed, and it has to LOOK closed rather than
   merely fail when pressed: a plain `disabled` button is easy to read as a bug.
   The padlock and the dimmed, dashed edge say "not yours yet" instead of
   "broken", and `cursor: not-allowed` answers the press before it happens.
   The sentence naming the condition rides on `title` and the accessible name,
   so this is never a colour-only message. The SERVER is what actually refuses;
   this is the explanation. */
.moon-exchange-buy-btn--locked,
.moon-exchange-buy-btn--locked:disabled {
    opacity: 0.55;
    cursor: not-allowed;
    border-style: dashed;
    filter: saturate(0.4);
    /* No hover lift and no press: a control that cannot act must not pretend
       to be interactive. */
    transform: none;
    box-shadow: none;
}

.moon-exchange-buy-btn--locked:hover,
.moon-exchange-buy-btn--locked:focus-visible {
    transform: none;
    box-shadow: none;
}

.moon-exchange-buy-qty {
    grid-area: qty;
    width: 54px;
    min-height: 44px;
    text-align: center;
}

.moon-exchange-buy-total {
    grid-area: total;
    font-size: 0.75rem;
    color: rgba(238, 219, 241, 0.75);
    text-align: right;
    white-space: normal;
    overflow-wrap: anywhere;
}

/* Its own cell, and specific enough to beat the `.btn` rule below it. */
.moon-exchange-buy .moon-exchange-share {
    grid-area: share;
    min-height: 44px;
    min-width: 44px;
    /* Wraps rather than clips. Every language sets this label and several are
       longer than English; `exchange.real.spec.js` forbids clipped translated
       text outright and is right to. */
    white-space: normal;
    /* `.deck-btn` carries `margin: 4px 0`, which turned a 44px control into a
       52px row inside a lane that allots 50px per listing — every card and
       control then spilled out of the lane at 844x280. The grid spaces this
       row with `gap`; the button must not also space itself. */
    margin: 0;
    padding-inline: 6px;
    font-size: 0.78rem;
    line-height: 1.15;
}

.moon-exchange-buy .btn {
    grid-area: action;
    min-width: 0;
    min-height: 44px;
    white-space: normal;
    /* `.deck-btn` carries `margin: 4px 0`, which is EIGHT PIXELS of column
       height inside a grid that already spaces its rows with `gap` — and the
       actions column has to stay under the 118px card preview or it, not the
       card, sets the row height. Measured: with the margin the column reached
       121px at 1920x1080 and every row grew to 131px, which put a fourth
       listing 19px past the bottom of a lane the pager had sized for four.
       The touch target is untouched: the button stays 44px, only its outside
       margin goes. The landscape tier below already does this, for the same
       reason at a smaller size. */
    margin: 0;
}

.collection-section-packs {
    display: flex;
    flex-direction: column;
    gap: 16px;
    flex: 1;
    min-height: 0;
    overflow: hidden;
    padding-bottom: 18px;
}

.card-packs-shell {
    --packs-bg: #0a0e17;
    --packs-ink: #0f1523;
    --packs-gold: #e6bf6a;
    --packs-teal: #26b5a4;
    --packs-text: rgba(247, 240, 223, 0.9);
    --packs-muted: rgba(247, 240, 223, 0.6);
    --packs-border: rgba(230, 191, 106, 0.25);
    --packs-stellar: rgba(176, 132, 255, 0.9);
    display: grid;
    grid-template-rows: auto minmax(280px, 1fr) auto;
    gap: 14px;
    flex: 1;
    width: 100%;
    min-width: 0;
    min-height: 0;
    padding: 16px;
    border-radius: 18px;
    background:
        radial-gradient(circle at 12% 18%, rgba(38, 181, 164, 0.18), transparent 45%),
        radial-gradient(circle at 82% 20%, rgba(230, 191, 106, 0.18), transparent 46%),
        linear-gradient(160deg, rgba(12, 18, 30, 0.98), rgba(8, 10, 16, 0.98));
    border: 1px solid var(--packs-border);
    position: relative;
    /* The oversized texture layer uses inset:-30%. Keep that paint inside the
       rounded vault instead of letting it enlarge the Collection workspace's
       scrollable overflow area. The actual card reveal is already contained
       by .card-packs-stage. */
    overflow: hidden;
}

.card-packs-shell::before {
    content: "";
    position: absolute;
    inset: 0;
    background: radial-gradient(circle at 20% 10%, rgba(255, 255, 255, 0.08), transparent 40%);
    opacity: 0.5;
    pointer-events: none;
}

.card-packs-shell::after {
    content: "";
    position: absolute;
    inset: -30%;
    background: repeating-linear-gradient(120deg, rgba(255, 255, 255, 0.03) 0 1px, transparent 1px 70px);
    opacity: 0.35;
    pointer-events: none;
    transform: rotate(-3deg);
}

.card-packs-header {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    align-items: center;
    gap: 12px;
    position: relative;
    z-index: 1;
}

.card-packs-title {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.card-packs-eyebrow {
    text-transform: uppercase;
    letter-spacing: 0.2em;
    font-size: 0.62rem;
    color: rgba(230, 191, 106, 0.85);
    font-family: "Cinzel", serif;
}

.card-packs-title h3 {
    margin: 0;
    font-size: 1.2rem;
    color: var(--packs-text);
    font-family: "Cinzel", serif;
}

.card-packs-subtitle {
    margin: 0;
    font-size: 0.78rem;
    color: var(--packs-muted);
    font-family: "Spectral", serif;
}

/* Published loot-box odds. This is a legal disclosure,

not flavor: it is the
 * one line in the header that no viewport is allowed to hide,

so it stays
 * short and keeps its own quiet weight next to the Buy button. */
.card-packs-odds{
    margin: 0;
    font-size: 0.72rem;
    letter-spacing: 0.04em;
    font-family: "Spectral", serif;
}

.card-packs-odds {
    /* Its own full-width row beneath the title and the controls: squeezed
       between them as a flex sibling it wrapped mid-phrase. */
    flex: 0 0 100%;
    order: 3;
    color: rgba(230, 191, 106, 0.72);
}

/* Development-only banner sitting under the published odds. It must read as an
   obviously abnormal state — a debug port whose rate is NOT the real one — so
   it deliberately breaks the header's gold palette instead of blending in. */
.card-packs-debug-odds {
    flex: 0 0 100%;
    order: 4;
    margin: 4px 0 0;
    padding: 3px 10px;
    border: 1px dashed rgba(255, 128, 168, 0.55);
    border-radius: 999px;
    align-self: flex-start;
    font-size: 0.68rem;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    font-family: "Spectral", serif;
    color: rgba(255, 168, 196, 0.92);
    background: rgba(120, 20, 60, 0.22);
}

.card-packs-debug-odds[hidden] {
    display: none;
}

.card-packs-header-actions {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
    justify-content: flex-end;
}

.card-packs-count {
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.18em;
    color: rgba(247, 240, 223, 0.8);
    border: 1px solid rgba(230, 191, 106, 0.35);
    border-radius: 999px;
    padding: 6px 12px;
    background: rgba(12, 18, 28, 0.7);
}

.card-packs-duplicates {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 6px;
    padding: 6px 10px;
    border-radius: 12px;
    border: 1px solid rgba(230, 191, 106, 0.25);
    background: rgba(12, 18, 28, 0.75);
    color: rgba(247, 240, 223, 0.85);
}

.card-packs-duplicates-btn {
    flex: 0 0 auto;
    padding: 6px 12px;
    font-size: 0.72rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    border-radius: 999px;
    background: linear-gradient(135deg, #62e0c9, #26b5a4);
    color: #071b19;
    border: 1px solid rgba(151, 255, 235, 0.9);
    box-shadow: 0 6px 14px rgba(38, 181, 164, 0.24);
    font-weight: 800;
    /* One line on desktop — the short-landscape media block below opts back
       into wrapping where horizontal space genuinely runs out. */
    white-space: nowrap;
}

.card-packs-duplicates-btn:hover {
    filter: brightness(1.08);
}

.card-packs-duplicates-btn:disabled {
    opacity: 0.68;
    cursor: not-allowed;
    box-shadow: none;
}

.card-packs-buy-btn {
    background: linear-gradient(140deg, rgba(230, 191, 106, 0.95), rgba(214, 130, 52, 0.95));
    color: #22160b;
    border: none;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    box-shadow: 0 12px 20px rgba(214, 130, 52, 0.25);
    white-space: nowrap;
}

.card-packs-buy-btn:hover {
    filter: brightness(1.08);
}

.card-packs-buy-btn:disabled {
    filter: grayscale(0.6) brightness(0.85);
    box-shadow: none;
}

/* Desktop: one exact control axis. The duplicates box dissolves into the
   grid (display: contents) so the Exchange and Buy buttons sit on the SAME
   row and midline. The exchange rules span both action tracks so their long
   localized copy can never widen the Exchange button. Its compact width and
   left edge align naturally beneath that control, without adding decorative
   chrome. Phone/short-landscape media blocks below keep their stacked layouts. */
@media (min-width: 769px) and (min-height: 561px) {
    .card-packs-header-actions {
        display: grid;
        grid-template-columns: auto auto auto;
        grid-template-rows: auto auto auto;
        align-items: center;
        column-gap: 14px;
        row-gap: 4px;
        justify-content: end;
    }

    .card-packs-count {
        grid-column: 1;
        grid-row: 2;
    }

    .card-packs-duplicates {
        display: contents;
    }

    .card-packs-duplicates-btn {
        grid-column: 2;
        grid-row: 2;
        justify-self: stretch;
    }

    .card-packs-buy-btn {
        grid-column: 3;
        grid-row: 2;
    }

    /* Identical control metrics: same height, type scale, and pill radius. */
    .card-packs-duplicates-btn,
    .card-packs-buy-btn {
        height: 40px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        font-size: 0.75rem;
        border-radius: 999px;
        padding: 0 18px;
    }
}

.card-packs-stage {
    /* Shared height budget: the sealed wrapper sizes itself from this same
       variable so it can never outgrow the stage that clips it. */
    --pack-stage-h: clamp(300px, 50svh, 430px);
    /* The hint owns a lane beneath the wrapper; without it the line printed
       straight over the pack's crimped bottom edge. It must hold the hint's
       TALLEST rendering — two lines of Spectral at 0.8rem is 38px — plus the
       offset the hint is anchored at, or the line climbs back into the
       wrapper. 44px = 38 + 4 of anchor + 2 of slack. */
    --pack-hint-lane: 44px;
    /* One period for the whole idle body language. The lift, the foil sheen
       travelling across the wrapper, and the ground contact all run on this, so
       the pack reads as ONE object catching one light rather than three effects
       stacked on a rectangle. Declared on the STAGE because the contact shadow
       is the pack's sibling, not its child. */
    --pack-breath-dur: 3.2s;
    padding-bottom: var(--pack-hint-lane);
    position: relative;
    border-radius: 16px;
    background:
        radial-gradient(circle at 50% 0%, rgba(38, 181, 164, 0.22), rgba(11, 15, 24, 0.95) 60%),
        linear-gradient(140deg, rgba(12, 18, 28, 0.95), rgba(8, 10, 16, 0.98));
    border: 1px solid rgba(230, 191, 106, 0.18);
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: var(--pack-stage-h);
    overflow: hidden;
    transition: box-shadow 0.3s ease;
}

/* A cold pack read has no trustworthy physical state yet. Keep the stage's
   exact geometry, but show a neutral moonlit placeholder rather than briefly
   inventing a sealed pack that then flashes back to the already-open fan. */
.card-packs-state-placeholder {
    position: absolute;
    inset: 12% 22%;
    z-index: 5;
    border: 1px solid rgba(238, 219, 241, 0.14);
    border-radius: 24px;
    background:
        /* The sweep rides ON TOP of the neutral fill, so the panel keeps the
           exact colour it had and only gains motion. */
        linear-gradient(100deg, transparent 20%, rgba(226, 210, 255, 0.16) 46%, transparent 72%),
        radial-gradient(circle at 50% 42%, rgba(211, 190, 255, 0.18), transparent 34%),
        linear-gradient(145deg, rgba(67, 49, 94, 0.22), rgba(15, 10, 27, 0.5));
    box-shadow: inset 0 0 32px rgba(211, 190, 255, 0.08);
    opacity: 0;
    pointer-events: none;
    /* IT HAS TO MOVE. A neutral panel that just sits there is what produced
       "the deck sillhouette appears but the asset is not loading instantly,
       looks buggy" — the player cannot tell a slow load from a broken one.
       docs/asset-loading.md law 2: a static placeholder is the same defect with
       a class name on it. Same sweep as the landing card popup, so the two
       waiting states read as one language. */
    background-size: 220% 100%, auto, auto;
    background-repeat: no-repeat;
    animation: card-packs-state-wait 1.25s ease-in-out infinite;
}

@keyframes card-packs-state-wait {
    from { background-position: 130% 0, 0 0, 0 0; }
    to { background-position: -130% 0, 0 0, 0 0; }
}

@media (prefers-reduced-motion: reduce) {
    .card-packs-state-placeholder { animation: none; }
}

#collection-packs-section[data-packs-state-known="false"] :is(.card-packs-pack) {
    visibility: hidden;
}

#collection-packs-section[data-packs-state-known="false"] :is(
    .card-packs-open-btn,
    .card-packs-pack-glow,
    .card-packs-pack-hint,
    .card-packs-cards
) {
    visibility: hidden;
}

#collection-packs-section[data-packs-state-known="false"] .card-packs-state-placeholder {
    opacity: 1;
}

.card-packs-stage::before {
    content: "";
    position: absolute;
    inset: 0;
    background: radial-gradient(circle at 80% 30%, rgba(230, 191, 106, 0.12), transparent 45%);
    opacity: 0.6;
    pointer-events: none;
}
.card-packs-stage::after {
    content: "";
    position: absolute;
    inset: -40%;
    background: radial-gradient(circle, rgba(255, 255, 255, 0.05) 1px, transparent 2px);
    background-size: 80px 80px;
    opacity: 0.3;
    pointer-events: none;
    transform: rotate(18deg);
}
.card-packs-stage.card-packs-stage-opened::after {
    opacity: 0.55;
}
.card-packs-stage.card-packs-stage-opened {
    box-shadow: 0 0 0 1px rgba(230, 191, 106, 0.2), 0 0 50px rgba(230, 191, 106, 0.18);
}

.card-packs-open-btn {
    position: absolute;
    top: 14px;
    right: 14px;
    border-radius: 999px;
    padding: 8px 16px;
    background: rgba(15, 20, 30, 0.75);
    color: var(--packs-text);
    border: 1px solid rgba(38, 181, 164, 0.4);
    text-transform: uppercase;
    letter-spacing: 0.12em;
    font-size: 0.7rem;
    z-index: 2;
}

.card-packs-open-btn:hover {
    border-color: rgba(38, 181, 164, 0.8);
}

.card-packs-pack {
    position: relative;
    /* 460 / 765 is the wrapper art's exact alpha-trimmed ratio; every size
       override keeps it via aspect-ratio. Height-first sizing against the
       stage budget: 34px of clearance absorbs centering plus the idle float
       so the crimped edges are never clipped (1400x640 clipped 23px when the
       wrapper was width-sized).
       The third cap is the load-bearing one: `--pack-stage-h` is derived from
       the VIEWPORT, not from the stage the grid actually handed out, so any
       header that grows a row leaves the wrapper sized for a stage that no
       longer exists and `overflow:hidden` clips it. `100%` is the real stage
       content box (its padding already carries the hint lane). */
    height: min(
        350px,
        calc(var(--pack-stage-h, 430px) - 34px - var(--pack-hint-lane, 38px)),
        calc(100% - 34px)
    );
    width: auto;
    aspect-ratio: 460 / 765;
    /* True-transparency wrapper: the container paints NOTHING — no backing
       rectangle, border, radius, or box shadow. The wrapper silhouette sits
       directly on the stage; depth comes from the art layer's drop-shadow. */
    background: none;
    border: none;
    display: flex;
    align-items: center;
    justify-content: center;
    transform: translateY(-8px);
    transition: transform 0.3s ease, opacity 0.3s ease;
    /* One period for the whole idle body language — declared on the STAGE, so
       the pack and its sibling contact shadow read the same number. */
    animation: packFloat var(--pack-breath-dur, 3.2s) ease-in-out infinite;
    z-index: 1;
    cursor: default;
}

/* Two semantic inventory states, one visual policy. Both are inert because
   there is no charged pack whose seal can be pulled. Keeping the classes
   distinct lets copy and controls explain "none owned" versus "ready to
   charge" without pretending either inventory count is a physical object. */
.card-packs-pack-disabled,
.card-packs-pack-uncharged {
    pointer-events: none;
}

/* No inventory and no active ceremony. */
.card-packs-pack-disabled {
    opacity: 0.4;
    animation: none;
}

/* Owned inventory is available to charge, but it is not yet a staged pack. */
.card-packs-pack-uncharged {
    opacity: 1;
    transition-property: transform;
}

/* Inventory is not a staged object. Keep the stable ceremony geometry and
   moonlit background, but paint no generic wrapper or contact glow until a
   charged pack actually exists. The hint and Charge action remain as the
   intentional empty-state affordances. */
.card-packs-stage-vacant .card-packs-pack,
.card-packs-stage-vacant .card-packs-pack-glow {
    visibility: hidden;
    opacity: 0;
    animation: none;
    pointer-events: none;
}

/* --- The charge beat ----------------------------------------------------
   The one moment in this ceremony the player pays for, and the only one that
   used to have nothing to feel: a status line, then a pack that simply existed
   on the next frame. Everything downstream already had craft (the seal drag
   builds light, the tear strains, the fan lands), so the arrival has to earn
   its place next to them.

   Five beats, all on transform/opacity so the stage stays on the compositor:
     gather   the stage draws breath and contracts — anticipation
     bloom    moonlight spikes and HOLDS for three frames — impact
     arrive   the pack drops past its rest and rolls back — overshoot, off-axis
     contact  its shadow expands LATE, on its own clock — secondary motion
   The rim sweep that would have closed it is deliberately absent: that pseudo
   element already belongs to the drag's charging sweep (--pack-open-progress),
   and two owners of one surface is how a later beat silently eats an earlier
   one. The idle float takes over directly instead.
   ---------------------------------------------------------------------- */

@keyframes packChargeGather {
    0%   { opacity: 0; transform: scale(0.72); }
    /* Contract before release: the stage pulls in, then lets go. */
    45%  { opacity: 0.85; transform: scale(0.88); }
    100% { opacity: 0; transform: scale(1.9); }
}

@keyframes packChargeBloom {
    0%   { opacity: 0; transform: scale(0.4); }
    18%  { opacity: 1; transform: scale(1.18); }
    /* The held frame. Impact is a pause, not a curve. */
    30%  { opacity: 1; transform: scale(1.18); }
    100% { opacity: 0; transform: scale(2.1); }
}

@keyframes packChargeArrive {
    0%   { opacity: 0; transform: translate3d(0, -34%, 0) scale(0.86) rotate(-7deg); }
    38%  { opacity: 1; transform: translate3d(0, 4%, 0) scale(1.09) rotate(3.2deg); }
    /* Squash on landing, then roll back through the axis rather than onto it. */
    58%  { transform: translate3d(0, 1%, 0) scale(0.975) rotate(-1.4deg); }
    78%  { transform: translate3d(0, 0, 0) scale(1.018) rotate(0.6deg); }
    100% { opacity: 1; transform: translate3d(0, 0, 0) scale(1) rotate(0deg); }
}

@keyframes packChargeContact {
    0%   { opacity: 0; transform: scale(0.55); }
    100% { opacity: 1; transform: scale(1); }
}

.card-packs-charging::before,
.card-packs-charging::after {
    animation: none;
}

/* Overshoot needs room past the settle point, which `ease-out` cannot give. */
.card-packs-charging .card-packs-pack.card-packs-pack-charging {
    animation: packChargeArrive 620ms cubic-bezier(0.22, 1.24, 0.32, 1) 240ms both;
    /* The phone tiers pin an idle lift here; the arrival owns the transform. */
    transform: none;
    scale: 1;
}

/* 120ms behind the pack: the shadow is a separate object with its own weight,
   and the lag is the whole reason the drop reads as a landing. */
.card-packs-charging .card-packs-pack-glow {
    animation: packChargeContact 340ms cubic-bezier(0.2, 0.7, 0.3, 1) 360ms both;
}

/* Reduced motion keeps the EVENT and drops the travel. Removing the beat
   entirely would make a paid action look like nothing happened. */
@media (prefers-reduced-motion: reduce) {
    .card-packs-charging::before,
    .card-packs-charging::after,
    .card-packs-charging .card-packs-pack.card-packs-pack-charging,
    .card-packs-charging .card-packs-pack-glow {
        animation: none;
    }

    .card-packs-charging .card-packs-pack.card-packs-pack-charging {
        opacity: 1;
        filter: brightness(1.28) drop-shadow(0 0 14px rgba(214, 200, 255, 0.6));
        transition: filter 420ms ease-out;
    }

    .card-packs-charging .card-packs-pack-glow {
        opacity: 1;
    }
}

/* --- Pack opening experience ------------------------------------------- */

/* Drag build-up: the pack charges with light as the seal travels; near the
   tear it strains. Driven per-frame by --pack-open-progress (0..1). */
.card-packs-pack-dragging {
    animation: none;
    /* Anticipation. Foil under tension necks IN before it splits, so the pull
       narrows the wrapper as it charges. The `scale` property is used rather
       than folding this into `transform` so the base lift, the nudge and the
       tear keyframes keep sole ownership of that slot.
       Vertical is pinned at 1 on purpose: the short-landscape stage hands the
       pack zero clearance, and any factor above 1 would push the crimped edge
       straight back into `overflow:hidden`. */
    scale: calc(1 - var(--pack-open-progress, 0) * 0.028) 1;
}

/* The charge light follows the BAG'S SILHOUETTE.
   `box-shadow` on the wrapper drew a glowing rectangle around an element that
   paints nothing — a lit box floating behind a crimped foil pouch, with visible
   corners where the pack has none. `drop-shadow` reads the art's real alpha, so
   the light hugs the crimps and the notches. Both shadows live in one filter
   because a second `filter` declaration would replace the first. */
.card-packs-pack-dragging .card-packs-pack-art {
    filter:
        drop-shadow(0 18px 22px rgba(0, 0, 0, 0.5))
        drop-shadow(0 0 calc(6px + var(--pack-open-progress, 0) * 22px)
            rgba(246, 226, 122, calc(0.18 + var(--pack-open-progress, 0) * 0.62)));
}

/* The stress lives at the SEAM, not on the whole pack.
   `filter: brightness(1.22)` on the art bleached the crescent, the filigree and
   the crimson seal at exactly the moment the player is staring at them — the
   pack got PALER the closer it came to opening, which is the opposite of
   material under load. Foil whitens along the line it is about to fail on, so
   that is the only place this brightens, and it brightens with the pull.
   Masked to the art's alpha like every other light on this wrapper. */
.card-packs-pack-dragging .card-packs-pack-body::after {
    content: "";
    position: absolute;
    inset: 0;
    /* A LENS, not a strip. Foil under tension does not whiten evenly along its
       whole length — it gives where the load concentrates, and the ribbon is
       pulling from the middle of the seam. A full-height band also ran over the
       crimped bottom edge and read as a lamp behind the pack. The ellipse dies
       well inside the silhouette in both axes, so this needs no alpha mask of
       its own (one fewer compositing layer during the drag) and can never light
       the transparent crimp notches. */
    background: radial-gradient(ellipse 9% 56% at 50% 48%,
            rgba(255, 255, 255, calc(var(--pack-open-progress, 0) * 0.95)) 0%,
            rgba(255, 248, 224, calc(var(--pack-open-progress, 0) * 0.42)) 38%,
            transparent 88%);
    mix-blend-mode: screen;
    pointer-events: none;
    z-index: 2;
}

/* The last of the travel, where the seal is about to give: a fast, tiny
   shiver that says the material is at its limit. Amplitude stays under a
   pixel and a half so it never reads as a glitch. */
.card-packs-pack-straining .card-packs-pack-art {
    animation: packSealStrain 0.11s linear infinite;
}

@keyframes packSealStrain {
    0%, 100% { transform: translateX(0); }
    25% { transform: translateX(-1.2px); }
    75% { transform: translateX(1.2px); }
}

/* While the halves fly, the pack holds full presence; the opened dim state
   lands only after the one-shot tear completes. */
/* During the tear the wrapper must stay on screen so the halves can shear
   apart — but it has to be GONE by the time the tear class is dropped. Holding
   opacity:1 flat meant that, the moment JS removed `-tearing`, the intact pack
   was revealed at full opacity and only then began its 0.3s fade: the "whole
   pack flashes back after the halves detach". Fading inside the tear leaves
   nothing to reveal. */
.card-packs-pack-tearing.card-packs-pack-opened {
    visibility: visible;
    animation: card-packs-tear-vanish 0.68s ease-out forwards;
}

@keyframes card-packs-tear-vanish {
    0%, 55% { opacity: 1; }
    100% { opacity: 0; }
}

/* The tear moment: light escaping along the SEAM.
   The flash used to be a radial disc inset -4% — a circle covering the whole
   wrapper, so the one thing the player was looking at (the crescent, the
   filigree, the halves shearing apart) was bleached out by the effect meant to
   celebrate it. The rip is a vertical line down the middle of the pack, so the
   flash is a vertical line too: narrow enough that the art either side of it
   stays legible, and stretched in X as the seam opens. */
.card-packs-pack-tearing::after {
    content: "";
    position: absolute;
    /* Inside the pack's own height, and shaped by ONE gradient.
       Two earlier shapes were wrong in the same way. A `radial-gradient(circle)`
       at `inset: -4%` covered the entire wrapper and bleached out the halves it
       was celebrating. Replacing it with a linear band plus a radial MASK cost a
       whole extra compositing layer (ablation: the flash was 4 of the tear's 7
       long frames, and the mask was most of that) — and the mask's Y radius was
       62% of its own box, i.e. PAST the box, so it faded nothing and the flash
       rendered as a hard-edged column poking out above and below the pack: a
       fluorescent tube, not light escaping a rip.
       The element is already narrow, so an ellipse whose radii BOTH fall short
       of it shapes the light in both axes at once: brightest along the seam,
       dying out toward the crimps and toward the art either side. */
    inset: 1% 30%;
    background: radial-gradient(ellipse 62% 50% at 50% 50%,
            #fffdf3 0%,
            rgba(255, 248, 224, 0.72) 26%,
            rgba(246, 226, 122, 0.2) 62%,
            transparent 92%);
    /* `screen` is what makes this read as light escaping rather than as paint
       laid over the art. It costs about two long frames in the ablation and it
       is worth them: this is the moment. */
    mix-blend-mode: screen;
    /* Short. On a near-black stage `screen` keeps even a 20%-opacity column
       plainly visible, so a 420ms flash left a soft white beam standing in the
       middle of the stage long after the halves had tumbled out of it — a
       lightsaber where the pack used to be. The rip is an event, not a lamp:
       it is over before the debris is. */
    animation: card-packs-tear-flash 0.24s ease-out forwards;
    pointer-events: none;
    z-index: 4;
}

/* An impact frame, not a bloom. The old curve took 99ms to reach full — long
   enough to read as a light turning on. Contact now lands inside three frames
   and the rest of the clip is falloff, which is what makes the moment feel
   struck rather than lit. The scale is asymmetric on purpose: the seam WIDENS
   as the halves part, it does not inflate. */
@keyframes card-packs-tear-flash {
    0% {
        opacity: 0;
        transform: scale(0.24, 0.86);
    }

    7% {
        opacity: 1;
        transform: scale(0.62, 1.02);
    }

    /* The hold: two frames at full before anything recovers. */
    12% {
        opacity: 1;
        transform: scale(0.86, 1.04);
    }

    /* Nearly all of the falloff inside the first third: what the eye keeps is
       the contact, not the glow after it. */
    38% {
        opacity: 0.2;
        transform: scale(1.5, 1.02);
    }

    100% {
        opacity: 0;
        transform: scale(2.3, 0.98);
    }
}

/* Behind the halves, the pack's own dark interior opening along the seam.
   Without it the rip parted onto the stage and the wrapper read as a decal
   splitting rather than a bag tearing. Painted from the wrapper's ::before, so
   it sits UNDER the art halves in DOM paint order and can never wash them out;
   gone by the time they are clear of it. */
.card-packs-pack-tearing::before {
    content: "";
    position: absolute;
    inset: 5% 45%;
    background: linear-gradient(90deg,
            rgba(6, 8, 14, 0) 0%,
            rgba(6, 8, 14, 0.9) 32%,
            rgba(2, 3, 6, 0.98) 50%,
            rgba(6, 8, 14, 0.9) 68%,
            rgba(6, 8, 14, 0) 100%);
    animation: packTearVoid 0.5s cubic-bezier(0.1, 0.72, 0.28, 1) forwards;
    pointer-events: none;
    z-index: 0;
}

@keyframes packTearVoid {
    0% {
        opacity: 0;
        transform: scaleX(0.12);
    }

    14% {
        opacity: 1;
        transform: scaleX(0.8);
    }

    100% {
        opacity: 0;
        transform: scaleX(3.2);
    }
}

/* The ribbon is attached to the right-hand half, so it goes where that half
   goes. It used to hang motionless in mid-air while the pack it was fastened
   to was flung apart underneath it — the single clearest tell that the tear
   was a stack of effects rather than one object coming apart.
   `transform-origin: top center` (from the base rule) is what makes it whip
   from its anchor rather than slide. */
.card-packs-pack-tearing .card-packs-pack-seal {
    animation: packSealFling 0.62s cubic-bezier(0.1, 0.72, 0.28, 1) forwards;
    transition: none;
    pointer-events: none;
}

@keyframes packSealFling {
    0% {
        transform: translate(0, 0) rotate(0deg);
        opacity: 1;
    }

    /* It snaps taut before it travels — the same beat the halves split on. */
    14% {
        transform: translate(26%, -6%) rotate(14deg);
        opacity: 1;
    }

    100% {
        transform: translate(190%, 86%) rotate(58deg);
        opacity: 0;
    }
}

/* TORN FOIL, not sparks.
   The tear used to borrow `UI.FX.burst` — the shared button flourish, which
   lays its particles at `cos(i/count * 2PI)`, i.e. perfectly evenly around a
   circle. Two calls therefore drew two concentric rings of identical round
   dots, expanding in lockstep and dying on the same frame: a firework, on a
   moment that is supposed to be a foil bag being ripped in half.
   These are shards. Every one is a different sliver at a different angle,
   thrown a different distance at a different speed, spinning a different way,
   and dying at its own time — and they ARC, because the only thing that makes
   debris read as debris is that it falls.
   The container is positioned by JS at the rip; the shards are its children.
   Each carries its own --sx/--sy/--fall/--spin/--tone from JS, so the whole
   burst is one CSS animation per shard and no per-frame JS at all. */
.card-packs-foil-burst {
    position: absolute;
    width: 0;
    height: 0;
    pointer-events: none;
    z-index: 5;
}

.card-packs-foil-shard {
    position: absolute;
    left: 0;
    top: 0;
    width: var(--shard-w, 3px);
    height: var(--shard-h, 9px);
    /* A torn scrap of foil catches light across its width: bright edge, darker
       body. Flat fills would read as confetti. */
    background: linear-gradient(112deg,
            var(--shard-hi, rgba(255, 250, 226, 0.98)) 0%,
            var(--shard-lo, rgba(214, 173, 78, 0.92)) 62%,
            rgba(120, 92, 34, 0.86) 100%);
    border-radius: 1px;
    will-change: transform, opacity;
    animation: packFoilShard var(--shard-dur, 640ms) var(--shard-delay, 0ms) forwards;
}

@keyframes packFoilShard {
    0% {
        /* Out of the rip fast, then decelerating — the throw. */
        animation-timing-function: cubic-bezier(0.05, 0.78, 0.28, 1);
        transform: translate3d(0, 0, 0) rotate(0deg) scale(1);
        opacity: 1;
    }

    /* Apex. From here gravity owns it, so the curve flips to accelerating. */
    54% {
        animation-timing-function: cubic-bezier(0.42, 0, 0.78, 0.92);
        transform:
            translate3d(calc(var(--sx, 0px) * 0.82), var(--sy, 0px), 0) rotate(calc(var(--spin, 240deg) * 0.58)) scale(0.92);
        opacity: 1;
    }

    100% {
        transform:
            translate3d(var(--sx, 0px), calc(var(--sy, 0px) + var(--fall, 90px)), 0) rotate(var(--spin, 240deg)) scale(0.4);
        opacity: 0;
    }
}

/* The stage answers the tear once — a bloom proportional to the event, on the
   surface the pack is sitting on. Applied by JS for the duration of the tear
   so it can never latch on a surface that outlives the moment.
   THE LIGHT IS PAINTED ONCE AND ONLY ITS OPACITY MOVES. The bloom used to
   animate the STAGE's own `box-shadow`, sweeping its blur radius from 50px to
   120px and back. A blur radius that changes every frame cannot be composited:
   the browser re-rasterizes a full-width blurred shadow on every one of them,
   and a layer-by-layer ablation measured this as the single largest frame cost
   in the whole ceremony — 16 frames over 24ms during the tear, 9 with this one
   effect removed and 1 with the ceremony switched off entirely. Sitting behind
   the pack (z-index 0) it lights the SURFACE rather than washing the subject,
   which is what the box-shadow on the stage did too. */
.card-packs-stage-flare {
    position: absolute;
    inset: 0;
    border-radius: inherit;
    pointer-events: none;
    opacity: 0;
    z-index: 0;
    box-shadow: inset 0 0 0 1px rgba(255, 246, 214, 0.9);
    background: radial-gradient(ellipse 62% 92% at 50% 46%,
            rgba(246, 226, 122, 0.32),
            rgba(246, 226, 122, 0.09) 44%,
            transparent 78%);
}

.card-packs-stage-tearing .card-packs-stage-flare {
    animation: packStageBloom 0.72s ease-out;
}

@keyframes packStageBloom {
    0% { opacity: 0; }
    12% { opacity: 1; }
    100% { opacity: 0; }
}

/* Click nudge: the pack refuses a plain click with a small headshake so the
   player learns the seal wants to be dragged. */
.card-packs-pack-nudge {
    animation: card-packs-pack-nudge 0.5s ease;
}

@keyframes card-packs-pack-nudge {

    0%,
    100% {
        transform: translateY(-8px) rotate(0deg);
    }

    25% {
        transform: translateY(-8px) rotate(-2.2deg);
    }

    55% {
        transform: translateY(-8px) rotate(2.2deg);
    }

    80% {
        transform: translateY(-8px) rotate(-1deg);
    }
}

.card-packs-pack-hint-emphasis {
    color: #f6e27a;
    text-shadow: 0 0 12px rgba(246, 226, 122, 0.65);
    transform: scale(1.06);
    transition: transform 0.2s ease, color 0.2s ease;
}

/* Affordable buy button breathes so the purchase invites itself. */
.card-packs-buy-btn.card-packs-buy-ready {
    animation: card-packs-buy-pulse 2.2s ease-in-out infinite;
}

@keyframes card-packs-buy-pulse {

    0%,
    100% {
        box-shadow: 0 0 0 rgba(246, 226, 122, 0);
    }

    50% {
        box-shadow: 0 0 18px rgba(246, 226, 122, 0.55);
    }
}

/* Once claiming starts the pack leaves the scene entirely — a dimmed ghost
   behind the reveal fan reads as clutter. The tear plays first (the tearing
   override below keeps it fully present), then this rest state fades the
   wrapper out and removes it from hit-testing and paint. */
.card-packs-pack-opened {
    opacity: 0;
    visibility: hidden;
    transform: translateY(-4px) scale(0.95);
    animation: none;
    transition: opacity 0.3s ease, transform 0.3s ease, visibility 0s linear 0.3s;
}

/* Once the tear class leaves, make the intact wrapper physically impossible
   to repaint during a visibility/opacity transition or a stale style frame.
   The tearing exclusion is load-bearing: its pseudo-elements still carry the
   two real wrapper halves while they fly apart. */
.card-packs-pack-opened:not(.card-packs-pack-tearing) .card-packs-pack-art {
    background: none !important;
    filter: none;
    visibility: hidden;
}

.card-packs-pack-opened:not(.card-packs-pack-tearing) .card-packs-pack-body {
    visibility: hidden;
}

/* The ground contact — and it belongs to the STAGE, not to the pack.
   It used to be a child of `.card-packs-pack`, which meant the pack's own
   `packFloat` transform carried it: the shadow rose and fell WITH the object
   casting it, so a 6px lift changed the object-to-shadow gap by 1.12px. A
   shadow that follows its object is not a shadow, it is a decal, and the float
   read as a wobble because of it. As a sibling of the pack it is pinned to the
   surface, JS gives it the pack's LAYOUT box (offsetTop/offsetWidth ignore
   transforms, so the float cannot leak back in), and the lift now opens a real
   gap. It still breathes in anti-phase — higher means wider and fainter — which
   is the part that reads as height. */
.card-packs-pack-glow {
    position: absolute;
    top: var(--pack-shadow-top, 100%);
    left: 50%;
    width: var(--pack-shadow-width, 60%);
    height: 15px;
    margin-top: 3px;
    transform: translateX(-50%);
    /* Dark and tight where the object nearly touches, thinning fast outward —
       the stage under it is a near-black panel with a teal wash at the top, so
       the contact is warm-black rather than neutral grey and never reads as a
       painted ellipse. */
    background: radial-gradient(ellipse at center,
            rgba(0, 0, 0, 0.66) 0%,
            rgba(4, 6, 10, 0.36) 44%,
            rgba(4, 6, 10, 0) 74%);
    filter: blur(5px);
    pointer-events: none;
    z-index: 0;
    animation: packContactShadow var(--pack-breath-dur, 3.2s) ease-in-out infinite;
}

@keyframes packContactShadow {
    0%, 100% {
        transform: translateX(-50%) scale(1);
        opacity: 0.85;
    }

    50% {
        transform: translateX(-50%) scale(1.14, 0.86);
        opacity: 0.55;
    }
}

/* The foil sheen: a specular band drifting across the sealed wrapper, masked
   to the art's own alpha so it lights the pack and never the transparent
   crimp notches. Same period as the lift and the contact shadow — one object,
   one light. Painted on the BODY (which sits above the art in DOM order) so
   it never competes with the tear pseudo-elements for the art's own slots. */
.card-packs-pack-body::before {
    content: "";
    position: absolute;
    inset: 0;
    /* Narrowed from a 32%-wide band: at its peak the old sheen veiled a third
       of the illustration at once, so the pack looked hazy rather than lit. A
       tighter band reads as a harder, more specular foil and leaves the art
       readable underneath it. */
    background: linear-gradient(108deg,
            transparent 40%,
            rgba(255, 244, 214, 0.44) 47.5%,
            rgba(255, 255, 255, 0.68) 50%,
            rgba(255, 244, 214, 0.44) 52.5%,
            transparent 60%);
    background-size: 300% 100%;
    mix-blend-mode: screen;
    opacity: 0.42;
    -webkit-mask: url('../assets/images/card-pack-front.webp') center / 100% 100% no-repeat;
    mask: url('../assets/images/card-pack-front.webp') center / 100% 100% no-repeat;
    pointer-events: none;
    animation: packFoilSheen var(--pack-breath-dur, 3.2s) ease-in-out infinite;
}

/* The band tracks back and forth rather than looping in one direction: a
   sweep that resets reads as a repeating effect, a band that returns reads as
   a light source the pack is turning under. */
@keyframes packFoilSheen {
    0%, 100% {
        background-position: 118% 0;
        opacity: 0.26;
    }

    50% {
        background-position: -18% 0;
        opacity: 0.5;
    }
}

/* Once the seal is pulled the idle light stops competing with the charge
   sweep, and once torn there is no wrapper left to light.
   The shadow is now the pack's SIBLING, so its state selectors are sibling
   combinators — a descendant selector would silently stop matching and leave a
   shadow under a pack that is no longer there. */
.card-packs-pack-dragging .card-packs-pack-body::before,
.card-packs-pack-tearing .card-packs-pack-body::before,
.card-packs-pack-opened .card-packs-pack-body::before,
.card-packs-pack-disabled .card-packs-pack-body::before,
.card-packs-stage-vacant .card-packs-pack-body::before,
.card-packs-pack-disabled ~ .card-packs-pack-glow,
.card-packs-pack-tearing ~ .card-packs-pack-glow,
.card-packs-pack-opened ~ .card-packs-pack-glow {
    opacity: 0;
    animation: none;
}

/* The generated pack illustration IS the pack. The charging sweep and the
   tear halves are layered over this art (scripts/generatePackArt.js). */
.card-packs-pack-art {
    position: absolute;
    inset: 0;
    /* The asset carries real alpha (chroma-keyed in generatePackArt.js) and
       the container matches its exact ratio, so it renders 1:1 with no crop
       and no letterbox. The drop-shadow follows the crimped silhouette. */
    background: url('../assets/images/card-pack-front.webp') center / 100% 100% no-repeat;
    filter: drop-shadow(0 18px 22px rgba(0, 0, 0, 0.5));
    overflow: visible;
    pointer-events: none;
}

/* Charging light: a foil sheen that sweeps across the art, its reach and
   brightness driven by the live drag progress. Masked to the wrapper's alpha
   so the sweep never lights the transparent crimp notches. */
.card-packs-pack-art::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(115deg,
            transparent 30%,
            rgba(255, 246, 214, 0.55) 50%,
            transparent 70%);
    background-size: 260% 100%;
    background-position: calc(120% - var(--pack-open-progress, 0) * 240%) 0;
    opacity: calc(var(--pack-open-progress, 0) * 0.9);
    mix-blend-mode: screen;
    -webkit-mask: url('../assets/images/card-pack-front.webp') center / 100% 100% no-repeat;
    mask: url('../assets/images/card-pack-front.webp') center / 100% 100% no-repeat;
    pointer-events: none;
}

/* The tear: the artwork splits into two halves that shear apart while the
   flash fires; each half carries the real art, clipped. */
.card-packs-pack-tearing .card-packs-pack-art {
    background: none;
    /* AND NO FILTER. The base rule's `drop-shadow` makes this element a filter
       root: while its two pseudo children animate, the browser re-renders the
       whole subtree into a filter buffer and re-blurs a 22px shadow EVERY
       frame — and then does it again underneath the wrapper's own opacity
       fade. A layer-by-layer ablation put it at 16 long frames (>24ms) during
       the tear with the filter, 10 without, against 1 for the ceremony
       switched off entirely. Debris in mid-air has nothing to cast a ground
       shadow onto in any case. */
    filter: none;
}

.card-packs-pack-tearing .card-packs-pack-art::before,
.card-packs-pack-tearing .card-packs-pack-art::after {
    content: "";
    position: absolute;
    inset: 0;
    background: url('../assets/images/card-pack-front.webp') center / 100% 100% no-repeat;
    -webkit-mask: none;
    mask: none;
    mix-blend-mode: normal;
    opacity: 1;
}

/* Debris leaves a rip at its FASTEST and decelerates — `ease-in` accelerated
   the halves from rest, which read as the pack politely parting. They are now
   flung: hard initial velocity, a tumble that keeps turning after the throw,
   and a slight shrink as they fall away from the light. */
/* TORN, not cut. `inset(0 50% 0 0)` gave the two halves a dead-straight edge —
   the pack looked like it had been opened with scissors, which is the one thing
   a foil wrapper never looks like. The two polygons below walk the SAME list of
   boundary points in opposite directions, so the ragged seam tiles exactly:
   no sliver of overlap, no gap, and a rip that reads as a rip at the moment it
   is most visible. */
.card-packs-pack-tearing .card-packs-pack-art::before {
    clip-path: polygon(0 0, 52% 0, 46% 9%, 53% 19%, 45% 30%, 52% 41%, 44% 52%, 51% 63%, 45% 74%, 52% 85%, 46% 94%, 50% 100%, 0 100%);
    animation: card-packs-tear-left 0.62s cubic-bezier(0.1, 0.72, 0.28, 1) forwards;
}

.card-packs-pack-tearing .card-packs-pack-art::after {
    clip-path: polygon(50% 100%, 46% 94%, 52% 85%, 45% 74%, 51% 63%, 44% 52%, 52% 41%, 45% 30%, 53% 19%, 46% 9%, 52% 0, 100% 0, 100% 100%);
    background-position: center;
    background-size: 100% 100%;
    animation: card-packs-tear-right 0.62s cubic-bezier(0.1, 0.72, 0.28, 1) forwards;
}

@keyframes card-packs-tear-left {
    0% {
        transform: translate(0, 0) rotate(0deg) scale(1);
        opacity: 1;
    }

    /* The split itself: the halves part before they travel, so the eye sees a
       seam open rather than two shapes sliding. */
    14% {
        transform: translate(-19%, -2%) rotate(-8deg) scale(1.03);
        opacity: 1;
    }

    100% {
        transform: translate(-58%, 20%) rotate(-24deg) scale(0.88);
        opacity: 0;
    }
}

@keyframes card-packs-tear-right {
    0% {
        transform: translate(0, 0) rotate(0deg) scale(1);
        opacity: 1;
    }

    14% {
        transform: translate(19%, -2%) rotate(8deg) scale(1.03);
        opacity: 1;
    }

    100% {
        transform: translate(58%, 20%) rotate(24deg) scale(0.88);
        opacity: 0;
    }
}

.card-packs-pack-body {
    position: relative;
    width: 100%;
    height: 100%;
    display: flex;
    align-items: flex-start;
    justify-content: center;
    overflow: visible;
}

/* The seal is the R1 crimson fork ribbon: it hangs from the pack's top-right
   shoulder like a real bookmark ribbon and is pulled DOWNWARD to open. The
   pull ELONGATES the ribbon itself (--seal-stretch, set by JS from the drag
   travel) with a slight pendulum sway toward the pointer (--seal-sway /
   --seal-rot). The PULL label is printed on the ribbon at 84% of its CURRENT
   length, so the drag drags the text down with the material (0.84x the
   stretch) while holding a constant clearance above the fork cavity, which
   the clip-path opens at that same 84%. */
.card-packs-pack-seal {
    position: absolute;
    top: 0;
    right: 20%;
    /* The label inside measures itself against this box. */
    container-type: inline-size;
    /* 30px on the lab's 190px wrapper — the proportion the pick was made at. */
    width: clamp(20px, 15.8%, 34px);

    height: calc(30% + var(--seal-stretch, 0px));
    /* The button itself paints NOTHING and is never clipped: a clip-path on a
       real <button> inside the pack's transform-animated subtree survives
       getComputedStyle but is not always rasterized (the fork vanished on GPU
       Chrome while headless raster drew it fine). The ribbon body lives on a
       plain ::before instead, which composites reliably. */
    background: none;
    border: 0;
    /* main.css gives every <button> `margin: clamp(4px, 0.58vmin, 10px)`, and
       a margin offsets an absolutely positioned box — the ribbon hung 4px
       below the wrapper's top edge at 640px tall and ~6px at 1080p, drifting
       further from the pack the bigger the screen. It hangs from the edge. */
    margin: 0;
    color: #fff4d6;
    font-family: "Cinzel", serif;
    font-size: 0.55rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.85);
    cursor: grab;
    transform: translateX(var(--seal-sway, 0px)) rotate(var(--seal-rot, 0deg));
    transform-origin: top center;
    transition: height 0.3s cubic-bezier(0.2, 1.6, 0.4, 1), transform 0.3s ease;
    padding: 0;
    appearance: none;
    -webkit-appearance: none;
    touch-action: none;
    user-select: none;
    -webkit-user-select: none;
    z-index: 3;
}

/* The ribbon body: crimson silk with the forked tail cut out of it. */
.card-packs-pack-seal::before {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(180deg, #8c2f3f, #5e1622);
    clip-path: polygon(0 0, 100% 0, 100% 100%, 50% 84%, 0 100%);
    pointer-events: none;
}

/* Invisible widened hit area: the ribbon stays visually narrow while the
   interactive surface reaches the 44px touch class. */
.card-packs-pack-seal::after {
    content: "";
    position: absolute;
    inset: -8px -12px;
}

@media (pointer: coarse) {
    .card-packs-pack-seal::after {
        inset: -10px -16px;
    }
}

.card-packs-pack-seal:focus-visible {
    outline: 3px solid rgba(255, 255, 255, 0.95);
    outline-offset: 3px;
}

.card-packs-pack-dragging .card-packs-pack-seal {
    cursor: grabbing;
    transition: none;
}

.card-packs-pack-seal-text {
    pointer-events: none;
    position: absolute;
    left: 0;
    right: 0;
    text-align: center;
    top: max(4px, calc(100% * 0.84 - 22px));
    /* Scale the word to the RIBBON, not to the page. The ribbon is a clamped
       fraction of the pack (20-34px), so on a phone-sized pack a fixed font
       size put "PULL" wider than the ribbon carrying it — reported as "pull
       text on ribbon is overshooting ribbon". cqw measures the ribbon itself,
       so the label shrinks with it and cannot outgrow it. */
    /* A glyph, not a word. Two constraints collided: the label must not
       overshoot a ribbon that is 20px wide by design ("pull text on ribbon is
       overshooting ribbon"), and no text on a phone may render below a
       readable size. Four letters cannot satisfy both in 20px, so the ribbon
       carries a downward arrow — which says "pull" more directly anyway — and
       the word stays as the accessible name for screen readers. The ribbon is
       59px TALL, so the arrow has room to be large. */
    font-size: 0;
    letter-spacing: 0;
    line-height: 1;
    overflow: hidden;
}

.card-packs-pack-seal-text::before {
    content: '\2193';
    display: block;
    font-size: max(15px, 70cqw);
    line-height: 1;
}

.card-packs-pack-hint {
    position: absolute;
    /* The lane below is `--pack-hint-lane` tall and this line is measured from
       the stage's PADDING box, so every pixel of offset here is a pixel the
       hint climbs OUT of its lane and into the wrapper. At 16px a two-line
       hint (38px in Spectral at 0.8rem) needed 54px of a 38px lane and ran
       8.27px under the wrapper at 1400x640 — visible only during part of the
       idle float, which is why it read as a flaky test rather than a defect. */
    bottom: 4px;
    left: 0;
    right: 0;
    text-align: center;
    font-size: 0.8rem;
    color: var(--packs-muted);
    font-family: "Spectral", serif;
    z-index: 2;
    transition: opacity 0.2s ease, transform 0.2s ease;
}

.card-packs-stage.card-packs-stage-opened .card-packs-pack-hint {
    opacity: 0;
    transform: translateY(8px);
}

.card-packs-cards {
    /* One knob sizes the whole reveal fan. PHYSICAL sizing, not a transform
       upscale: the fan's rotated cards raster at their real displayed size,
       so they stay sharp (a transform zoom displayed a layout-size raster
       stretched — permanently soft, the same trap the collection detail
       documents). --card-unit multiplies the same floor expression as
       cards.css's phone block (max(1vmin, 4.6px)), and width/height must be
       redeclared because :root computes them with ITS unit. */
    --pack-card-zoom: 1.4;
    --card-unit: calc(max(1vmin, 4.6px) * var(--pack-card-zoom, 1.4));
    --card-width: calc(12.29 * var(--card-unit));
    --card-height: calc(22.2 * var(--card-unit));
    --card-spread: calc(var(--card-width) * 0.5);
    position: absolute;
    left: 0;
    right: 0;
    bottom: 8px;
    height: calc(var(--card-height) * 1.46);
    pointer-events: none;
    z-index: 2;
    /* NO `perspective` HERE. It lived on this container for a long time and
       bought nothing: `perspective` applies to an element's DIRECT children,
       and this container's children are the `.card-pack-card` wrappers, whose
       own transforms are purely 2D. The element that actually rotates is one
       level deeper, so the flip was projected orthographically — a rectangle
       squashing horizontally, measured at an edge-height ratio of 1.0000 in all
       three engines. The perspective belongs on `.card-pack-card`; see there. */
}

@media (min-width: 1200px) and (min-height: 700px) {
    .card-packs-cards {
        --pack-card-zoom: 1.65;
    }
}

.card-packs-cards .card,
.card-packs-cards .deck-builder-card {
    margin: 0 !important;
}

.card-pack-card .deck-builder-card {
    /* Physically sized by the fan's --card-unit scope — both transform slots
       stay neutral (the shared pipeline MULTIPLIES hover x base, and any
       transform upscale re-blurs the raster). In-place cards only tilt; all
       enlargement belongs to the popup preview. */
    --card-base-transform: scale(1);
    transform-origin: center center;
    /* No transform transition: the shared CardTilt rotation must follow the
       pointer live instead of easing 300ms behind it. */
    transition: filter 0.3s ease;
    pointer-events: none;
}

.card-pack-card:hover .deck-builder-card {
    --card-hover-transform: scale(1);
    --card-base-transform: scale(1);
    filter: none;
}

.card-pack-card {
    position: absolute;
    left: 50%;
    bottom: 0;
    /* THE REST TRANSFORM. packCardFan's 100% frame repeats it exactly; if the
       two ever drift the fan jumps at the moment the animation hands back.
       NOTHING ELSE MAY WRITE HERE. packCardFan fills `forwards`, and a filled
       keyframe is the one place a var() substitution stops being live: Chromium
       re-resolves it when a referenced registered property animates, Firefox and
       WebKit freeze it. The landing pop used to ride this slot through a
       `--card-pop` custom property and therefore rendered at scale 1 on every
       iPhone and every Firefox — running, reporting itself as running, drawing
       nothing. It owns the inner's `scale` now. */
    transform:
        translateX(calc(var(--card-offset, 0) * var(--card-spread))) translateX(-50%) translateY(calc(var(--card-lift, 0px) * -1)) rotate(var(--card-tilt, 0deg));
    opacity: 0;
    /* THE CAMERA FOR THE FLIP. Read by `.card-pack-card-inner`, which consumes
       it as a `perspective()` FUNCTION inside its own transform — see there for
       why the `perspective` PROPERTY cannot be used for this.
       Proportional to the card rather than a fixed pixel distance, so the
       camera sits a constant number of card-widths away and the turn reads with
       the same depth from a phone page box to 4K. At 9.25 card-widths the near
       vertical edge is measured 9.18% taller than the far one at 55 degrees, in
       every engine and at every viewport. */
    --flip-perspective: calc(var(--card-width, 130px) * 9.25);
    /* One knob for the flip, inherited by the 3D inner AND readable by the
       wrapper's own reveal animations, so the landing pop and the Stellar
       aura start exactly when the card finishes turning. */
    --card-flip-dur: 0.6s;
    animation: packCardFan 0.62s cubic-bezier(0.19, 0.86, 0.31, 1) forwards;
    animation-delay: var(--card-delay, 0ms);
    transition: transform 0.25s ease, box-shadow 0.25s ease;
    pointer-events: auto;
    cursor: pointer;
    overflow: visible;
    border: 0;
    padding: 0;
    background: none;
    /* main.css gives every <button> a frosted backdrop blur; on a card-shaped
       button that reads as a permanent halo blurring the neighbouring cards. */
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    box-shadow: none;
    color: inherit;
    font: inherit;
}

.card-pack-card:focus-visible {
    outline: 3px solid rgba(246, 226, 122, 0.95);
    outline-offset: 4px;
}

/* The button tag lifts specificity to (0,2,1) so main.css's global
   button:hover:not([disabled]) chrome (translucent white background + glow)
   cannot repaint the wrapper on hover — or via sticky tap-hover on touch. */
button.card-pack-card:hover {
    transform:
        translateX(calc(var(--card-offset, 0) * var(--card-spread))) translateX(-50%) translateY(calc(var(--card-lift, 0px) * -1)) rotate(0deg);
    z-index: 2000 !important;
    background: none;
    box-shadow: none;
    color: inherit;
}

/* Rarity, spent at the right moment: only AFTER the flip commits. Keying any
   treatment off `data-stellar` before that would tell the player which
   face-down card is the good one, and spend the payoff for free.
   The fan is redeclared alongside it because `animation` is a shorthand —
   naming only the aura here would drop the fan's `forwards` fill and snap the
   revealed card back to opacity 0. */
.card-pack-card-revealed[data-stellar="true"] {
    /* THE FLASH IS ON THE SOUND, NOT ON THE LANDING. `playSound('stellar')`
       fires the instant the reveal is accepted, and this used to wait
       `--card-flip-dur` — 600ms — so the sting landed on a silent screen and
       the light arrived half a second later, reading as lag rather than impact.
       Sound should lead the visual by a frame or two, so 30ms is the whole
       offset. The card is still mid-turn when it blows out, which is correct:
       the energy escapes as the card comes over, and the separate
       `packCardLandStellar` beat still waits for the flip to finish, so the
       moment has a flash AND a landing instead of one late event. */
    animation:
        packCardFan 0.62s cubic-bezier(0.19, 0.86, 0.31, 1) var(--card-delay, 0ms) forwards,
        packStellarAura 1.4s ease-out 30ms both;
}

.card-pack-card-inner {
    position: relative;
    /* --card-width/height are already zoom-scaled by the fan scope. */
    width: var(--card-width);
    height: var(--card-height);
    transform-style: preserve-3d;
    /* THE CARD TURNS IN DEPTH, and the camera has to live in THIS element's own
       transform to do it. Measured edge-height ratio at 55 degrees, near edge
       over far edge (1.0000 == a flat rectangle squashing horizontally, which
       is what shipped: "a window blind closing, not a card turning over"):

         placement                                chromium  firefox  webkit
         `perspective` on .card-packs-cards        1.0000   1.0000   1.0000
         `perspective` on .card-pack-card          1.0918   1.0918   1.0000
         ...also transform-style: preserve-3d      1.0918   1.0918   1.0000
         `perspective()` in THIS transform         1.0918   1.0918   1.0918

       The container placement never worked because `perspective` applies to an
       element's DIRECT children and the container's children are the wrappers,
       whose transforms are 2D. Moving it to the wrapper fixes Chromium and
       Firefox but WebKit ignores it there — including with `preserve-3d` added,
       with the wrapper's own transform removed, and under `-webkit-perspective`.
       The function form is part of this element's own matrix, so no parent 3D
       context has to be negotiated and all three agree.
       DO NOT ALSO PUT IT BACK ON A PARENT: with both, Chromium and Firefox
       apply it twice (1.1925) while WebKit still applies it once, and the
       engines visibly diverge.
       The rest state declares the same function list as the revealed state so
       the transition interpolates per-function instead of through a matrix
       decomposition that would flatten the perspective mid-turn. */
    transform: perspective(var(--flip-perspective, 1020px)) rotateY(0deg);
    /* Anticipation, then follow-through: a card has mass, so it resists at the
       start of the turn and rings slightly past 180 at the end instead of
       stopping dead.
       The curve is POINT-SYMMETRIC about (0.5, 0.5) — P2 = (1,1) - P1 — which
       is the only way the 90-degree crossing lands at exactly half the
       duration, and that crossing is what the back-face hide below is timed
       against. The previous curve, cubic-bezier(0.62, 0.04, 0.3, 1.04), was
       described as symmetric and was not: it reached half its progress at 46%
       of its duration, so the hide fired 4% of the flip LATE — the wrong side
       of a guard that exists to stop the back face leaking on iOS. Balancing
       the anticipation against the overshoot buys the symmetry and a real
       backswing at the same time.
       THE MASS IS IN THE Y CONTROL POINTS, and it has to be big enough to see.
       -0.04 / +1.04 sounds like anticipation but the curve's actual extremes
       are only -0.195 and +0.195 DEGREES on a 180-degree turn — sub-pixel, a
       comment describing motion that was not there. -0.15 / +1.15 gives
       -2.17/+2.17 degrees: the card rolls back before it commits, snaps through
       the middle at 1.73x the average rate, and rings past flat before it
       settles. Symmetry is preserved exactly (P2 = (1,1) - P1 holds for ANY
       y1), so the 90-degree crossing stays on the 50% hide beat — verified
       numerically, not assumed, in tests/unit/styles/cardPackJuice.test.js. */
    transition: transform var(--card-flip-dur, 0.6s) cubic-bezier(0.62, -0.15, 0.38, 1.15);
    will-change: transform;
}

/* The landing beat, on the INNER — the one transform slot in this subtree that
   is a transition rather than a `forwards`-filled animation, and therefore the
   one an engine cannot freeze. It animates the bare `scale` property, which
   composes with the flip's rotateY without either owning the other's slot. */
.card-pack-card-revealed .card-pack-card-inner {
    animation: packCardLand 0.44s cubic-bezier(0.2, 0.9, 0.3, 1) var(--card-flip-dur, 0.6s) both;
}

/* The Stellar swell rides the SAME beat as its flash and its sting, not the
   flip's completion. Left on `--card-flip-dur` it fired 600ms after the sound,
   so the moment arrived in three separate pieces — sting, then nothing, then
   light, then a pop — and the owner read every one of them as lag.
   30ms matches the aura exactly, so the card swells WHILE the light blows out
   and while it is still turning: one impact, then a settle. An ordinary card
   keeps its landing on the flip (below/above), because for a normal reveal the
   pop IS the arrival and there is no sting to sync to. */
.card-pack-card-revealed[data-stellar="true"] .card-pack-card-inner {
    animation: packCardLandStellar 0.44s cubic-bezier(0.2, 0.9, 0.3, 1) 30ms both;
}

.card-pack-card-face {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    backface-visibility: hidden;
}

.card-pack-card-front {
    transform: rotateY(180deg);
}

.card-pack-card-revealed .card-pack-card-inner {
    /* Same function list as the rest state, or the transition decomposes to a
       matrix and the depth disappears mid-turn. */
    transform: perspective(var(--flip-perspective, 1020px)) rotateY(180deg);
}

/* iOS WebKit lets a transformed grandchild leak through backface-visibility,
   so the just-flipped card showed ANOTHER CARD BACK for a beat ("they turn
   and on the other side there's another card back for a second"). Rely on
   state, not backface math: from the flip's midpoint the back face is simply
   gone — at 90 degrees neither face is visible, so the switch cannot show. */
.card-pack-card-revealed .card-pack-card-back {
    visibility: hidden;
    /* DERIVED, never hand-typed. This must fire at the flip's 90-degree
       crossing; a literal 0.3s silently desynchronises the moment anyone
       retunes the flip, and the iOS back-face leak comes straight back. */
    transition: visibility 0s calc(var(--card-flip-dur, 0.6s) * 0.5);
}

.card-pack-card-front-preparing {
    visibility: hidden;
    pointer-events: none;
}

/* Retire the reveal compositor after the flip. Keeping preserve-3d and a
   promoted transform forever makes otherwise-settled card pixels shimmer on
   some GPU/browser combinations. The front remains visually identical. */
.card-pack-card-flip-complete .card-pack-card-inner {
    transform: none;
    transform-style: flat;
    transition: none;
    will-change: auto;
}

.card-pack-card-flip-complete .card-pack-card-back {
    display: none;
}

.card-pack-card-flip-complete .card-pack-card-front {
    transform: none;
}

/* Firefox needs the revealed card to inherit the flipping face's backface
   contract. Keep the shared CardTilt transform intact; the parent inner owns
   the 180-degree reveal while the child owns pointer rotation. */
.card-pack-card-face .card {
    backface-visibility: hidden;
    overflow: visible;
    z-index: auto;
}

.card-packs-status {
    min-height: 18px;
    font-size: 0.78rem;
    color: var(--packs-muted);
    font-family: "Spectral", serif;
    text-align: center;
    position: relative;
    z-index: 1;
}

.card-packs-status.success {
    color: rgba(159, 255, 204, 0.9);
}

.card-packs-status.error {
    color: rgba(255, 154, 154, 0.95);
}

/* --pack-float scales the levitation so cramped stages (short landscape
   phones) keep a gentle bob instead of losing the float entirely. */
@keyframes packFloat {
    0% {
        transform: translateY(calc(-8px * var(--pack-float, 1)));
    }

    50% {
        transform: translateY(calc(-14px * var(--pack-float, 1)));
    }

    100% {
        transform: translateY(calc(-8px * var(--pack-float, 1)));
    }
}

/* The deal. The old version animated OPACITY ALONE — five cards materialising
   in mid-air out of nothing, which is why the ceremony's payoff read flat.
   They now come OUT of the torn pack: gathered and small at its mouth, thrown
   outward along the fan, past their slot, and settling back into it.
   The 100% frame is the base rule's rest transform, verbatim. */
/* The clip-safe CHARGED idle: where the stage has no vertical slack, the pack
   breathes instead of bobbing. Only ever scales DOWN, so it cannot reach an
   edge, and `scale:` is used rather than `transform:` because the phone tier
   pins that slot to `none` to keep the crimped edge inside the stage.
   DEPTH IS MEASURED, not guessed. At 0.982 with the default centre origin the
   pack's top edge moved 1.44px on a 159.6px pack — technically animating and
   visually parked. The pair below makes the charged pack equal the desktop
   idle: 0.965 doubles the amplitude,
   and the bottom origin (in the phone tier, where this runs) spends all of it
   on the top edge instead of splitting it between two. Measured 5.59-5.62px in
   Chromium, Firefox and WebKit against the desktop float's 6.00px. */
@keyframes packSealedBreath {
    0%, 100% { scale: 1; }
    50% { scale: 0.965; }
}

@keyframes packCardFan {
    /* ONE focal point. The cards used to gather at a fixed fraction of their
       own height above their slot, which put their origin 94px BELOW the pack
       they are supposedly coming out of — the tear happened in one place and
       the payoff started in another, and the eye had to travel between them.
       `--fan-origin-dy` is measured by card-packs.js from the pack's LAYOUT box
       to this container's own rest line, so the deal genuinely starts inside
       the torn wrapper at every viewport. The fallback keeps the old behaviour
       if the measurement has not landed yet. */
    0% {
        opacity: 0;
        transform:
            translateX(calc(var(--card-offset, 0) * var(--card-spread) * 0.06)) translateX(-50%) translateY(var(--fan-origin-dy, calc(var(--card-height, 0px) * -0.3))) rotate(0deg) scale(0.66);
    }

    26% {
        opacity: 1;
    }

    /* Overshoot: the cards fly a little past where they belong and further
       open than they end, so the settle has somewhere to come back from. */
    62% {
        transform:
            translateX(calc(var(--card-offset, 0) * var(--card-spread) * 1.09)) translateX(-50%) translateY(calc(var(--card-lift, 0px) * -1.34)) rotate(calc(var(--card-tilt, 0deg) * 1.16)) scale(1.035);
    }

    100% {
        opacity: 1;
        transform:
            translateX(calc(var(--card-offset, 0) * var(--card-spread))) translateX(-50%) translateY(calc(var(--card-lift, 0px) * -1)) rotate(var(--card-tilt, 0deg));
    }
}

/* Reduced motion still DEALS the cards — the arc is what calms, not the fact
   that five cards arrived. */
@keyframes packCardFanReduced {
    0% { opacity: 0; }
    100% { opacity: 1; }
}

/* The landing. A card that stops dead on arrival is a div; this is the two
   frames of give that make it a physical object coming to rest.
   The bare `scale` property, never a value folded into `transform`: the fan
   fills that slot `forwards` and Firefox/WebKit freeze a filled keyframe's
   substitutions, which is how this beat spent a release rendering nothing. */
@keyframes packCardLand {
    0% { scale: 1; }
    22% { scale: 1.045; }
    58% { scale: 0.991; }
    100% { scale: 1; }
}

/* Rarity emphasis lives in INTENSITY, not in timing: a Stellar card lands on
   the same beat as every other card, and hits harder. */
@keyframes packCardLandStellar {
    0% { scale: 1; }
    22% { scale: 1.092; }
    58% { scale: 0.985; }
    100% { scale: 1; }
}

/* ...and it is the only one that keeps burning afterwards. */
/* ---- The face-down rarity tell, on hover -----------------------------------
   Reference: reference/tcg/hearthstone/moments/pack-open — a face-down card
   holding something rare wears a violet aura that HUGS ITS SILHOUETTE, bleeding
   out from behind the card's own edges, while its neighbours wear nothing.
   `drop-shadow` is the right tool for that and the previous attempt's gradient
   sweep was the wrong one: drop-shadow is derived from the element's own alpha,
   so it traces the card's rounded-rect outline exactly and CANNOT paint outside
   it. That is a structural guarantee, not a tuning choice — the bars that swept
   across neighbouring cards are not reachable from here.
   Three stacked radii rather than one: a single ring reads as a flat outline,
   while tight-bright plus wide-dim gives the bloom volume the reference has. */
@keyframes packStellarBackBreathe {
    0% {
        filter:
            drop-shadow(0 0 3px rgba(245, 232, 255, 1))
            drop-shadow(0 -4px 14px rgba(198, 150, 255, 0.85))
            drop-shadow(0 -10px 30px rgba(160, 100, 250, 0.55))
            drop-shadow(0 4px 20px rgba(120, 66, 220, 0.35));
    }

    100% {
        filter:
            drop-shadow(0 0 4px rgba(252, 244, 255, 1))
            drop-shadow(0 -9px 22px rgba(214, 176, 255, 0.95))
            drop-shadow(0 -20px 48px rgba(172, 116, 255, 0.68))
            drop-shadow(0 5px 26px rgba(130, 74, 230, 0.42));
    }
}

.card-pack-card[data-stellar="true"]:not(.card-pack-card-revealed):hover .card-back {
    /* Alternating so the bloom swells and settles instead of restarting from
       cold every 1.6s, which reads as a flicker rather than a flame. */
    animation: packStellarBackBreathe 1.6s ease-in-out infinite alternate;
}

/* A PHONE HAS NO HOVER, so gating the tell on `:hover` hides it from every
   touch player — the rarity would simply never be announced there. On a
   pointerless device it is always on, which is also what the reference does
   (its face-down rare burns continuously; hover is our desktop refinement, not
   the mechanism). `hover: none` keys off the primary input rather than the
   viewport, so a small desktop window keeps the hover behaviour and a large
   tablet still gets the always-on one. */
@media (hover: none) {
    .card-pack-card[data-stellar="true"]:not(.card-pack-card-revealed) .card-back {
        animation: packStellarBackBreathe 1.6s ease-in-out infinite alternate;
    }
}

/* The revealed card's own glow, scaled up: at 26px the bloom died inside the
   card's own silhouette and read as a slightly brighter card rather than a
   Stellar one. It is a `drop-shadow` on the card element, so a wider radius
   spends nothing structurally — no new layer, no geometry, nothing that can
   paint over a neighbour — and the peak still decays to a calmer resting burn
   so the fan does not end up five glowing rectangles. */
@keyframes packStellarAura {
    0% {
        filter: drop-shadow(0 0 0 rgba(176, 132, 255, 0)) brightness(1);
    }

    14% {
        filter: drop-shadow(0 0 54px rgba(198, 160, 255, 0.98)) brightness(1.2);
    }

    /* The RESTING value is the one that matters most and was the weakest: a
       settled fan is what the player actually looks at, and at the old resting
       burn a Stellar card was not distinguishable from the three normals beside
       it. A white-hot contact edge over a wide violet bloom is what separates
       them at a glance — the same tight-bright/wide-dim split the reference
       uses on its face-down rare. */
    100% {
        filter:
            drop-shadow(0 0 3px rgba(250, 240, 255, 0.95))
            drop-shadow(0 0 22px rgba(198, 160, 255, 0.7))
            drop-shadow(0 0 44px rgba(150, 96, 245, 0.45))
            brightness(1.03);
    }
}

/* Calm the motion, keep every piece of information it was carrying. Each loop
   below has a still equivalent: the pack still sits on a shadow, the tear
   still flashes once, the fan still arrives, and a Stellar card is still
   unmistakably the Stellar card. */
@media (prefers-reduced-motion: reduce) {

    .card-packs-pack-nudge,
    .card-packs-buy-btn.card-packs-buy-ready,
    .card-packs-pack-tearing::after,
    .card-packs-pack-tearing::before,
    .card-packs-stage-tearing .card-packs-stage-flare,
    .card-packs-pack-straining .card-packs-pack-art,
    .card-packs-pack-tearing .card-packs-pack-seal,
    .card-packs-pack-body::before,
    .card-packs-pack-glow {
        animation: none;
    }

    /* The ribbon does not whip away; it simply goes with the wrapper's own
       fade. Leaving its fling running would be the single largest motion left
       on a calmed tear. */
    .card-packs-pack-tearing .card-packs-pack-seal {
        opacity: 0;
        transition: opacity 0.3s ease-out;
    }

    /* card-packs.js already refuses to spawn debris under reduced motion; this
       makes it structurally impossible rather than merely unreached, so a
       future caller cannot reintroduce thirty tumbling shards by accident. */
    .card-packs-foil-shard {
        animation: none;
        display: none;
    }

    /* The rarity tell keeps its glow and loses its breath. Which face-down card
       is the Stellar is information, not decoration, so calming the motion may
       not calm away the answer. */
    .card-pack-card[data-stellar="true"]:not(.card-pack-card-revealed) .card-back {
        animation: none;
    }

    /* The glow itself survives on both input models — it is the rarity, not
       decoration. Only the breath stops. `animation: none` has to be repeated
       on the :hover selector: the pointerless rule above is LESS specific, so
       it cannot override the hover rule's animation and the breath would keep
       running for a mouse user who asked for stillness. */
    .card-pack-card[data-stellar="true"]:not(.card-pack-card-revealed):hover .card-back {
        animation: none;
        filter:
            drop-shadow(0 0 6px rgba(216, 176, 255, 0.95))
            drop-shadow(0 0 20px rgba(174, 118, 255, 0.75))
            drop-shadow(0 0 42px rgba(138, 78, 240, 0.48));
    }

    @media (hover: none) {
        .card-pack-card[data-stellar="true"]:not(.card-pack-card-revealed) .card-back {
            filter:
                drop-shadow(0 0 6px rgba(216, 176, 255, 0.95))
                drop-shadow(0 0 20px rgba(174, 118, 255, 0.75))
                drop-shadow(0 0 42px rgba(138, 78, 240, 0.48));
        }
    }

    .card-packs-pack,
    /* The short-landscape breath excludes every non-idle state so it cannot
       fight the drag or the tear—or animate a vacant inventory stage. That
       specificity also beats a
       plain `.card-packs-pack` here, so calming it requires MATCHING the
       chain — a bare selector would leave the pack breathing at exactly the
       viewport where a player asked for stillness. */
    .card-packs-pack:not(.card-packs-pack-dragging):not(.card-packs-pack-tearing):not(.card-packs-pack-opened):not(.card-packs-pack-disabled):not(.card-packs-pack-uncharged):not(.card-packs-pack-charging) {
        animation: none;
        transform: translateY(0);
        scale: 1;
    }

    /* packSealedBreath, packFoilSheen and packContactShadow are all stilled;
       the sheen and the contact shadow keep their static presence so the pack
       is still a lit object resting on a surface. */
    .card-packs-pack-body::before {
        opacity: 0.3;
        background-position: 40% 0;
    }

    .card-packs-pack-glow {
        opacity: 0.7;
        transform: translateX(-50%);
    }

    /* The tear still reads: one seam of light, held, then gone — no stage
       bloom, no shiver, no whipping ribbon. */
    .card-packs-pack-tearing::after {
        opacity: 0.85;
        transition: opacity 0.3s ease-out;
    }

    /* PRE-EXISTING GAP, closed here. Nothing calmed the two halves, so a player
       who asked for reduced motion still had the wrapper flung 58% of its own
       width across the stage, tumbling 24 degrees. They part by fading now, and
       the information the moment carries — the pack is open — is still stated
       by the flash above and by the fan that follows. */
    .card-packs-pack-tearing .card-packs-pack-art::before,
    .card-packs-pack-tearing .card-packs-pack-art::after {
        animation: card-packs-tear-vanish 0.42s linear forwards;
    }

    /* packCardFan's arc becomes a plain arrival. */
    .card-pack-card,
    .card-pack-card-revealed,
    .card-pack-card-revealed[data-stellar="true"] {
        animation: packCardFanReduced 0.28s linear var(--card-delay, 0ms) forwards;
        /* Shorten the flip through the ONE knob, so the back-face hide moves
           with it. Overriding `transition-duration` on the inner instead would
           leave the hide firing at 0.3s on a 0.2s flip — the back visible for
           the entire turn, which is the exact iOS leak it exists to stop. */
        --card-flip-dur: 0.24s;
    }

    /* packStellarAura is gone, so the rarity it was announcing is stated
       outright instead. Dropping this would not calm the reveal, it would
       delete the one thing the reveal exists to tell the player. */
    .card-pack-card-revealed[data-stellar="true"] {
        filter: drop-shadow(0 0 12px rgba(176, 132, 255, 0.6));
    }

    /* The landing beat lives on the inner now, so it has to be calmed there.
       `scale: 1` is declared with it: `animation: none` alone would leave
       whatever the `both` fill had already committed. */
    .card-pack-card-revealed .card-pack-card-inner,
    .card-pack-card-revealed[data-stellar="true"] .card-pack-card-inner {
        animation: none;
        scale: 1;
    }

    /* `linear` and not `ease`: the back-face hide fires at half the flip's
       duration, and only a curve that is symmetric about its midpoint puts the
       90-degree crossing there. `ease` reaches half its progress at ~35% of its
       duration, which would hand the calmed path the same late-hide bug the
       main curve was just fixed for. */
    .card-pack-card-inner {
        transition-timing-function: linear;
    }
}

/* Tablet and desktop keep both Exchange panes permanent. Two intentional
   command rows give translated search/filter values their real width while
   preserving multiple complete listings above the pager. */
@media (orientation: landscape) and (min-width: 901px) and (min-height: 561px) {
    .moon-exchange-market-controls {
        display: grid;
        grid-template-columns: minmax(165px, 0.6fr) minmax(245px, 1fr) max-content;
        grid-template-rows: repeat(2, minmax(44px, auto));
        align-items: center;
        gap: 6px 10px;
        margin-bottom: 8px;
    }

    .moon-exchange-market-controls .moon-exchange-summary {
        grid-column: 1;
        grid-row: 1;
        min-width: 0;
        white-space: nowrap;
        font-size: 12px;
    }

    .moon-exchange-market-controls .deck-builder-search {
        grid-column: 2;
        grid-row: 1;
        min-width: 0;
        font-size: 14px;
    }

    .moon-exchange-market-controls #moon-exchange-owner-filter {
        grid-column: 1;
        grid-row: 2;
    }

    .moon-exchange-market-controls #moon-exchange-sort {
        grid-column: 2 / 4;
        grid-row: 2;
        width: auto;
        max-width: 100%;
        justify-self: stretch;
    }

    .moon-exchange-market-controls .moon-exchange-refresh {
        grid-column: 3;
        grid-row: 1;
        max-width: 100%;
        justify-self: end;
    }
}

@media (orientation: landscape) and (min-width: 901px) and (max-width: 1199px) and (min-height: 561px) {
    .moon-exchange-market-controls {
        /* The count summary is redundant at this width. Give its first column
           to the ownership filter, and let Search span both text columns; this
           keeps French labels complete without shrinking tablet type. */
        grid-template-columns: minmax(192px, 0.7fr) minmax(0, 1fr) max-content;
        column-gap: 4px;
    }

    .moon-exchange-market-controls .moon-exchange-summary {
        display: none;
    }

    .moon-exchange-market-controls .deck-builder-search {
        grid-column: 1 / 3;
    }

    .moon-exchange-market-controls #moon-exchange-owner-filter {
        grid-column: 1;
    }

    .moon-exchange-market-controls .moon-exchange-refresh {
        padding-inline: 7px;
        font-size: 13px;
    }
}

@media (max-width: 900px) {
    .moon-exchange-grid {
        grid-template-columns: minmax(0, 1fr);
    }

    .moon-exchange-market-controls {
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        align-items: center;
    }

    .moon-exchange-market-controls .deck-builder-search,
    #moon-exchange-owner-filter,
    #moon-exchange-sort {
        min-width: 0;
    }

    .moon-exchange-buy {
        width: auto;
    }

    .moon-exchange-buy-qty {
        width: 54px;
    }

    .moon-exchange-sell-row {
        grid-template-columns: minmax(88px, 110px) minmax(0, 1fr);
    }

    .moon-exchange-sell-preview {
        width: 110px;
        height: 156px;
        margin: 0 auto;
    }

    .card-packs-header {
        flex-direction: column;
        align-items: flex-start;
    }

    .card-packs-header-actions {
        width: 100%;
        justify-content: space-between;
    }

    .card-packs-duplicates {
        width: 100%;
        align-items: flex-start;
    }

    .card-packs-open-btn {
        left: 14px;
        right: 14px;
        text-align: center;
    }

    .card-packs-stage {
        min-height: 320px;
    }

    .card-packs-cards {
        --card-spread: calc(var(--card-width) * 0.6);
        height: calc(var(--card-height) * 1.6);
    }

    .card-packs-pack {
        /* Width-first here: releasing the base's stage-budget height lets the
           aspect-ratio derive it (both set would stretch the wrapper). */
        width: 165px;
        height: auto;
    }

    .card-pack-card {
        bottom: -8px;
    }
}

@media (max-height: 820px) {
    .card-packs-stage {
        /* Both, together, always. `--pack-stage-h` is the wrapper's height
           BUDGET and this is the stage's real floor; lowering only the floor
           left the wrapper sizing itself for the 410px stage the viewport
           implies while the grid handed it 280px, so 42px of crimped edge sat
           outside the stage and `overflow:hidden` cut it off at 1180x820 —
           exactly the clipping this band was once fixed for. */
        min-height: 280px;
    }

    .moon-exchange-grid {
        grid-template-rows: minmax(0, 1fr);
        flex: 1 1 auto;
        min-height: 0;
    }

    .moon-exchange-listings {
        overflow: hidden;
    }

    .moon-exchange-sell-preview .deck-builder-card {
        --moon-exchange-card-scale: 1;
    }
}

.collection-empty {
    color: rgba(238, 219, 241, 0.6);
    text-align: center;
    padding: 40px 0;
    font-style: italic;
}

/* A dropped catalog request is recoverable in place.

   The host is a CARD grid whose tracks are one card wide, so leaving the
   message and the button in those tracks squeezed "Failed to load cards." into
   a ~60px column and pushed the button out of a short phone page box entirely —
   a recovery control nobody can reach is not a recovery control. In the failed
   state the container stops being a grid and becomes one centred column. */
#deck-builder-collection-cards[data-raster-state="failed"],
#collection-cards-grid[data-raster-state="failed"] {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 10px;
    overflow: auto;
}

#deck-builder-collection-cards[data-raster-state="failed"] .collection-empty,
#collection-cards-grid[data-raster-state="failed"] .collection-empty {
    padding: 0;
    max-width: 100%;
}

.deck-builder-collection-retry,
.collection-cards-retry {
    flex: 0 0 auto;
    min-width: 132px;
    min-height: var(--mf-touch, 44px);
    padding: 8px 18px;
}

/* The collection list arrives with a card page, and the CLOSED select shows
   only its selected option — so without a cue on the control itself a player
   reads the bootstrap pair as their whole holdings. A native <select> cannot be
   restyled internally across engines, so the pending state is carried by the
   control's own border and a travelling underline. Presentation only: the
   control stays fully usable while it waits. */
.deck-filter[aria-busy="true"] {
    border-color: rgba(246, 226, 122, 0.55);
    background-image: linear-gradient(90deg,
        transparent 0%,
        rgba(246, 226, 122, 0.65) 45%,
        rgba(246, 226, 122, 0.65) 55%,
        transparent 100%);
    background-repeat: no-repeat;
    background-size: 60% 2px;
    background-position: -60% 100%;
    animation: deck-filter-pending-sweep 1.4s linear infinite;
}

@keyframes deck-filter-pending-sweep {
    from { background-position: -60% 100%; }
    to { background-position: 160% 100%; }
}

@media (prefers-reduced-motion: reduce) {
    .deck-filter[aria-busy="true"] {
        animation: none;
        background-position: 20% 100%;
    }
}

.collection-card-cost {
    position: absolute;
    bottom: 6px;
    right: 6px;
    padding: 2px 6px;
    border-radius: 4px;
    font-size: 0.65rem;
    color: #f4d8ff;
    background: rgba(0, 0, 0, 0.7);
    border: 1px solid rgba(238, 219, 241, 0.3);
    z-index: 10;
}

/* The tile is a transparent, border-less box exactly the size of the card it
   holds (`.deck-builder-cards .deck-card-item`), so the `overflow: hidden` it
   inherits from `.deck-card-item` clips nothing a player wants — except the
   first-discovery ribbon, which now hangs off the corner deliberately. Two
   classes so it outranks that inherited rule wherever either one is edited. */
.collection-modal .collection-card-item {
    overflow: visible;
}

/* The owned-count chip owns the right end of a tile's bottom line, so anything
   else sharing that line reserves its seat instead of sliding under it. Where
   the count chip is hidden the whole line is free again — one budget, declared
   against the same breakpoint that hides it, so the two can never disagree. */
.collection-card-item {
    --tile-bottom-line-max: calc(100% - 42px);
}

.collection-card-item-first {
    position: relative;
    /* Stellar-style presence, tinted by the card's own discovery palette. */
    border-color: hsl(var(--fd-hue-a, 262) var(--fd-sat-a, 62%) 62% / 0.55);
    box-shadow: 0 0 12px hsl(var(--fd-hue-a, 262) var(--fd-sat-a, 62%) 55% / 0.28);
}

/* First-discoverer ribbon — display chrome on the tile, never on the card face.
   z-index 10 matches the tile's cost chip, which provably paints above the card.
   This originally had to clear an unscoped `.card { z-index: 5 }` in
   game-board.css; that rule is now scoped to the board, and a test keeps it
   from returning, so 10 is simply the tile's chrome layer. */
.collection-first-badge {
    position: absolute;
    /* Hugging the corner from OUTSIDE. Inside the tile the ribbon lands on the
       card's title plate, which is the whole complaint; the tile is transparent
       and un-clipped (`.collection-card-item`) and the grid centres its columns,
       so the few pixels it hangs into are empty gutter. */
    top: -5px;
    left: -7px;
    z-index: 10;
    pointer-events: none;
    padding: 2px 8px;
    border-radius: 999px;
    font-size: clamp(9px, 0.75vmin, 12px);
    font-weight: 700;
    letter-spacing: 0.02em;
    color: #f2e8ff;
    background: linear-gradient(
        135deg,
        hsl(var(--fd-hue-a, 262) var(--fd-sat-a, 46%) calc(var(--fd-light-a, 31%) + 9%) / 0.92),
        hsl(var(--fd-hue-b, 262) var(--fd-sat-b, 44%) var(--fd-light-b, 25%) / 0.92)
    );
    border: 1px solid hsl(var(--fd-hue-a, 262) 64% 77% / 0.55);
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.45);
}

/* A phone page box leaves the grid about 2px of vertical slack, and the grid
   clips — so out there the overhang costs the ribbon its top edge instead of
   buying it room. Hug the corner from INSIDE at those sizes: less of a win, but
   a whole banner beats three quarters of one. */
@media (orientation: landscape) and (max-height: 360px), (max-width: 768px) {
    .collection-first-badge {
        top: 0;
        left: 0;
    }
}

/* Detail-header variant: inline chip instead of an absolute tile ribbon. */
.collection-first-badge--detail {
    position: static;
    margin-left: 10px;
    align-self: center;
    flex: 0 0 auto;
}

/* Stellar honor: same hash-driven identity system, fed its own salted palette
   (discovery-palette.js guarantees it never matches the card's normal ribbon),
   lifted brighter with a starlit border so it still reads as the rarer chip. */
.collection-first-badge--stellar {
    background: linear-gradient(
        135deg,
        hsl(var(--fd-hue-a, 48) var(--fd-sat-a, 62%) calc(var(--fd-light-a, 34%) + 14%) / 0.95),
        hsl(var(--fd-hue-b, 28) var(--fd-sat-b, 58%) calc(var(--fd-light-b, 28%) + 6%) / 0.95)
    );
    border-color: hsl(var(--fd-hue-a, 48) 74% 84% / 0.75);
    box-shadow:
        0 2px 6px rgba(0, 0, 0, 0.45),
        0 0 8px hsl(var(--fd-hue-a, 48) 70% 62% / 0.4);
}

.collection-card-item-stellar {
    border-color: rgba(80, 190, 255, 0.5);
    box-shadow: 0 0 12px rgba(60, 160, 220, 0.25);
}

.collection-card-item-stellar-loading {
    border-color: rgba(120, 210, 255, 0.65);
    box-shadow: 0 0 16px rgba(90, 190, 255, 0.35);
}

.collection-card-item-stellar-failed {
    border-color: rgba(255, 120, 120, 0.6);
    box-shadow: 0 0 14px rgba(255, 120, 120, 0.25);
}

/* Bottom row, sharing the count chip's line. At bottom:32px this chip printed
   across the middle of the description; the frame's own bottom margin makes the
   row below it the only place on the face that carries no text. */
/* Same defect as `.deck-card-stellar-badge`, found while fixing that one: the
   seat is `calc(100% - 42px)` of a tile that shrinks, the type was a flat
   0.58rem that does not. Measured: ink 56.22px against 50.45px of room at
   1400x500, so the label ellipsed there too. It is confined to roughly the
   420-620px height band — below 420px the landscape rule further down widens
   the seat to `calc(100% - 12px)`, and above ~620px the tile is big enough —
   which is why the seating spec, whose viewports are all 640px tall or more,
   never saw it.

   `min()`, not a straight multiplier, and that asymmetry is the point: this
   chip is anchored `bottom: 6px` and grows UPWARD toward
   `.collection-card-stellar-status` at `bottom: 27px`, whose 6px + 17px + 4px
   stack is measured and guarded. Capping at today's value means a tile at or
   above the reference renders byte-identical — the seating spec sees no change
   at any viewport it runs — while a smaller tile only ever shrinks the chip,
   moving its top edge DOWN and away from that neighbour. Reference is a
   1400x640 tile, where --catalog-card-unit * --collection-card-scale = 11.07px:
   0.8383 -> 9.28px, 0.1807/0.4517 -> 2px/5px, 0.03162 -> 0.35px. */
.collection-card-stellar-badge {
    --stellar-tile-unit: calc(
        var(--catalog-card-unit, var(--card-unit, 1vmin))
        * var(--collection-card-scale, var(--catalog-card-scale, 1.05))
    );
    position: absolute;
    bottom: 6px;
    left: 2px;
    max-width: var(--tile-bottom-line-max);
    overflow: hidden;
    white-space: nowrap;
    padding: min(2px, calc(0.1807 * var(--stellar-tile-unit)))
             min(5px, calc(0.4517 * var(--stellar-tile-unit)));
    border-radius: 3px;
    font-size: min(0.58rem, calc(0.8383 * var(--stellar-tile-unit)));
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: min(0.35px, calc(0.03162 * var(--stellar-tile-unit)));
    color: #e9f7ff;
    background: linear-gradient(135deg, rgba(36, 94, 150, 0.95), rgba(78, 190, 220, 0.95));
    border: 1px solid rgba(140, 220, 255, 0.45);
    z-index: 12;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
}

/* Directly above the Stellar chip, which is the seat it vacated. Both only ever
   appear on a Stellar tile, so this is a straight swap rather than a conditional
   one. 27px, not the 32px the chip used to sit at: a phone-page-box tile is only
   165px tall, its rim sits at 120px, and 32px put this row back OVER the
   summoner chip resting on that rim. 6px + a 17px chip + a 4px gap clears both
   neighbours at every size the seating spec measures. */
.collection-card-stellar-status {
    position: absolute;
    bottom: 27px;
    left: 6px;
    padding: 2px 6px;
    border-radius: 4px;
    font-size: 0.55rem;
    text-transform: uppercase;
    letter-spacing: 0.3px;
    color: rgba(210, 240, 255, 0.95);
    background: rgba(10, 18, 30, 0.75);
    border: 1px solid rgba(120, 190, 230, 0.45);
    z-index: 12;
    display: inline-flex;
    align-items: center;
    gap: 4px;
    max-width: calc(100% - 12px);
}

/* Measured: a landscape-phone tile is ~147-165px tall, so the band between the
   rim and its bottom edge is ~40-45px — room for ONE 17px chip line plus its
   margins, not two. Stacking them there put the upper line back over the
   summoner chip resting on the rim (-2.5px at a 932x300 page box). While a
   Stellar print is attuning or has failed, the status is the chip that is
   telling the player something new, so the ✦ Stellar chip yields the line for
   that stretch — the tile keeps its Stellar border and glow throughout, and the
   chip returns the moment the print completes. Keyed on height rather than the
   count chip's width tier, because what runs out here is vertical band. */
@media (orientation: landscape) and (max-height: 420px) {
    .collection-card-item:has(.collection-card-stellar-status) .collection-card-stellar-badge {
        display: none;
    }

    .collection-card-stellar-status {
        bottom: 6px;
        max-width: var(--tile-bottom-line-max);
    }
}

.collection-card-stellar-status-pending {
    border-color: rgba(140, 210, 255, 0.7);
    background: linear-gradient(135deg, rgba(20, 32, 56, 0.92), rgba(30, 60, 100, 0.85));
    box-shadow: 0 0 10px rgba(80, 170, 240, 0.35);
}

.collection-card-stellar-status-spinner {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    border: 1.5px solid rgba(140, 200, 255, 0.3);
    border-top-color: rgba(200, 240, 255, 1);
    border-right-color: rgba(170, 220, 255, 0.95);
    animation: stellar-status-spin 0.95s linear infinite;
    flex-shrink: 0;
    box-shadow: 0 0 4px rgba(140, 220, 255, 0.6);
}

.collection-card-stellar-status-label {
    flex-shrink: 0;
}

.collection-card-stellar-status-eta {
    text-transform: none;
    letter-spacing: 0;
    font-weight: 600;
    color: rgba(200, 230, 255, 0.95);
    opacity: 0.92;
}

.collection-craft-btn-locked {
    cursor: not-allowed;
    opacity: 0.6;
    filter: grayscale(0.25);
}

.collection-craft-btn-locked .craft-icon {
    opacity: 0.6;
}

.deck-builder-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 14px 24px;
    border-bottom: 1px solid rgba(238, 219, 241, 0.2);
}

.deck-builder-header h2 {
    margin: 0;
    color: #eedbf1;
    font-size: 1.5rem;
}

.deck-builder-header-actions {
    display: flex;
    align-items: center;
    gap: 12px;
}

.deck-builder-close {
    background: none;
    border: none;
    color: rgba(255, 255, 255, 0.6);
    font-size: 2rem;
    cursor: pointer;
    padding: 0;
    margin: 0;
    /* The bare glyph measured ~19x32 — below minimum hit-target size even for
       mouse. Guarantee a 44x44 hitbox unconditionally; flex-centering keeps
       the x glyph the same size and visually centered. */
    min-width: 44px;
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    line-height: 1;
    transition: color 0.2s;
}

.deck-builder-close:hover {
    color: #fff;
}

.collection-modal .deck-builder-close:focus-visible {
    outline: 2px solid #f6e27a;
    outline-offset: 2px;
}

.deck-builder-content {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(220px, 252px);
    gap: 12px;
    padding: 18px 16px 14px;
    flex: 1;
    overflow: hidden;
    min-height: 0;
}

.deck-builder-view-switcher {
    display: none;
}

.deck-builder-mobile-filters-toggle {
    display: none;
}

.deck-builder-filter-popover {
    display: contents;
}

.deck-builder-container.deck-builder-embedded {
    max-width: none;
    max-height: none;
    width: 100%;
    height: 100%;
    border: none;
    border-radius: 0;
    background: transparent;
    box-shadow: none;
    overflow-y: auto;
}

/* mobile.css intentionally turns standalone modal containers into 100vw x
   100dvh sheets. The embedded editor lives inside Collection's bounded
   workspace, so it must resolve percentages against that host instead of
   leaking the viewport-sized standalone geometry into the tab panel. The
   extra ancestor specificity keeps this reset authoritative after mobile.css. */
.collection-modal .deck-builder-container.deck-builder-embedded {
    width: 100%;
    max-width: 100%;
    min-width: 0;
    height: 100%;
    max-height: 100%;
    padding: 0;
    overflow: hidden;
}

.collection-section-deckbuilder > .deck-builder-container.deck-builder-embedded:not(.deck-builder-root-mode) {
    flex: 1 1 auto;
    height: 100%;
    min-height: 0;
}

.deck-builder-container.deck-builder-embedded .deck-builder-header {
    display: none;
}

.deck-builder-container.deck-builder-embedded .deck-builder-character-select {
    padding: 8px 8px 6px;
}

.deck-builder-character-select {
    display: block;
    flex: none;
    min-height: 0;
    overflow: visible;
}

.deck-builder-container.deck-builder-embedded.deck-builder-root-mode {
    display: flex;
    flex-direction: column;
    gap: 0;
    overflow-y: auto;
}

.deck-builder-root-mode .deck-builder-character-select {
    width: 100%;
}

.deck-builder-root-mode .deck-builder-saved {
    display: block;
}

.deck-builder-root-mode .deck-builder-saved .deck-list {
    overflow: hidden;
    flex-direction: row;
    flex-wrap: wrap;
}

.deck-builder-container.deck-builder-embedded .deck-builder-content {
    padding: 10px 0 0;
}

.deck-builder-section-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 10px;
}

.deck-builder-collection .deck-builder-section-header {
    gap: 12px;
    flex-wrap: nowrap;
    justify-content: flex-start;
}

.deck-builder-collection .deck-builder-controls {
    flex-wrap: nowrap;
    gap: 4px;
    margin-left: auto;
    min-width: 0;
    position: relative;
}

.deck-builder-collection .deck-builder-search {
    width: clamp(140px, 18vw, 250px);
    min-width: 120px;
    flex: 1 1 auto;
}

.deck-builder-collection .deck-filter {
    min-width: 74px;
    padding: 6px 8px;
}

#filter-collection {
    min-width: 104px;
}

#filter-mana,
#sort-cards {
    min-width: 74px;
}

.deck-builder-section-header h3 {
    margin: 0;
    color: #eedbf1;
    font-size: 1.1rem;
}

.deck-builder-search,
.deck-name-input {
    background: rgba(0, 0, 0, 0.4);
    border: 1px solid rgba(238, 219, 241, 0.3);
    border-radius: 6px;
    color: #fff;
    padding: 8px 12px;
    font-size: 0.9rem;
    outline: none;
    transition: border-color 0.2s;
}

.deck-builder-search:focus,
.deck-name-input:focus {
    border-color: rgba(238, 219, 241, 0.6);
}

.deck-builder-search::placeholder,
.deck-name-input::placeholder {
    color: rgba(238, 219, 241, 0.78);
    opacity: 1;
}

.deck-name-input {
    width: 100%;
    box-sizing: border-box;
}

/* Controls row with filters */
.deck-builder-controls {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    align-items: center;
}

.deck-filter {
    background: rgba(0, 0, 0, 0.4);
    border: 1px solid rgba(238, 219, 241, 0.3);
    border-radius: 6px;
    color: #fff;
    padding: 6px 10px;
    font-size: 0.85rem;
    outline: none;
    cursor: pointer;
    transition: border-color 0.2s;
}

.deck-filter:focus {
    border-color: rgba(238, 219, 241, 0.6);
}

.deck-filter option {
    background: #1a1a2e;
    color: #fff;
}

/* Touch ergonomics: give search fields and filter selects a >=44px hit target
   on coarse pointers and in the canonical phone viewport. The viewport branch
   matters because WebKit can report a fine pointer in browser automation and
   on hybrid devices. font-size at 16px also stops iOS Safari from zooming the
   page on focus. iOS ignores height on natively-styled selects, so .deck-filter
   opts out of the native appearance and draws its own chevron. Covers the
   collection modal, deck builder, and moon-exchange controls (which reuse
   these classes). */
@media (pointer: coarse),
       (orientation: portrait) and (max-width: 768px),
       (orientation: landscape) and (max-height: 560px) {
    .deck-builder-search {
        min-height: 44px;
        font-size: 16px;
    }

    .deck-filter {
        min-height: 44px;
        font-size: 16px;
        padding: 8px 26px 8px 10px;
        appearance: none;
        -webkit-appearance: none;
        background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23eedbf1' stroke-opacity='0.7' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
        background-repeat: no-repeat;
        background-position: right 9px center;
        background-size: 10px 6px;
    }

    /* Restate padding at the same specificity as the compact desktop
       overrides (.collection-controls-row/.deck-builder-collection set
       padding: 6px 8px) so they cannot squeeze text under the chevron. */
    .collection-controls-row .deck-filter,
    .deck-builder-collection .deck-filter {
        padding: 8px 26px 8px 10px;
    }

    .deck-name-input {
        min-height: 44px;
        font-size: 16px;
    }

    .deck-list-actions {
        opacity: 1;
    }

    .deck-action-btn {
        min-width: var(--mf-touch, 44px);
        min-height: var(--mf-touch, 44px);
        display: inline-flex;
        align-items: center;
        justify-content: center;
        padding: 0;
    }

    .deck-card-remove {
        min-width: 44px;
        min-height: 44px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }

    /* Touch browsers retain :hover after a tap. Do not lift the selected card
       out of the bounded catalog and clip its focus/selection ring. */
    .deck-card-item:hover {
        transform: none;
    }
}

/* Deck Builder responsive authority. The desktop editor remains the original
   two-pane workspace; only phone-sized viewports trade the inline filter row
   for a surfaced popover. The catalog stays paginated, while a short landscape
   phone presents the complete selected deck as a fixed, non-scrolling canvas. */
@media (orientation: portrait) and (max-width: 768px),
       (orientation: landscape) and (max-height: 560px) {
    /* Only the actual phone composition uses a two-tile, pagination-owned
       saved-deck lane. Tablet browsers may carry the mobile-ui class by UA at
       1024×768, but their restored stacked workspace has room for six decks. */
    .deck-builder-root-mode .deck-builder-saved .deck-list {
        display: grid;
        overflow: hidden;
        grid-template-rows: repeat(2, minmax(0, 1fr));
        grid-template-columns: minmax(0, 1fr);
        gap: 6px;
        padding: 0;
    }

    .deck-builder-root-mode .deck-list-item {
        width: 100%;
        min-width: 0;
        height: 100%;
        min-height: 0;
    }

    .collection-modal .collection-section-deckbuilder .deck-builder-collection {
        overflow: visible;
    }

    .collection-modal .collection-section-deckbuilder .deck-builder-collection > .deck-builder-section-header {
        position: relative;
        z-index: 30;
        display: grid;
        grid-template-columns: auto minmax(0, 1fr);
        align-items: center;
        gap: 6px;
        margin-bottom: 4px;
        overflow: visible;
    }

    .collection-modal .collection-section-deckbuilder .deck-builder-collection .deck-builder-controls {
        position: relative;
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        align-items: center;
        width: 100%;
        min-width: 0;
        gap: 6px;
        margin: 0;
        overflow: visible;
    }

    .collection-modal .collection-section-deckbuilder .deck-builder-search {
        width: 100%;
        min-width: 0;
    }

    .deck-builder-mobile-filters-toggle {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        min-width: 76px;
        min-height: var(--mf-touch, 44px);
        padding: 7px 11px;
        border: 1px solid rgba(238, 219, 241, 0.42);
        border-radius: 10px;
        background: rgba(35, 27, 49, 0.98);
        color: #f4e9f6;
        font: inherit;
        font-size: 13px;
        font-weight: 700;
        cursor: pointer;
        touch-action: manipulation;
    }

    .deck-builder-mobile-filters-toggle[aria-expanded="true"] {
        border-color: rgba(246, 226, 122, 0.82);
        background: rgba(78, 61, 104, 0.98);
        box-shadow: 0 0 0 2px rgba(246, 226, 122, 0.16);
    }

    .deck-builder-mobile-filters-toggle:focus-visible,
    .deck-builder-view-btn:focus-visible {
        outline: 3px solid #f6e27a;
        outline-offset: 2px;
    }

    .deck-builder-filter-popover {
        position: absolute;
        z-index: 80;
        top: calc(100% + 6px);
        right: 0;
        display: none;
        width: min(360px, calc(100vw - var(--mf-safe-left, 0px) - var(--mf-safe-right, 0px) - 24px));
        max-width: calc(100vw - var(--mf-safe-left, 0px) - var(--mf-safe-right, 0px) - 24px);
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 6px;
        padding: 8px;
        border: 1px solid rgba(238, 219, 241, 0.38);
        border-radius: 12px;
        background: rgba(17, 12, 29, 0.99);
        box-shadow: 0 16px 34px rgba(0, 0, 0, 0.58);
    }

    .deck-builder-filter-popover.is-open {
        display: grid;
    }

    .deck-builder-filter-popover .deck-filter {
        width: 100%;
        min-width: 0;
        min-height: var(--mf-touch, 44px);
        font-size: 16px;
    }

    .deck-builder-filter-popover #sort-cards {
        grid-column: 1 / -1;
    }

    .collection-section-deckbuilder .deck-builder-cards {
        --deckbuilder-column-count: 5;
        --catalog-card-scale: var(--deckbuilder-fit-card-scale, 1.28);
        grid-template-columns: repeat(var(--deckbuilder-column-count), calc(var(--card-width) * var(--catalog-card-scale)));
        justify-content: center;
        min-height: 0;
        overflow: hidden;
        padding: 0 4px;
    }

    .collection-section-deckbuilder .deck-builder-deck-cards {
        --deckbuilder-deck-row-height: var(--mf-touch, 44px);
        min-width: 0;
        overflow: hidden;
    }

    .collection-section-deckbuilder .deck-current-pagination {
        display: grid;
        grid-template-columns: minmax(44px, 1fr) minmax(0, auto) minmax(44px, 1fr);
        align-items: center;
        gap: 4px;
        width: 100%;
        min-width: 0;
        padding: 0 0 4px;
    }

    .collection-section-deckbuilder .deck-current-pagination[hidden] {
        display: none;
    }

    .collection-section-deckbuilder .deck-current-pagination .deck-btn {
        width: 100%;
        min-width: 0;
        min-height: var(--mf-touch, 44px);
        padding: 8px 2px;
        font-size: 0;
        line-height: 1;
    }

    .collection-section-deckbuilder #deck-current-prev::before {
        content: '‹';
        font-size: 20px;
    }

    .collection-section-deckbuilder #deck-current-next::before {
        content: '›';
        font-size: 20px;
    }

    .collection-section-deckbuilder .deck-current-pagination .deck-builder-page-info {
        min-width: 0;
        text-align: center;
        font-size: 0.72rem;
        line-height: 1.1;
        letter-spacing: 0;
        white-space: normal;
    }
}

@media (orientation: landscape) and (max-height: 560px) and (max-width: 720px) {
    .collection-section-deckbuilder .deck-builder-cards {
        --deckbuilder-column-count: 4;
    }
}

@media (orientation: portrait) and (max-width: 768px) {
    .collection-modal .deck-builder-container:not(.deck-builder-root-mode) > .deck-builder-view-switcher {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 6px;
        flex: 0 0 auto;
        padding: 4px 0;
    }

    .deck-builder-view-btn {
        min-width: 0;
        min-height: var(--mf-touch, 44px);
        padding: 7px 10px;
        border: 1px solid rgba(238, 219, 241, 0.28);
        border-radius: 9px;
        background: rgba(34, 26, 51, 0.94);
        color: rgba(238, 219, 241, 0.78);
        font: inherit;
        font-weight: 700;
        cursor: pointer;
    }

    .deck-builder-view-btn[aria-pressed="true"] {
        border-color: rgba(246, 226, 122, 0.74);
        background: rgba(91, 70, 133, 0.82);
        color: #fff8d8;
    }

    .collection-modal .collection-section-deckbuilder .deck-builder-content {
        display: grid;
        grid-template-columns: minmax(0, 1fr);
        grid-template-rows: minmax(0, 1fr);
        width: 100%;
        height: 100%;
        min-height: 0;
        overflow: hidden;
        padding: 4px 0;
    }

    .collection-section-deckbuilder .deck-builder-content[data-mobile-pane="catalog"] .deck-builder-deck,
    .collection-section-deckbuilder .deck-builder-content[data-mobile-pane="deck"] .deck-builder-collection {
        display: none;
    }

    .collection-modal .collection-section-deckbuilder .deck-builder-collection,
    .collection-modal .collection-section-deckbuilder .deck-builder-deck {
        width: 100%;
        height: 100%;
        max-height: none;
        min-width: 0;
        min-height: 0;
        margin: 0;
    }

    .collection-section-deckbuilder .deck-builder-cards {
        --deckbuilder-column-count: 3;
        --catalog-card-scale: var(--deckbuilder-fit-card-scale, 1.7);
    }
}

/* Collection Grid */
.deck-builder-collection {
    display: grid;
    grid-template-rows: auto minmax(0, 1fr) auto;
    width: 100%;
    min-width: 0;
    overflow: hidden;
    min-height: 0;
}

.deck-builder-cards {
    --deckbuilder-column-count: 4;
    display: grid;
    grid-template-columns: repeat(var(--deckbuilder-column-count), minmax(calc(var(--card-width) * var(--catalog-card-scale)), 1fr));
    gap: var(--catalog-card-gap);
    row-gap: calc(var(--catalog-card-gap) + 2px);
    overflow: visible;
    padding: 6px 4px 2px;
    flex: 1;
    min-height: 0;
    max-height: none;
    align-content: start;
    justify-items: center;
    position: relative;
    --collection-card-scale: var(--catalog-card-scale);
}

.deck-builder-collection-paint-stage {
    position: absolute;
    inset: 0;
    display: grid;
    grid-template-columns: repeat(
        var(--deckbuilder-column-count),
        minmax(calc(var(--card-width) * var(--catalog-card-scale)), 1fr)
    );
    gap: var(--catalog-card-gap);
    row-gap: calc(var(--catalog-card-gap) + 2px);
    align-content: start;
    justify-items: center;
    visibility: hidden;
    pointer-events: none;
}

.deck-builder-cards.deck-builder-cards-balanced {
    grid-template-columns: repeat(
        var(--deckbuilder-column-count),
        var(--deckbuilder-card-track-width, calc(var(--card-width) * var(--catalog-card-scale)))
    );
    justify-content: space-evenly;
    align-content: center;
}

.deck-builder-cards-balanced .deck-builder-collection-paint-stage {
    grid-template-columns: repeat(
        var(--deckbuilder-column-count),
        var(--deckbuilder-card-track-width, calc(var(--card-width) * var(--catalog-card-scale)))
    );
    justify-content: space-evenly;
    align-content: center;
}

.deck-builder-cards .deck-card-item {
    background: transparent;
    border: none;
    min-height: 0;
    width: calc(var(--card-width) * var(--collection-card-scale));
    height: calc(var(--card-height) * var(--collection-card-scale));
    /* Capture the ambient unit for the card to multiply. A custom property
       cannot reference itself — `--card-unit: calc(var(--card-unit) * S)` is
       guaranteed-invalid — so the capture happens one level up, on the tile
       that still sees the inherited value. That indirection is also what keeps
       this DRY: the :root phone floor (`max(1vmin, 4.6px)`) and the 4K `zoom`
       counter-scales reach the card automatically instead of being re-derived
       per surface, the way the packs fan had to hard-code them. */
    --catalog-card-unit: var(--card-unit);
}

/* A card has ONE shape and its container does not get a vote. The tile is a
   flex parent, so the card inherited `flex-shrink: 1` and was squeezed
   horizontally whenever the track was tight — while its height, which is set
   explicitly, stayed put. Measured on the 844x288 page box: the catalog card
   came out 31x91 (aspect 0.34) beside a collection card of 77x140 (0.55) from
   the identical `--card-width`/`--card-height`. That is "deck building is
   functional but small cards are misrendered severely": not a size problem, a
   SHAPE one, which is why it read as squashed rather than small. */
.deck-builder-cards .deck-card-item > .card {
    flex: 0 0 auto;
    width: var(--card-width);
    height: var(--card-height);
}

.deck-builder-cards .deck-card-item.in-deck {
    outline: 3px solid #8ff0b2;
    outline-offset: -3px;
    box-shadow: 0 0 0 2px rgba(21, 55, 38, 0.82), 0 0 18px rgba(111, 232, 153, 0.48);
}

.deck-card-item {
    position: relative;
    background: rgba(0, 0, 0, 0.4);
    border: 2px solid rgba(255, 255, 255, 0.1);
    border-radius: 8px;
    overflow: hidden;
    cursor: pointer;
    transition: transform 0.2s, border-color 0.2s;
    min-height: 240px;
}

.deck-card-item:hover {
    transform: translateY(-4px);
    border-color: rgba(238, 219, 241, 0.5);
}

.deck-card-item.in-deck {
    border-color: rgba(100, 200, 100, 0.6);
}

.deck-card-image {
    aspect-ratio: 3/4;
    overflow: hidden;
}

.deck-card-image img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.deck-card-name {
    padding: 8px;
    font-size: 0.85rem;
    color: #fff;
    text-align: center;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.deck-card-cost {
    position: absolute;
    top: 4px;
    left: 4px;
    background: rgba(100, 150, 255, 0.9);
    color: #fff;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.75rem;
    font-weight: bold;
}

.deck-card-check {
    position: absolute;
    top: 4px;
    right: 4px;
    /* Above the hovered card's own elevation, always. */
    z-index: 20;
    background: rgba(26, 124, 68, 0.96);
    color: #fff !important;
    min-width: 24px;
    width: auto;
    height: 24px;
    padding: 0 7px;
    border-radius: 999px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.7rem;
}

/* Current Deck Panel */
.deck-builder-deck {
    display: grid;
    grid-template-rows: auto auto minmax(0, 1fr) auto auto;
    background: rgba(0, 0, 0, 0.3);
    border-radius: 10px;
    padding: 16px;
    width: 100%;
    height: 100%;
    min-height: 0;
    max-height: none;
    overflow: hidden;
}

.deck-builder-deck-info {
    color: rgba(255, 255, 255, 0.7);
    font-size: 0.9rem;
    margin-bottom: 12px;
}

.deck-status {
    margin-left: 10px;
    font-size: 0.8rem;
}

.deck-status-warning {
    color: #ffaa00;
}

.deck-status-error {
    color: #ff6666;
}

.deck-status-success {
    color: #66ff66;
}

.deck-builder-deck-cards {
    --deckbuilder-deck-row-height: 42px;
    display: grid;
    grid-auto-rows: var(--deckbuilder-deck-row-height);
    align-content: start;
    gap: 4px;
    flex: 1;
    min-height: 0;
    overflow: hidden;
    margin-bottom: 6px;
    max-height: none;
    position: relative;
}

.deck-builder-deck-paint-stage {
    position: absolute;
    inset: 0;
    margin: 0;
    visibility: hidden;
    pointer-events: none;
}

/* A mini whose art has not settled yet (or failed to). docs/asset-loading.md
   law 2: the entry is still TRUE — it names a card that really is in the deck —
   so it stays on screen and only the art well reads as unfinished. Withholding
   the whole lane instead is what made the deck counter and the deck list
   disagree. */
/* Opacity only — never a filter. Card art is held to a crispness contract
   (tests/unit/styles/deckBuilderChooserLayout.test.js) that forbids filtering
   the art well, and a placeholder has no business relaxing it. */
.deck-card-mini.deck-card-mini-art-unsettled .deck-card-mini-art {
    opacity: 0.4;
}

.deck-card-mini.deck-card-mini-art-unsettled::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    pointer-events: none;
    background: linear-gradient(
        100deg,
        transparent 30%,
        rgba(238, 219, 241, 0.14) 50%,
        transparent 70%
    );
    background-size: 220% 100%;
    animation: deck-mini-art-shimmer 1.9s ease-in-out infinite;
}

@keyframes deck-mini-art-shimmer {
    from { background-position: 180% 0; }
    to { background-position: -80% 0; }
}

@media (prefers-reduced-motion: reduce) {
    .deck-card-mini.deck-card-mini-art-unsettled::after { animation: none; }
}

.deck-builder-deck-cards.is-empty {
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
}

/* The highlight marks the PAINTED PANEL, because that is now the box that
   accepts the drop. Outlining the inner grid while a wider region worked was a
   highlight that disagreed with the hit area. The inner-grid selector stays so
   a layout without the panel wrapper still shows an affordance. */
.deck-builder-deck.drag-over,
.deck-builder-deck-cards.drag-over {
    outline: 1px dashed rgba(120, 160, 255, 0.6);
    outline-offset: -2px;
    background: rgba(90, 120, 200, 0.08);
}

.deck-empty {
    color: rgba(255, 255, 255, 0.82);
    text-align: center;
    width: 100%;
    margin: 0;
    padding: 14px 10px;
    font-style: italic;
    line-height: 1.35;
    border: 1px dashed rgba(238, 219, 241, 0.22);
    border-radius: 8px;
    background: rgba(255, 255, 255, 0.04);
}

.deck-card-mini {
    position: relative;
    isolation: isolate;
    display: grid;
    grid-template-columns: 42px minmax(0, 1fr) auto auto;
    align-items: center;
    gap: 7px;
    padding: 0 7px 0 0;
    overflow: hidden;
    background: rgba(16, 13, 31, 0.88);
    border: 1px solid rgba(238, 219, 241, 0.12);
    border-radius: 6px;
    min-height: 0;
    margin-bottom: 0;
    box-shadow: inset 0 1px rgba(255, 255, 255, 0.035), 0 2px 7px rgba(0, 0, 0, 0.16);
    transition: background 0.2s, border-color 0.2s, box-shadow 0.2s;
}

.deck-card-mini::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 1;
    background: linear-gradient(90deg, transparent 0, rgba(16, 13, 31, 0.18) 34px, rgba(16, 13, 31, 0.92) 54px);
    pointer-events: none;
}

.deck-card-mini-art {
    grid-column: 1;
    width: 42px;
    height: 100%;
    min-height: 40px;
    align-self: stretch;
    object-fit: cover;
    object-position: center;
    pointer-events: none;
    user-select: none;
    -webkit-user-select: none;
}

.deck-current-pagination {
    flex: 0 0 auto;
    padding: 0 0 6px;
}

.deck-current-pagination[hidden] {
    display: none;
}

.deck-card-item.dragging {
    opacity: 0.7;
}

.deck-card-mini:hover {
    background: rgba(255, 255, 255, 0.1);
    border-color: rgba(238, 219, 241, 0.26);
    box-shadow: inset 0 1px rgba(255, 255, 255, 0.06), 0 3px 10px rgba(0, 0, 0, 0.24);
}

.deck-card-mini-cost {
    position: absolute;
    top: 4px;
    left: 4px;
    z-index: 2;
    background: rgba(100, 150, 255, 0.9);
    color: #fff;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.75rem;
    font-weight: bold;
    flex-shrink: 0;
}

.deck-card-mini-name {
    min-width: 0;
    z-index: 2;
    color: #fff;
    font-size: 0.85rem;
    font-weight: 600;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
}

.deck-card-remove {
    position: relative;
    z-index: 2;
    display: grid;
    place-items: center;
    align-self: stretch;
    min-width: 28px;
    background: none;
    border: none;
    color: rgba(255, 100, 100, 0.7);
    cursor: pointer;
    font-size: 1.2rem;
    padding: 0 4px;
    transition: color 0.2s;
}

.deck-card-remove:hover {
    color: #ff6666;
}

.deck-card-remove:focus-visible {
    outline: 2px solid #f6e27a;
    outline-offset: -2px;
    border-radius: 5px;
}

/* Action Buttons */
.deck-builder-actions {
    display: flex;
    gap: 10px;
}

.deck-btn {
    flex: 1;
    padding: 12px 16px;
    border: none;
    border-radius: 8px;
    font-size: 0.95rem;
    font-weight: 500;
    cursor: pointer;
    transition: all 0.2s;
}

.deck-btn-primary {
    background: linear-gradient(135deg, #7459c8, #5a3fa8);
    color: #fff;
}

.deck-btn-primary:hover:not(:disabled) {
    background: linear-gradient(135deg, #8569d8, #6a4fb8);
    transform: translateY(-2px);
}

.deck-btn-primary:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

.deck-btn-secondary {
    background: rgba(255, 255, 255, 0.1);
    color: #fff;
    border: 1px solid rgba(255, 255, 255, 0.2);
}

.deck-btn-secondary:hover {
    background: rgba(255, 255, 255, 0.15);
}

.deck-btn-danger {
    background: linear-gradient(135deg, #d05252, #9f2f3f);
    color: #fff;
    border: 1px solid rgba(255, 160, 160, 0.3);
}

.deck-btn-danger:hover:not(:disabled) {
    background: linear-gradient(135deg, #e16363, #b23c4c);
    transform: translateY(-2px);
}

/* Saved Decks List */
.deck-builder-saved {
    padding: 10px 24px 12px;
    border-top: 1px solid rgba(238, 219, 241, 0.2);
}

.deck-builder-saved-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 15px;
}

.deck-builder-saved-header h3 {
    margin: 0;
    color: #eedbf1;
    font-size: 1.1rem;
}

.deck-btn-cardback {
    background: linear-gradient(135deg, #5a4a8a, #4a3a7a);
    color: #fff;
    border: 1px solid rgba(238, 219, 241, 0.3);
    border-radius: 6px;
    padding: 8px 14px;
    font-size: 0.9rem;
    cursor: pointer;
    transition: all 0.2s ease;
    white-space: nowrap;
}

.deck-btn-cardback:hover {
    background: linear-gradient(135deg, #6a5a9a, #5a4a8a);
    border-color: rgba(238, 219, 241, 0.5);
    transform: translateY(-1px);
}

/* Cardbacks Section */
.deck-builder-cardbacks {
    padding: 20px 24px;
    border-top: 1px solid rgba(238, 219, 241, 0.2);
    text-align: center;
    background: linear-gradient(135deg, rgba(90, 74, 138, 0.1), rgba(74, 58, 122, 0.1));
}

.deck-builder-cardbacks h3 {
    margin: 0 0 8px;
    color: #eedbf1;
    font-size: 1.1rem;
}

.cardbacks-subtitle {
    margin: 0 0 16px;
    color: rgba(238, 219, 241, 0.6);
    font-size: 0.9rem;
}

.deck-btn-cardback-large {
    background: linear-gradient(135deg, #6a5a9a, #5a4a8a);
    color: #fff;
    border: 1px solid rgba(238, 219, 241, 0.4);
    border-radius: 8px;
    padding: 12px 24px;
    font-size: 1rem;
    cursor: pointer;
    transition: all 0.2s ease;
    display: inline-flex;
    align-items: center;
    gap: 8px;
}

.deck-btn-cardback-large:hover {
    background: linear-gradient(135deg, #7a6aaa, #6a5a9a);
    border-color: rgba(238, 219, 241, 0.6);
    transform: translateY(-2px);
    box-shadow: 0 4px 15px rgba(100, 80, 160, 0.3);
}

.deck-builder-saved h3 {
    margin: 0 0 8px;
    color: #eedbf1;
    font-size: 1.1rem;
}

.deck-list {
    display: flex;
    gap: 10px;
    overflow: hidden;
    padding-bottom: 4px;
    flex-wrap: wrap;
}

.deck-list-empty {
    color: rgba(255, 255, 255, 0.5);
    font-style: italic;
}

.deck-list-item {
    flex-shrink: 0;
    background: linear-gradient(135deg, rgba(38, 30, 62, 0.96), rgba(19, 15, 34, 0.98));
    border: 1px solid rgba(255, 255, 255, 0.08);
    border-radius: 10px;
    min-width: 180px;
    height: 100px;
    display: block;
    cursor: pointer;
    transition: box-shadow 0.2s ease, border-color 0.2s ease, background 0.2s ease;
    position: relative;
    overflow: hidden;
}

.deck-list-item-skeleton {
    background: linear-gradient(135deg, rgba(38, 30, 62, 0.72), rgba(19, 15, 34, 0.78));
    cursor: default;
    pointer-events: none;
}

.deck-list-item-has-bg {
    position: relative;
    border-color: transparent;
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1);
}

.deck-list-portrait-bg {
    position: absolute;
    inset: 0;
    z-index: 0;
    display: block;
    width: 100%;
    height: 100%;
    max-width: none;
    object-fit: contain;
    object-position: center;
    background:
        radial-gradient(circle at 50% 35%, rgba(111, 82, 158, 0.24), transparent 64%),
        #090711;
    filter: none;
    transform: none;
    pointer-events: none;
    -webkit-user-select: none;
    user-select: none;
}

.deck-list-item-has-bg::after {
    content: none;
}

/* Keep both identity lines on the full-width lower vignette. */
.deck-list-name-bar {
    position: relative;
    z-index: 3;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    justify-content: flex-end;
    gap: 1px;
    width: 100%;
    min-width: 0;
    max-height: 100%;
    padding: 7px 10px;
    background: linear-gradient(
        to top,
        rgba(8, 4, 18, 0.94),
        rgba(8, 4, 18, 0.66) 72%,
        transparent
    );
}

.deck-list-edit,
.deck-list-view {
    position: absolute;
    inset: 0;
    z-index: 2;
    display: flex;
    align-items: flex-end;
    width: 100%;
    padding: 0;
    border: 0;
    background: transparent;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
}

/* Keep the established edit control's explicit no-blur contract independently
   addressable by the visual regression guard. */
.deck-list-edit {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
}

.deck-list-edit:focus-visible,
.deck-list-view:focus-visible {
    outline: 3px solid #f6e27a;
    outline-offset: -3px;
}

.deck-list-name {
    flex: 0 0 auto;
    width: 100%;
    min-width: 0;
    color: #fff;
    font-weight: 600;
    font-size: 0.9rem;
    letter-spacing: 0.03em;
    text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    display: block;
}

.deck-list-summoner {
    display: block;
    width: 100%;
    min-width: 0;
    overflow: visible;
    color: rgba(238, 219, 241, 0.78);
    font-size: 0.72rem;
    font-weight: 500;
    line-height: 1.15;
    text-overflow: clip;
    white-space: normal;
    overflow-wrap: anywhere;
    text-shadow: 0 1px 3px rgba(0, 0, 0, 0.85);
}

/* Action buttons — top-right, revealed by hover or keyboard focus */
.deck-list-actions {
    position: absolute;
    top: 6px;
    right: 6px;
    z-index: 4;
    display: flex;
    gap: 4px;
    opacity: 0;
    transition: opacity 0.2s ease;
}

.deck-list-item:hover .deck-list-actions,
.deck-list-item:focus-within .deck-list-actions {
    opacity: 1;
}

/* Keep the direct portrait on an untransformed layer so hover cannot soften it. */
.deck-list-item:hover {
    box-shadow: 0 6px 24px rgba(0, 0, 0, 0.5);
    border-color: rgba(200, 180, 255, 0.3);
}

.deck-list-item.deck-default {
    border-color: rgba(255, 200, 50, 0.4);
}

.deck-list-item.deck-default:hover {
    border-color: rgba(255, 200, 50, 0.6);
}

.deck-list-count {
    color: rgba(255, 255, 255, 0.6);
    font-size: 0.85rem;
}

.deck-list-item-draft {
    border-color: rgba(255, 180, 90, 0.55);
    box-shadow: inset 0 0 0 1px rgba(255, 180, 90, 0.25);
}

.deck-list-item-draft:hover {
    border-color: rgba(255, 200, 110, 0.85);
}

.deck-list-draft-badge {
    flex: 0 0 auto;
    display: inline-block;
    max-width: 100%;
    margin-left: 0;
    padding: 1px 8px;
    border-radius: 999px;
    background: rgba(255, 165, 70, 0.85);
    color: #1b0f08;
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-shadow: none;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    vertical-align: middle;
}

.deck-list-item-official {
    border-color: rgba(137, 199, 255, 0.46);
    box-shadow: inset 0 0 0 1px rgba(137, 199, 255, 0.14);
}

.deck-list-item-official:hover {
    border-color: rgba(175, 220, 255, 0.8);
}

.deck-list-official-badge {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    max-width: 100%;
    padding: 1px 7px;
    border: 1px solid rgba(160, 213, 255, 0.42);
    border-radius: 999px;
    background: rgba(23, 60, 91, 0.88);
    color: #d9f0ff;
    font-size: 0.66rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    line-height: 1.3;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.deck-list-official-badge svg {
    flex: 0 0 auto;
    width: 12px;
    height: 12px;
}

.deck-default-badge {
    background: rgba(255, 200, 50, 0.2);
    color: #ffc832;
    font-size: 0.75rem;
    padding: 2px 8px;
    border-radius: 10px;
    width: fit-content;
}

.deck-action-btn {
    background: rgba(8, 4, 18, 0.86);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 6px;
    padding: 3px 7px;
    cursor: pointer;
    font-size: 0.8rem;
    transition: background 0.15s, border-color 0.15s;
    line-height: 1;
}

.deck-action-btn svg {
    width: 20px;
    height: 20px;
    display: block;
}

.deck-action-btn:hover {
    background: rgba(255, 255, 255, 0.15);
    border-color: rgba(255, 255, 255, 0.25);
}

.deck-action-delete:hover {
    background: rgba(255, 60, 60, 0.35);
    border-color: rgba(255, 100, 100, 0.4);
}

/* Scrollbar styles */
.deck-builder-cards::-webkit-scrollbar,
.deck-builder-deck-cards::-webkit-scrollbar {
    width: 6px;
}

.deck-builder-cards::-webkit-scrollbar-track,
.deck-builder-deck-cards::-webkit-scrollbar-track {
    background: transparent;
}

.deck-builder-cards::-webkit-scrollbar-thumb,
.deck-builder-deck-cards::-webkit-scrollbar-thumb {
    background: rgba(255, 255, 255, 0.2);
    border-radius: 3px;
}

/* Responsive */
@media (max-width: 768px) {
    .deck-builder-content {
        grid-template-columns: 1fr;
    }

    .deck-builder-deck {
        max-height: 200px;
    }
}

/* Character Selection Screen */
.deck-builder-character-select {
    padding: 12px 24px 8px;
    text-align: center;
    color: #eedbf1;
}

.deck-builder-character-select h3 {
    margin-bottom: 12px;
    font-size: 1.5rem;
}

.character-options {
    display: flex;
    justify-content: center;
    gap: 30px;
}

.character-btn {
    background: rgba(30, 25, 50, 0.6);
    border: 2px solid rgba(238, 219, 241, 0.3);
    border-radius: 12px;
    padding: 30px;
    display: flex;
    flex-direction: column;
    align-items: center;
    width: 200px;
    cursor: pointer;
    transition: all 0.2s;
    color: #eedbf1;
}

.character-btn:hover {
    background: rgba(238, 219, 241, 0.1);
    transform: translateY(-2px);
    border-color: rgba(238, 219, 241, 0.6);
}

.character-btn.selected {
    background: rgba(238, 219, 241, 0.2);
    border-color: #eedbf1;
    box-shadow: 0 0 15px rgba(238, 219, 241, 0.3);
}

.character-icon {
    font-size: 3rem;
    margin-bottom: 15px;
}

.character-name {
    font-size: 1.2rem;
    font-weight: bold;
    margin-bottom: 8px;
}

.character-desc {
    font-size: 0.9rem;
    color: rgba(238, 219, 241, 0.7);
}

/* Tabs */
.deck-builder-tabs {
    display: flex;
    gap: 10px;
    margin-bottom: 15px;
}

.deck-tab {
    background: transparent;
    border: none;
    padding: 8px 16px;
    color: rgba(238, 219, 241, 0.6);
    font-size: 1rem;
    cursor: pointer;
    border-bottom: 2px solid transparent;
    transition: all 0.2s;
}

.deck-tab:hover {
    color: rgba(238, 219, 241, 0.9);
}

.deck-tab.active {
    color: #eedbf1;
    border-bottom-color: #eedbf1;
}

/* Touch ergonomics: the tab strips (Cards/Deck Builder/... and the
   Characters/Spells sub-tabs) sit ~36px tall with text padding alone. Grow
   them to a >=44px hit target on coarse pointers only; the flex tab rows
   stretch to fit, and the active underline stays on the bottom edge. */
@media (pointer: coarse) {
    .deck-tab {
        min-width: 44px;
        min-height: 44px;
    }
}

/* Character Display in Deck Info */
#selected-character-display {
    margin-right: 15px;
    font-weight: bold;
    color: #eedbf1;
    background: rgba(255, 255, 255, 0.1);
    padding: 4px 10px;
    border-radius: 4px;
}

/* Character Selection Screen in Deck Builder */
.db-character-grid {
    display: grid;
    grid-template-columns: repeat(4, minmax(210px, 1fr));
    gap: 16px;
    margin-top: 0;
    padding: 0 8px;
}

.db-character-pagination {
    margin-top: 6px;
    padding: 0 8px;
}

.db-character-card {
    background: rgba(255, 255, 255, 0.05);
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 12px;
    overflow: hidden;
    cursor: pointer;
    transition: background-color 0.25s ease, border-color 0.25s ease, box-shadow 0.25s ease;
    display: flex;
    flex-direction: column;
    height: 100%;
    position: relative;
    min-height: 0;
}

.db-character-card:hover {
    background: rgba(255, 255, 255, 0.1);
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
    border-color: rgba(238, 219, 241, 0.6);
    /* Generic theme color */
}

.db-char-image-wrapper {
    width: 100%;
    aspect-ratio: 16 / 9;
    height: auto;
    flex: 0 0 auto;
    overflow: hidden;
    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
    background:
        radial-gradient(circle at 50% 35%, rgba(111, 82, 158, 0.2), transparent 62%),
        #090711;
    box-shadow: inset 0 0 0 1px rgba(238, 219, 241, 0.08);
}

.db-char-image {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
    object-position: center;
}

.db-char-info {
    padding: 10px 12px;
    text-align: center;
    flex: 1;
    display: flex;
    flex-direction: column;
    justify-content: center;
}

/* TWO LINE BOXES, ALWAYS — so every summoner box is the same height and every
   subtitle sits at the same offset.

   The name is the only variable-height thing in this card, and it was free to
   be one line or two. Measured on the real pool at 820x560, where the names
   start wrapping: card heights came out 120.5px and 103.5px side by side, and
   "The Lunar Sage" and its neighbours sat 8.5px apart. `.db-char-info` centres
   its column, so a shorter name did not just leave a gap — it moved the
   subtitle up.

   Reserving the taller of the two shapes fixes both at once: the box stops
   varying, and the subtitle's offset stops varying with it. The name is
   bottom-aligned inside that reserve, so a one-line name sits on the same
   baseline a two-line name's LAST line does — the name-to-subtitle gap is then
   identical everywhere, and the slack collects above the name where the
   column's centring absorbs it.

   `flex: none` because this is an in-flow child of a column flex container:
   without it the reserve is the first thing shrunk when the row is tight, which
   is exactly the variability being removed. */
.db-char-name {
    font-size: clamp(1rem, 1.2vw, 1.4rem);
    font-weight: bold;
    margin-bottom: 3px;
    color: #fff;
    line-height: 1.2;
    flex: none;
    min-height: calc(2em * 1.2);
    display: flex;
    align-items: flex-end;
    justify-content: center;
}

.db-char-desc {
    color: rgba(255, 255, 255, 0.7);
    font-size: 0.9rem;
    line-height: 1.4;
    max-height: 2.8em;
    overflow: hidden;
}

.loading-spinner {
    text-align: center;
    padding: 40px;
    color: rgba(255, 255, 255, 0.6);
    font-size: 1.2rem;
    font-style: italic;
}

/* Deck Builder Card Styles - scrolling handled by container */

.deck-card-item {
    position: relative;
    cursor: pointer;
    transition: all 0.3s ease;
    display: flex;
    justify-content: center;
    align-items: center;
    overflow: visible !important;
    /* Critical: allow card to overflow on zoom */
}

.deck-card-item .deck-builder-card {
    transform-origin: center center;
    --card-base-transform: scale(0.6);
    transition: all 0.3s ease;
    pointer-events: none;
    /* Let clicks pass through to wrapper */
}

.deck-card-item:hover {
    z-index: 1000 !important;
    /* Ensure hovered card is on top of everything */
}

.deck-card-item:hover .deck-builder-card {
    --card-hover-transform: scale(2.0);
    --card-base-transform: scale(1);
    /* Much larger zoom for readability */
    filter: brightness(1.15) drop-shadow(0 10px 30px rgba(0, 0, 0, 0.8));
}

/* The catalog card is PHYSICALLY larger than base size — never a transform
   upscale. `transform: scale()` is a compositor operation: the layer rasters at
   the element's LAYOUT box and is then stretched, so this grid displayed a 74px
   raster blown up to 127.46px and the art was permanently soft. Firefox happens
   to raster at the composited scale and showed no loss at all, which is exactly
   why a Chrome+Firefox desktop matrix never caught it; WebKit at DPR3 — a phone
   — lost 48% of the detail already sitting in the card's own canvas.

   Scaling the UNIT is dimensionally identical to scaling the transform, because
   every card dimension is a multiple of --card-unit — but the art now rasters
   for the size it is actually displayed at. Same trap and same fix as the packs
   fan (.card-packs-cards) and the collection detail card. */
.deck-builder-cards .deck-card-item .deck-builder-card {
    /* The 1vmin fallback is insurance, not the design: the capturing
     ancestor is what carries the phone floor and the 4K counter-scale.
     If a card ever renders outside it, degrade to the desktop unit
     rather than collapsing to an invalid width. */
  --card-unit: calc(var(--catalog-card-unit, 1vmin) * var(--collection-card-scale));
    --card-width: calc(12.29 * var(--card-unit));
    --card-height: calc(22.2 * var(--card-unit));
    /* Hairline and shadow are the only card metrics NOT expressed in
       --card-unit, so the transform used to stretch them along with everything
       else. Scale them explicitly to keep the change purely about sharpness. */
    border-width: calc(1px * var(--collection-card-scale));
    box-shadow: 0 0 calc(8px * var(--collection-card-scale)) rgba(238, 219, 241, 0.5);
    /* `.card`'s own margin is a multiple of --card-unit, which a transform used
       to leave alone (transforms do not affect layout) but physical sizing now
       grows with the card. The tile centres its single child and the grid owns
       spacing via `gap`, so symmetric margins were never load-bearing here —
       zeroing them keeps the card off its tile's edges instead of overflowing
       them. Same reason `.card-packs-cards .deck-builder-card` zeroes its own. */
    margin: 0;
    --card-base-transform: scale(1);
}

/* Over the ART's bottom-right, NOT the card's top-right.

   At the card's corner this pill spanned 41.3%-96.2% of the width while the
   name occupies 32%-93%: it covered 63px of "Enchanted Log"'s actual ink, so
   every card added to a deck lost the name identifying it. The art well runs
   15%-70.5% of the card's height, and the Stellar badge owns its TOP-left, so
   the pill takes the opposite corner and the two never meet. */
.deck-card-item .deck-card-check {
    position: absolute;
    top: auto;
    bottom: 31%;
    right: 9.5%;
    max-width: 62%;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    background: rgba(100, 255, 100, 0.9);
    color: #000;
    border-radius: 50%;
    width: 30px;
    height: 30px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: bold;
    font-size: 1.2rem;
    z-index: 10;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}

.deck-card-item.in-deck {
    opacity: 0.7;
}

.deck-card-item.in-deck .deck-builder-card {
    filter: brightness(0.8);
}

/* Unowned cards - greyed out with lock indicator */
.deck-card-item.unowned {
    opacity: 0.5;
    cursor: not-allowed;
}

.deck-card-item.unowned .deck-builder-card {
    filter: grayscale(0.7) brightness(0.6);
}

.deck-card-item.unowned:hover {
    transform: none;
}

.deck-card-item.unowned:hover .deck-builder-card {
    --card-hover-transform: scale(0.6);
    --card-base-transform: scale(1);
    filter: grayscale(0.5) brightness(0.7);
}

.deck-card-item.balance-banned {
    cursor: not-allowed;
}

.deck-card-item.balance-banned .deck-builder-card {
    filter: grayscale(0.55) brightness(0.65) saturate(0.7);
}

.deck-card-item.balance-banned:hover .deck-builder-card {
    filter: grayscale(0.4) brightness(0.72) saturate(0.8);
}

.deck-card-banned-badge {
    position: absolute;
    left: 50%;
    bottom: 9%;
    z-index: 12;
    transform: translateX(-50%);
    padding: 4px 8px;
    border: 1px solid rgba(255, 186, 186, 0.9);
    border-radius: 999px;
    background: rgba(91, 13, 24, 0.94);
    color: #fff1f2;
    box-shadow: 0 2px 9px rgba(0, 0, 0, 0.62);
    font-size: 0.65rem;
    font-weight: 800;
    letter-spacing: 0.06em;
    line-height: 1;
    text-transform: uppercase;
    white-space: nowrap;
}

.deck-card-mini-banned {
    border-color: rgba(255, 126, 143, 0.72);
}

.deck-card-mini-banned-label {
    color: #ffb9c3;
    font-size: 0.68rem;
    font-weight: 800;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

.deck-card-lock {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    background: rgba(0, 0, 0, 0.7);
    color: rgba(255, 255, 255, 0.8);
    border-radius: 50%;
    width: 40px;
    height: 40px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.5rem;
    z-index: 10;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}

.deck-card-item-stellar .deck-builder-card {
    filter: brightness(1.1) saturate(1.2);
}

/* Over the ART's quiet top-left corner, NOT the card's.

   At the card's corner this chip spanned 3.8%-57.2% of the width while the name
   occupies 32%-93% and the mana pip 11.3%-20.6%: it covered 33px of every name
   and 12px of every pip. With the Stellar filter on — the one tool that finds
   these cards in a collection hundreds deep — not one of them could be read.
   The art well starts at 15% of the card's height and 8% of its width, and the
   mana pip hangs into it to 18.5%, so the badge clears both and is capped so it
   cannot reach the far edge.
   Percentages, not pixels: the card is sized in vmin and scales per surface. */
/* Its BOX scaled with the card and its TEXT did not, so the label ate itself.

   Everything else on this card is proportional — the name runs 6.07px -> 2.04px
   and the cost 23.8px -> 7.97px between a 640px-tall viewport and a 340px one —
   while this badge sat at a flat 0.6rem everywhere. Measured: `scrollWidth` was
   71px at EVERY viewport on the ladder while `clientWidth` tracked the shrinking
   card (71 -> 49 -> 43 -> 31 -> 27), so below roughly 640px of viewport HEIGHT
   `text-overflow: ellipsis` fired and the chip read "✦ ..." — a Stellar banner
   that names nothing. Height, not width, is the trigger: the card is vmin-sized,
   so a short window shrinks it while a narrow one does not.

   So scale the type with the card, exactly like its neighbours. The multipliers
   below reproduce today's values EXACTLY at the reference viewport (1400x640,
   where --catalog-card-unit * --collection-card-scale = 10.746px): 0.8933 ->
   9.6px, 0.1861/0.5583 -> 2px/6px, 0.03722 -> 0.4px, 0.2792 -> 3px. Nothing
   moves where the badge already looked right; below that the ratio simply holds.

   Because the ratio is now constant, the 62% cap can no longer be outgrown: the
   widest shipped label is French "Stellaire" at 80px against an 81.9px cap
   (every other language is 71px or less), and that 2.3% of headroom is preserved
   at every size instead of being lost the moment the card shrinks. `ellipsis` is
   therefore gone — it can no longer trigger, and the owner's call if it somehow
   did is a clipped word over "...", which at least still reads as a word.
   `overflow: hidden` stays so a pathological label clips instead of escaping
   over the art. */
.deck-card-stellar-badge {
    --stellar-chip-unit: calc(
        var(--catalog-card-unit, var(--card-unit, 1vmin))
        * var(--collection-card-scale, var(--catalog-card-scale, 1.05))
    );
    position: absolute;
    /* Below the mana pip, which runs to 18.5% of the card's height. */
    top: 20%;
    left: 9.5%;
    max-width: 62%;
    overflow: hidden;
    white-space: nowrap;
    background: linear-gradient(135deg, #1e4d7a, #3aa7c3);
    color: #e6f6ff;
    border-radius: calc(0.2792 * var(--stellar-chip-unit));
    padding: calc(0.1861 * var(--stellar-chip-unit)) calc(0.5583 * var(--stellar-chip-unit));
    font-size: calc(0.8933 * var(--stellar-chip-unit));
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: calc(0.03722 * var(--stellar-chip-unit));
    z-index: 10;
    box-shadow: 0 1px 6px rgba(0, 0, 0, 0.4);
}

/* Card preview popup for deck list */
.deck-card-preview {
    position: fixed;
    z-index: 10000;
    pointer-events: none;
    filter: drop-shadow(0 10px 30px rgba(0, 0, 0, 0.8));
}

.deck-card-preview .card {
    --card-hover-transform: scale(1.5);
    --card-base-transform: scale(1);
    transform-origin: top left;
}

.deck-card-mini {
    cursor: pointer;
}

.deck-card-mini-stellar {
    border-left: 2px solid rgba(80, 200, 255, 0.8);
    box-shadow: inset 2px 0 6px rgba(30, 140, 200, 0.25);
}

.deck-card-mini-variant {
    margin-left: 6px;
    font-size: 0.8rem;
    color: rgba(120, 220, 255, 0.9);
}

.deck-card-mini:hover {
    background: rgba(255, 255, 255, 0.1) !important;
}

/* Large card hover preview - positioned in viewport */
.card-hover-preview {
    position: fixed;
    z-index: 2147483647;
    pointer-events: auto;
    /* Allow hover on the preview itself */
    /* Depth WITHOUT a filter. An ancestor filter renders the whole card
       subtree into an intermediate surface — Gecko loses subpixel glyph AA
       there and Y-snaps text per tilt frame ("title and mana costs of
       enlarged cards is jittering"), and every engine softens enlarged
       text/art (the same reason game-board.css and mulligan.css already opted
       out). The shadow lives on the CARD, not this host: the host measures
       wider than the card it centres, and a host shadow painted a visible
       rectangle over the Moon Fuse canvas. */
    filter: none;
}

.card-hover-preview > .card {
    box-shadow: 0 20px 50px rgba(0, 0, 0, 0.75);
}

.card-hover-preview .card {
    --card-hover-transform: scale(1);
    --card-base-transform: scale(1);
    transform-origin: center center;
    /* Enlarged previews must arrive at their final transform in one paint.
       Interpolation moves the raster through fractional pixels, which reads
       as a blur-to-sharp shimmer even when the bitmap itself is immutable. */
    transition: none;
}

/* The pack popup TAKES the pointer. It is centred on a fan card and up to 3.5x
   its size, so most of it hangs over the neighbouring cards; letting the
   pointer fall through handed the hover to whichever neighbour sat beneath
   the enlarged card the player was reading ("not switching to unhovered
   cards when cursor is technically above other cards"). The enlarged card
   owns its whole surface; card-packs.js dismisses it only where the pointer
   really leaves it.
   The HOST is the hit region, and it must be exactly the card's FLAT box:
   - flat, because the card tilts under the pointer and its edge recedes a
     few pixels — a hit region that tilted with it lost the popup 6px inside
     its own top edge, and at a side edge would have exposed the neighbour
     beneath (measured in Chrome at 1400x640);
   - exactly the card, because `.card` carries the hand fan's side margin
     (cards.css, `2.64 * --card-unit` = 59px on a 275px popup), and a host
     that shrink-wraps a margin owns an invisible strip on either side. */
.card-hover-preview.card-packs-hover-preview {
    pointer-events: auto;
}

.card-hover-preview.card-packs-hover-preview > .card {
    margin: 0;
}

.card-hover-preview.card-packs-hover-preview .card {
    --card-hover-transform: scale(1);
    --card-base-transform: scale(1);
    transform-origin: center center;
}

@media (max-width: 1100px) {
    .deck-builder-cards {
        --deckbuilder-column-count: 4;
        grid-template-columns: repeat(var(--deckbuilder-column-count), minmax(calc(var(--card-width) * var(--catalog-card-scale)), 1fr));
    }

    .deck-builder-collection .deck-builder-section-header {
        flex-wrap: wrap;
        align-items: flex-start;
    }

    .deck-builder-collection .deck-builder-controls {
        flex: 1 1 100%;
        width: 100%;
        margin-left: 0;
    }

    .cardback-grid {
        grid-template-columns: repeat(5, minmax(0, 1fr));
    }

    .db-character-grid {
        grid-template-columns: repeat(2, minmax(220px, 1fr));
    }
}

@media (max-width: 980px) {
    .collection-controls-row {
        flex-wrap: wrap;
        align-items: flex-start;
    }

    .collection-controls-row .deck-builder-controls {
        flex-wrap: wrap;
        justify-content: flex-start;
        width: 100%;
    }

    .collection-controls-row .deck-builder-search {
        width: 100%;
    }

    .deck-builder-collection .deck-builder-section-header {
        flex-wrap: wrap;
        align-items: flex-start;
    }

    .deck-builder-collection .deck-builder-controls {
        flex-wrap: wrap;
        justify-content: flex-start;
        width: 100%;
        margin-left: 0;
    }

    .deck-builder-collection .deck-builder-search {
        width: 100%;
    }
}

@media (max-width: 640px) {
    .deck-builder-cards {
        --deckbuilder-column-count: 2;
        grid-template-columns: repeat(var(--deckbuilder-column-count), minmax(calc(var(--card-width) * var(--catalog-card-scale)), 1fr));
        gap: 12px;
        row-gap: 16px;
    }

    .cardback-grid {
        grid-template-columns: repeat(5, minmax(0, 1fr));
        gap: 10px;
    }

    .db-character-grid {
        grid-template-columns: 1fr;
    }

    .db-character-card {
        flex-direction: row;
        min-height: 0;
    }

    .db-char-image-wrapper {
        width: 80px;
        height: 80px;
        flex-shrink: 0;
        border-bottom: none;
        border-right: 1px solid rgba(255, 255, 255, 0.1);
    }

    .db-char-info {
        text-align: left;
        padding: 8px 12px;
    }

    .collection-pagination,
    .deck-builder-pagination,
    .cardback-pagination {
        flex-wrap: wrap;
        justify-content: center;
    }
}

/* The popup owns enlargement; the in-grid card never zooms on hover. It stays
   at its physical catalog size (set once above via --card-unit), so hover is
   purely a brightness cue. */
.deck-builder-cards .deck-card-item:hover .deck-builder-card {
    --card-hover-transform: scale(1);
    filter: brightness(1.1);
    box-shadow: 0 0 calc(12px * var(--collection-card-scale)) rgba(238, 219, 241, 0.8);
}

/* ===== Skeleton Loading Cards ===== */

@keyframes skeletonShimmer {
    0% { background-position: -200% 0; }
    100% { background-position: 200% 0; }
}

.skeleton-card {
    border-radius: 12px;
    background: linear-gradient(
        90deg,
        rgba(238, 219, 241, 0.04) 0%,
        rgba(238, 219, 241, 0.10) 40%,
        rgba(238, 219, 241, 0.04) 80%
    );
    background-size: 200% 100%;
    animation: skeletonShimmer 1.6s ease-in-out infinite;
    border: 1px solid rgba(238, 219, 241, 0.08);
    aspect-ratio: 0.554;
    width: 100%;
}

.skeleton-card-inner {
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
    padding: 8%;
    gap: 8%;
    box-sizing: border-box;
}

.skeleton-card-image {
    flex: 1;
    border-radius: 8px;
    background: rgba(238, 219, 241, 0.05);
}

.skeleton-card-title {
    height: 10%;
    border-radius: 6px;
    background: rgba(238, 219, 241, 0.06);
    width: 70%;
}

.skeleton-card-desc {
    height: 16%;
    border-radius: 6px;
    background: rgba(238, 219, 241, 0.04);
    width: 90%;
}

/* ===== Card Detail Redesign ===== */

.collection-card-detail {
    flex: 1;
    min-height: 0;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    gap: 0;
    padding: 0;
    position: relative;
}

/* The already-connected final layout remains measurable underneath the detail
   skeleton until its main and recipe cards have terminal raster paints. */
.collection-card-detail.collection-detail-raster-staging > .collection-detail-skeleton {
    position: absolute;
    inset: 0;
    z-index: 3;
}

.collection-card-detail.collection-detail-raster-staging > .collection-card-detail-header,
.collection-card-detail.collection-detail-raster-staging > .collection-card-detail-body {
    visibility: hidden;
    pointer-events: none;
}

.collection-card-detail-header {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 8px 16px;
    flex-shrink: 0;
}

.collection-card-detail-back {
    flex: 0 0 auto;
    padding: 6px 14px;
    font-size: 0.8rem;
    border-radius: 8px;
}

.collection-card-detail-title {
    flex: 1;
    color: #eedbf1;
    font-size: 1.15rem;
    font-weight: 700;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    letter-spacing: 0.02em;
}

.collection-card-detail-variant {
    padding: 4px 12px;
    border-radius: 999px;
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.14em;
    color: #e8f8ff;
    background: linear-gradient(135deg, rgba(36, 94, 150, 0.95), rgba(78, 190, 220, 0.9));
    border: 1px solid rgba(140, 220, 255, 0.45);
    animation: fadeSlideIn 0.3s ease-out;
}

@keyframes fadeSlideIn {
    from { opacity: 0; transform: translateY(-6px); }
    to { opacity: 1; transform: translateY(0); }
}

/* The detail body contains the physical card. Its automatic entrance is
   opacity-only so the card's raster never passes through a scaled or translated
   ancestor; text-only badges can continue using fadeSlideIn above. */
@keyframes collection-detail-fade-in {
    from { opacity: 0; }
    to { opacity: 1; }
}

/* --- Side-by-side body layout --- */
.collection-card-detail-body {
    display: flex;
    flex-direction: row;
    gap: 32px;
    padding: 8px 28px 16px;
    flex: 1;
    min-height: 0;
    overflow: hidden;
    animation: collection-detail-fade-in 0.25s ease-out;
}

/* --- Left: card showcase --- */
.collection-card-detail-left {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    padding: 8px 0;
    animation: collection-detail-fade-in 0.35s ease-out;
}

.collection-card-detail-card {
    position: relative;
}

.collection-card-detail-card .deck-builder-card {
    /* Physical enlargement: every card-internal metric (fonts, paddings, the
       raster pipeline) derives from --card-unit, so the detail card renders
       crisp at its real size. A transform upscale here displayed a
       layout-size raster stretched 1.25× — permanently soft art on a static
       surface. --card-width/height must be redeclared in this scope: :root
       computes them with ITS unit, so an inherited copy would ignore the
       local override. */
    --card-unit: 1.25vmin;
    --card-width: calc(12.29 * var(--card-unit));
    --card-height: calc(22.2 * var(--card-unit));
}

/* --- Right: info panel --- */
.collection-card-detail-right {
    display: flex;
    flex-direction: column;
    gap: 16px;
    flex: 1;
    min-width: 0;
    overflow-y: auto;
    overflow-x: hidden;
    padding: 8px 4px 8px 0;
    justify-content: center;
    scrollbar-width: thin;
    scrollbar-color: rgba(238, 219, 241, 0.12) transparent;
}

/* Outside short landscape phones this wrapper deliberately contributes no
   layout box, preserving the established desktop/tablet flow. The phone rule
   below turns it into the only scroll owner so the action dock cannot be
   clipped by a browser-chrome-reduced page box. */
.collection-card-detail-info {
    display: contents;
}

/* --- Actions section --- */
.collection-card-detail-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    position: relative;
    overflow: visible;
}

.collection-craft-btn {
    position: relative;
    overflow: hidden;
    transform: translateZ(0);
    min-width: 130px;
    padding: 11px 20px;
    font-size: 0.88rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
}

.collection-craft-btn .craft-icon {
    font-size: 1.05em;
}

.collection-craft-btn.crafting {
    animation: craftButtonPulse 0.45s ease-out;
    box-shadow: 0 0 0 rgba(255, 215, 140, 0.6);
}

.collection-craft-btn.crafting::after {
    content: '';
    position: absolute;
    inset: -40%;
    background: radial-gradient(circle, rgba(255, 227, 165, 0.65), rgba(255, 227, 165, 0) 60%);
    opacity: 0;
    animation: craftButtonGlow 0.55s ease-out;
    pointer-events: none;
}

/* --- Meta info section --- */
.collection-card-detail-meta {
    background: linear-gradient(145deg, rgba(24, 18, 36, 0.7), rgba(32, 24, 46, 0.65));
    border: 1px solid rgba(170, 140, 210, 0.12);
    border-radius: 12px;
    padding: 12px 16px;
    display: flex;
    flex-wrap: wrap;
    gap: 8px 20px;
}

.collection-card-detail-meta-item {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 0.84rem;
    color: rgba(238, 219, 241, 0.85);
}

.collection-card-detail-meta-item .meta-label {
    color: rgba(238, 219, 241, 0.45);
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
}

.collection-card-detail-meta-line {
    color: rgba(238, 219, 241, 0.85);
    font-size: 0.82rem;
}

.collection-card-detail-owner {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 0.82rem;
    color: rgba(238, 219, 241, 0.9);
}

.collection-card-detail-owner-image {
    width: 24px;
    height: 24px;
    border-radius: 50%;
    object-fit: cover;
    border: 1px solid rgba(200, 220, 255, 0.35);
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35);
}

/* --- Stellar status --- */
.collection-card-detail-status {
    padding: 10px 14px;
    border-radius: 12px;
    background: rgba(12, 10, 24, 0.75);
    border: 1px solid rgba(120, 160, 220, 0.2);
    color: rgba(230, 240, 255, 0.9);
    display: flex;
    flex-direction: column;
    gap: 4px;
    position: relative;
    overflow: hidden;
}

.collection-card-detail-status-generating {
    border-color: rgba(140, 200, 255, 0.55);
    box-shadow: 0 0 14px rgba(80, 160, 230, 0.18) inset, 0 0 18px rgba(80, 160, 230, 0.12);
    background:
        radial-gradient(circle at top right, rgba(80, 170, 240, 0.18), transparent 60%),
        rgba(12, 10, 24, 0.78);
}

.collection-card-detail-status-generating::before {
    content: '';
    position: absolute;
    inset: 0;
    background: linear-gradient(120deg,
        transparent 0%,
        transparent 35%,
        rgba(140, 210, 255, 0.18) 50%,
        transparent 65%,
        transparent 100%);
    background-size: 220% 100%;
    animation: stellar-status-shimmer 2.4s linear infinite;
    pointer-events: none;
}

.collection-card-detail-status-ready {
    border-color: rgba(160, 230, 200, 0.45);
}

.collection-card-detail-status-failed {
    border-color: rgba(255, 140, 140, 0.5);
}

@keyframes stellar-status-shimmer {
    0% { background-position: -120% 0; }
    100% { background-position: 220% 0; }
}

.collection-card-detail-status-header {
    display: flex;
    align-items: center;
    gap: 8px;
    position: relative;
    z-index: 1;
}

.collection-card-detail-status-progress {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    flex-shrink: 0;
}

.collection-card-detail-status-spinner {
    width: 16px;
    height: 16px;
    border-radius: 50%;
    border: 2px solid rgba(140, 200, 255, 0.25);
    border-top-color: rgba(180, 230, 255, 0.95);
    border-right-color: rgba(160, 220, 255, 0.85);
    animation: stellar-status-spin 1.05s linear infinite;
    box-shadow: 0 0 6px rgba(120, 200, 255, 0.4);
}

@keyframes stellar-status-spin {
    to { transform: rotate(360deg); }
}

.collection-card-detail-status-title {
    font-size: 0.66rem;
    text-transform: uppercase;
    letter-spacing: 0.14em;
    color: rgba(180, 210, 255, 0.7);
}

.collection-card-detail-status-text {
    font-size: 0.8rem;
    color: rgba(230, 240, 255, 0.85);
    position: relative;
    z-index: 1;
}

.collection-card-detail-status-eta {
    font-size: 0.72rem;
    color: rgba(180, 220, 255, 0.85);
    font-weight: 600;
    letter-spacing: 0.04em;
    position: relative;
    z-index: 1;
}

/* --- Recipe section --- */
.collection-recipe-section {
    width: 100%;
}

.collection-recipe-title {
    color: rgba(238, 219, 241, 0.4);
    font-size: 0.68rem;
    text-transform: uppercase;
    letter-spacing: 0.12em;
    margin-bottom: 10px;
    text-align: center;
}

.collection-recipe-formula {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 14px;
    flex-wrap: wrap;
}

.collection-recipe-parent {
    display: flex;
    flex-direction: column;
    align-items: center;
    cursor: pointer;
    border-radius: 10px;
    padding: 6px;
    background: rgba(255, 255, 255, 0.025);
    border: 1px solid rgba(238, 219, 241, 0.1);
    transition: background 0.2s, border-color 0.2s, transform 0.2s;
}

.collection-recipe-parent:hover {
    background: rgba(255, 255, 255, 0.06);
    border-color: rgba(238, 219, 241, 0.3);
    transform: translateY(-2px);
}

.collection-recipe-parent .deck-builder-card {
    --card-base-transform: scale(0.55);
    transform-origin: center;
    transition: transform 0.2s ease;
}

.collection-recipe-parent:hover .deck-builder-card {
    --card-hover-transform: scale(0.65);
    --card-base-transform: scale(1);
}

.collection-recipe-parent-name {
    color: rgba(238, 219, 241, 0.7);
    font-size: 0.68rem;
    margin-top: 2px;
    text-align: center;
    max-width: 90px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.collection-recipe-operator {
    color: rgba(238, 219, 241, 0.3);
    font-size: 1.4rem;
    font-weight: 300;
    line-height: 1;
    flex-shrink: 0;
    -webkit-user-select: none;
    user-select: none;
}

.collection-recipe-hidden {
    color: rgba(238, 219, 241, 0.4);
    font-size: 0.8rem;
    font-style: italic;
    padding: 8px 0;
    text-align: center;
}

/* --- Skeleton for detail view --- */
.collection-detail-skeleton {
    display: flex;
    flex-direction: row;
    gap: 24px;
    padding: 16px 20px;
    flex: 1;
    animation: fadeSlideIn 0.2s ease-out;
}

.skeleton-detail-card {
    width: 160px;
    aspect-ratio: 0.554;
    border-radius: 12px;
    flex-shrink: 0;
    background: linear-gradient(
        90deg,
        rgba(238, 219, 241, 0.04) 0%,
        rgba(238, 219, 241, 0.10) 40%,
        rgba(238, 219, 241, 0.04) 80%
    );
    background-size: 200% 100%;
    animation: skeletonShimmer 1.6s ease-in-out infinite;
}

.skeleton-detail-right {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.skeleton-detail-actions {
    display: flex;
    gap: 8px;
}

.skeleton-detail-btn {
    width: 110px;
    height: 36px;
    border-radius: 10px;
    background: linear-gradient(
        90deg,
        rgba(238, 219, 241, 0.04) 0%,
        rgba(238, 219, 241, 0.08) 40%,
        rgba(238, 219, 241, 0.04) 80%
    );
    background-size: 200% 100%;
    animation: skeletonShimmer 1.6s ease-in-out infinite;
}

.skeleton-detail-meta {
    width: 80%;
    height: 50px;
    border-radius: 12px;
    background: linear-gradient(
        90deg,
        rgba(238, 219, 241, 0.03) 0%,
        rgba(238, 219, 241, 0.07) 40%,
        rgba(238, 219, 241, 0.03) 80%
    );
    background-size: 200% 100%;
    animation: skeletonShimmer 1.6s ease-in-out infinite;
}

/* --- Responsive: stack on small screens --- */
@media (max-width: 900px) {
    .collection-card-detail-body {
        flex-direction: column;
        align-items: center;
        padding: 8px 12px 16px;
        overflow-y: auto;
        gap: 16px;
    }

    .collection-card-detail-left {
        flex-shrink: 1;
    }

    .collection-card-detail-right {
        overflow: visible;
        width: 100%;
    }

    .collection-recipe-parent .deck-builder-card {
        --card-base-transform: scale(0.45);
    }

    .collection-card-detail-card .deck-builder-card {
        --card-unit: 1vmin;
        --card-width: calc(12.29 * var(--card-unit));
        --card-height: calc(22.2 * var(--card-unit));
    }

    .collection-detail-skeleton {
        flex-direction: column;
        align-items: center;
    }
}

@media (max-width: 600px) {
    .collection-card-detail-card .deck-builder-card {
        --card-unit: 0.8vmin;
        --card-width: calc(12.29 * var(--card-unit));
        --card-height: calc(22.2 * var(--card-unit));
    }

    .collection-craft-btn {
        min-width: 100px;
        padding: 8px 12px;
        font-size: 0.8rem;
    }
}

/* ============================================================================
 * MOBILE PHONE OVERRIDES — Card Packs / Cardbacks / Moon Exchange (BODY only)
 * ----------------------------------------------------------------------------
 * Scope: the three collection-modal tabs that had NO phone layer and were pure
 * "dumb resizes" of desktop @media(max-width:900/max-height:820/pointer:coarse)
 * fallbacks. Everything below is inside a phone-viewport gate, so desktop stays
 * byte-identical.
 *
 * Cascade notes (why the selectors look the way they do):
 *   - This block is appended at the END of the file, so at EQUAL specificity it
 *     wins over this file's own desktop @media fallbacks above (source order).
 *   - mobile.css loads AFTER this file and is the cross-file cascade authority.
 *     It already owns the collection MODAL CHROME (header/subtitle/tabs), the
 *     generic .collection-content scroll, the catalog card-scale, and the
 *     .cardback-grid / .cardback-item base — those are NOT duplicated here.
 *   - The one body rule we must beat in mobile.css (the cardback grid layout,
 *     mobile.css `.cardback-grid` 0,0,1,0) is overridden with a higher-
 *     specificity `.collection-modal .cardback-grid` (0,0,2,0) so it wins
 *     regardless of load order.
 *   - --mf-touch (44px) is defined by mobile.css :root inside the same gate; a
 *     `, 44px` fallback keeps these rules self-contained.
 * ==========================================================================*/
@media (orientation: portrait) and (max-width: 768px),
       (orientation: landscape) and (max-height: 560px) {

    /* ---------- Card Packs --------------------------------------------------
     * The desktop fallbacks column-stack the header (~180-220px), floor the
     * middle grid row at 280px + a clamp(300px,50svh,430px) stage, and pin a
     * fixed 200-270px pack — the vault overflows a ~400px landscape phone and
     * the packs-active container (overflow:hidden) CLIPS the Buy/Convert CTAs.
     * Compact every band, size the pack by vmin, and declutter flavor chrome. */

    /* drop the 280px hard floor on the stage grid row so it can collapse */
    .card-packs-shell {
        padding: 12px;
        gap: 8px;
        grid-template-rows: auto minmax(0, 1fr) auto;
    }

    /* declutter: eyebrow duplicates the tab label; subtitle is flavor.
     * .card-packs-odds is deliberately NOT in this list — the published odds
     * are a disclosure, and a disclosure that disappears on a phone is the
     * same as no disclosure at all. */
    .card-packs-eyebrow,
    .card-packs-subtitle {
        display: none;
    }

    /* keep the odds on their own full-width line so the cramped landscape
     * header row does not squeeze them into a column of single words */
    .card-packs-odds {
        width: 100%;
        font-size: 0.68rem;
    }

    /* header is a compact wrapped row, not a full-height column stack */
    .card-packs-header {
        flex-direction: row;
        flex-wrap: wrap;
        align-items: center;
        gap: 8px 12px;
    }

    .card-packs-title h3 {
        font-size: 1.05rem;
    }

    .card-packs-header-actions {
        width: 100%;
        justify-content: space-between;
        gap: 8px;
    }

    /* Keep the exchange rules in the action sequence on phones. Its full-width
       row comes after Exchange and before Buy, using the same quiet typography
       as the pack odds while its teal keeps the ownership unambiguous. */
    .card-packs-duplicates {
        order: 1;
    }

    .card-packs-buy-btn {
        order: 3;
    }

    /* duplicates: one horizontal chip-row (count + convert), no 3-line stack */
    .card-packs-duplicates {
        flex-direction: row;
        align-items: center;
        flex-wrap: wrap;
        width: auto;
        gap: 8px;
        padding: 6px 10px;
    }

    /* lift the sub-14px functional text toward the readability floor */
    .card-packs-count {
        font-size: 0.72rem;
    }

    /* the only interactive controls on the screen — meet the 44px touch floor */
    .card-packs-buy-btn,
    .card-packs-duplicates-btn,
    .card-packs-open-btn {
        min-height: var(--mf-touch, 44px);
        display: inline-flex;
        align-items: center;
        justify-content: center;
        font-size: 0.78rem;
        /* keep the label on ONE line (it dropped to 2-3 lines at 736/667). The
           parent's flex-wrap moves the whole CTA to its own row when needed. */
        white-space: nowrap;
    }

    /* stage: drop the oversized floor (later source order beats the 900/820
     * desktop rules), keep a small floor so the pack is never crushed */
    .card-packs-stage {
        min-height: clamp(120px, 28svh, 230px);
    }

    /* pack: size by vmin so it fits the shrunken stage in both orientations */
    .card-packs-pack {
        width: clamp(100px, 26vmin, 150px);
        height: auto;
    }

    #collection-cards-prev,
    #collection-cards-next,
    #cardback-prev,
    #cardback-next,
    #db-character-prev,
    #db-character-next,
    #deck-builder-prev,
    #deck-builder-next,
    #saved-deck-prev,
    #saved-deck-next {
        min-height: var(--mf-touch, 44px);
    }

    /* ---------- Cardbacks (body) -------------------------------------------
     * mobile.css set the grid to auto-fill minmax(88px,1fr): a page only ever
     * holds 5 items (CARDBACKS_PER_PAGE), so the items pack LEFT and leave empty
     * 1fr tracks (off-centre dead space, both orientations). auto-fit collapses
     * empty tracks and a capped (non-1fr) max lets justify-content center the
     * partial row. Higher specificity (.collection-modal .cardback-grid) beats
     * mobile.css's later-loading .cardback-grid. */
    .collection-modal .cardback-grid {
        /* ONE size for every cardback, owned or not. A locked item used to get
           an explicit clamp(56px, 18vmin, 62px) wrapper while an owned one
           filled a 84-116px track, so the same object was two different sizes
           in the same grid depending only on whether the player had it. The
           grid now carries the smaller (locked) size for everything, which is
           the size the owner asked to standardise on, and the locked override
           is gone so nothing can diverge again. */
        --collection-cardback-track-width: clamp(56px, 18vmin, 62px);
        grid-template-columns: repeat(auto-fit, minmax(var(--collection-cardback-track-width), 62px));
        justify-content: center;
        gap: 12px;
    }

    /* Equip + Prev/Next were ~40px .deck-btns — the two most-tapped controls */
    .cardback-item .select-cardback-btn,
    #cardback-prev,
    #cardback-next {
        min-height: var(--mf-touch, 44px);
    }

    /* 0.72em desc rendered ~11.5px — below the readable floor (portrait) */
    .cardback-desc {
        font-size: 0.8rem;
        line-height: 1.3;
    }

    /* ---------- Moon Exchange (body) ---------------------------------------
     * Zero phone rules before now: it rode the desktop 900/820/coarse
     * fallbacks. The desktop grid minmax(260px,1fr) minmax(320px,1.4fr) and the
     * 220/150px control min-widths overflow a 390px phone unless an unrelated
     * desktop breakpoint happens to match. Own the phone layout explicitly. */

    /* One task per portrait pane; short landscape overrides this with the
       side-by-side composer/market workspace below. */
    /* Bound the exchange so the PAGE never scrolls: on iOS a page scroll
       collapses the URL bar, which changes the viewport height mid-gesture and
       re-runs every size derived from it — the player sees the board reflow
       under their finger instead of moving. This surface is paginated by
       design rather than scrolled, so the fix is to make it fit and hold
       still, not to add a scroll lane. */
    .moon-exchange-grid {
        grid-template-columns: 1fr;
        grid-template-rows: minmax(0, 1fr);
        flex: 1 1 auto;
        min-height: 0;
        gap: 12px;
    }

    .moon-exchange-panel {
        padding: 12px;
        min-height: 0;
    }

    .moon-exchange-listings {
        overflow: hidden;
        flex: 1 1 auto;
        min-height: 0;
        padding: 0;
    }

    /* Keep the compact preview beside the fields. The wrapper fits the pane;
       the physical card itself still uses the shared renderer and transform. */
    .moon-exchange-sell-row {
        grid-template-columns: 120px minmax(0, 1fr);
        gap: 12px;
        align-items: start;
    }

    /* Pair UNIT PRICE + QUANTITY side by side so the short numeric inputs don't
       stretch the full panel width (a "10" floating in a ~700px field). */
    .moon-exchange-sell-fields {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 10px 12px;
    }

    .moon-exchange-sell-preview {
        margin: 0;
        background: linear-gradient(160deg, rgba(46, 34, 74, 0.55), rgba(10, 8, 20, 0.6));
        border-color: rgba(238, 219, 241, 0.22);
    }

    /* Portrait stacks the controls; short landscape replaces this with one
       compact row. Zero the desktop min-widths in both modes. */
    .moon-exchange-market-controls {
        display: grid;
        grid-template-columns: minmax(0, 1fr);
        align-items: stretch;
        gap: 8px;
    }

    .moon-exchange-market-controls .deck-builder-search {
        min-width: 0;
    }

    #moon-exchange-owner-filter,
    #moon-exchange-sort {
        min-width: 0;
    }

    /* Keep the actual card, listing identity, total and action in one complete
       row. Cursor pagination controls the number of rows that are rendered. */
    .moon-exchange-listing {
        grid-template-columns: minmax(0, 1fr) auto;
        align-items: center;
    }

    .moon-exchange-buy {
        width: auto;
        align-items: end;
    }

    .moon-exchange-buy-qty {
        width: 54px;
    }

    /* touch ergonomics on VIEWPORT (not only pointer:coarse) so selects, search
     * and number fields hit 44px / 16px in an emulator and on phones alike */
    .moon-exchange-form .deck-builder-search,
    .moon-exchange-form .deck-filter,
    .moon-exchange-market-controls .deck-builder-search,
    #moon-exchange-card-select,
    #moon-exchange-owner-filter,
    #moon-exchange-sort {
        min-height: var(--mf-touch, 44px);
        font-size: 16px;
    }

    .moon-exchange-pagination .btn {
        min-height: var(--mf-touch, 44px);
    }
}

/* ----------------------------------------------------------------------------
 * Landscape phones only (short viewport ≤560px tall). Extra compaction so the
 * primary CTA never needs a scroll. Shares the gate with mobile.css's own
 * landscape Moon Dream / collection block — desktop windows ≥561px tall never
 * match, and width-only queries deliberately avoided (they miss 845x400).
 * --------------------------------------------------------------------------*/
@media (orientation: landscape) and (max-height: 560px) {

    /* Collection's outer navigation is already a complete 44px command bar.
       Deck Builder needs the rest of the short viewport for its two card
       canvases, so remove the desktop tab margin and redundant block padding
       only while this section is active. Other Collection sections retain
       their established chrome. */
    .collection-modal.collection-deckbuilder-active > .collection-container > .deck-builder-header {
        padding-block: 4px;
    }

    .collection-modal.collection-deckbuilder-active > .collection-container > .deck-builder-header > .collection-tabs {
        margin-bottom: 0;
        padding-block: 0;
    }

    .deck-builder-container.deck-builder-embedded.deck-builder-root-mode {
        display: grid;
        grid-template-columns: minmax(0, 1fr) minmax(180px, 28%);
        grid-template-rows: minmax(0, 1fr);
        gap: 8px;
        overflow: hidden;
    }

    .deck-builder-root-mode .deck-builder-character-select {
        display: grid;
        grid-template-rows: minmax(0, 1fr) auto;
        grid-column: 1;
        grid-row: 1;
        min-height: 0;
        overflow: hidden;
    }

    .deck-builder-root-mode .deck-builder-saved {
        grid-column: 2;
        grid-row: 1;
        display: flex;
        flex-direction: column;
        min-height: 0;
        overflow: hidden;
        padding: 8px;
        border: 1px solid rgba(238, 219, 241, 0.16);
        border-radius: 10px;
    }

    .deck-builder-root-mode .deck-builder-saved .deck-list {
        flex: 1;
        min-height: 0;
        overflow: hidden;
        flex-direction: column;
        flex-wrap: nowrap;
    }

    /* The saved lane resolves to 180px on a 667px landscape phone. The generic
       flex pager's 12px gaps plus 16px button padding made its focusable Next
       control wider than that lane; focusing it then scrolled the overflow-
       hidden panel sideways and clipped every page-two label. Give all three
       controls explicit shrink tracks while preserving 44px touch buttons. */
    .deck-saved-pagination {
        display: grid;
        grid-template-columns: minmax(44px, 1fr) minmax(0, auto) minmax(44px, 1fr);
        align-items: center;
        gap: 4px;
        width: 100%;
        min-width: 0;
        box-sizing: border-box;
    }

    .deck-saved-pagination .deck-btn {
        width: 100%;
        min-width: 0;
        padding: 8px 2px;
        font-size: 0;
        line-height: 1;
    }

    .deck-saved-pagination #saved-deck-prev::before {
        content: '‹';
        font-size: 20px;
    }

    .deck-saved-pagination #saved-deck-next::before {
        content: '›';
        font-size: 20px;
    }

    .deck-saved-pagination .deck-builder-page-info {
        min-width: 0;
        text-align: center;
        font-size: 0.72rem;
        line-height: 1.1;
        letter-spacing: 0;
        white-space: normal;
        overflow-wrap: anywhere;
    }

    .db-character-grid {
        grid-template-columns: repeat(4, minmax(0, 1fr));
        grid-template-rows: minmax(0, 1fr);
        align-items: center;
        gap: 8px;
        min-height: 0;
        height: 100%;
        padding: 0;
        overflow: hidden;
    }

    .db-character-card {
        min-width: 0;
        height: auto;
        flex-direction: column;
    }

    .db-char-image-wrapper {
        width: 100%;
        aspect-ratio: 16 / 9;
        height: auto;
        flex: 0 0 auto;
        border-right: 0;
        border-bottom: 1px solid rgba(255, 255, 255, 0.1);
    }

    .db-char-info {
        padding: 6px;
    }

    .db-char-name {
        font-size: clamp(12px, 3.1vmin, 15px);
    }

    .db-char-desc {
        display: none;
    }

    .db-character-pagination {
        flex: 0 0 auto;
        margin-top: 4px;
        padding: 0;
    }

    /* The editor is a two-pane canvas on a wide, short phone. The paginated
       catalog stays beside a fixed 5x3 deck map, so every selected card is visible
       at once and neither pane becomes a nested scroll surface. */
    .collection-section-deckbuilder .deck-builder-content {
        display: grid;
        grid-template-columns: minmax(0, 1fr) minmax(238px, 42%);
        grid-template-rows: minmax(0, 1fr);
        gap: 8px;
        min-height: 0;
        overflow: hidden;
        padding: 4px 0;
    }

    .collection-section-deckbuilder .deck-builder-collection,
    .collection-section-deckbuilder .deck-builder-deck {
        width: auto;
        min-width: 0;
        min-height: 0;
        height: 100%;
    }

    .collection-section-deckbuilder .deck-builder-deck {
        grid-template-columns: minmax(0, 1fr) minmax(96px, 42%);
        grid-template-rows: 44px minmax(0, 1fr) 44px;
        gap: 4px;
        max-height: none;
        margin-top: 0;
        padding: 4px;
    }

    .collection-section-deckbuilder .deck-builder-deck-cards {
        grid-column: 1 / -1;
        grid-row: 2;
        grid-template-columns: repeat(5, minmax(44px, 1fr));
        grid-template-rows: repeat(3, minmax(44px, 1fr));
        grid-auto-rows: 0;
        align-content: stretch;
        gap: 2px;
        min-width: 0;
        min-height: 0;
        margin: 0;
        overflow: hidden;
        touch-action: manipulation;
    }

    .collection-section-deckbuilder .deck-card-mini {
        position: relative;
        width: 100%;
        height: 100%;
        min-width: 44px;
        min-height: 44px;
        padding: 0;
        overflow: hidden;
        box-sizing: border-box;
    }

    .collection-section-deckbuilder .deck-card-mini::before {
        background: linear-gradient(180deg, rgba(8, 6, 20, 0.06) 18%, rgba(8, 6, 20, 0.22) 48%, rgba(8, 6, 20, 0.94) 100%);
    }

    .collection-section-deckbuilder .deck-card-mini-art {
        position: absolute;
        inset: 0;
        width: 100%;
        height: 100%;
        min-height: 0;
    }

    .collection-section-deckbuilder .deck-card-mini-name {
        position: absolute;
        left: 3px;
        right: 13px;
        bottom: 3px;
        z-index: 2;
        min-width: 0;
        display: -webkit-box;
        overflow: hidden;
        -webkit-box-orient: vertical;
        -webkit-line-clamp: 2;
        white-space: normal;
        overflow-wrap: anywhere;
        font-size: 9px;
        line-height: 1.05;
        font-weight: 700;
        text-shadow: 0 1px 3px #000, 0 0 5px rgba(0, 0, 0, 0.9);
    }

    .collection-section-deckbuilder .deck-card-mini-cost {
        top: 3px;
        left: 3px;
        width: 18px;
        height: 18px;
        font-size: 10px;
    }

    .collection-section-deckbuilder .deck-card-mini-variant {
        position: absolute;
        right: 3px;
        bottom: 1px;
        z-index: 2;
        margin: 0;
        font-size: 9px;
        pointer-events: none;
    }

    /* A full 15-card deck needs 136px of lane (three 44px rows + gaps) and the
       844x288 / 667x275 page boxes give it 123. The overflow was `hidden`, so
       the bottom row was cut 13px short with no way to reach it — five selected
       cards the player could neither read nor remove. This is the same defect,
       and the same fix, an earlier round already shipped for the read-only deck
       viewer: the grid becomes the screen's one intentional scroll lane. Taller
       page boxes (915x336, 852x344) still fit all 15 and show no scrollbar. */
    .collection-section-deckbuilder .deck-builder-deck-cards {
        overflow-y: auto;
        -webkit-overflow-scrolling: touch;
        overscroll-behavior: contain;
        /* pinch-zoom too: the lane holds cards, and a pinch that touches a
           card is the phone's magnifier (touch-policy.css). */
        touch-action: pan-y pinch-zoom;
    }

    /* The tile's 1px border was costing the touch target 2px it did not have.
       `inset: 0` anchors the Remove overlay to the PADDING box, so on a 44px
       tile the control had 42px to sit in; `min-height: 44px` then pushed 1px
       of it out past the tile and left only 43px actually on the card — one
       pixel under the floor it exists to guarantee, at the 844x288 page box.
       The border becomes an inset ring: pixel-identical, but it no longer
       takes layout, so the padding box IS the 44px tile. */
    .collection-section-deckbuilder .deck-card-mini {
        border-width: 0;
        box-shadow:
            inset 0 0 0 1px rgba(238, 219, 241, 0.12),
            inset 0 1px rgba(255, 255, 255, 0.035),
            0 2px 7px rgba(0, 0, 0, 0.16);
    }

    /* The transparent full-tile Remove control makes every 44px deck cell a
       reliable touch target. The physical card identity remains visible below
       it and tap inspection is still owned by TouchPreview. */
    .collection-section-deckbuilder .deck-card-remove {
        position: absolute;
        z-index: 3;
        inset: 0;
        width: 100%;
        height: 100%;
        min-width: 44px;
        min-height: 44px;
        margin: 0;
        padding: 0;
        color: transparent;
        background: transparent;
        -webkit-backdrop-filter: none;
        backdrop-filter: none;
    }

    .collection-section-deckbuilder .deck-card-remove::after {
        content: '\00d7';
        position: absolute;
        top: 0;
        right: 3px;
        color: rgba(255, 130, 130, 0.9);
        font-size: 13px;
        line-height: 1;
        pointer-events: none;
    }

    .collection-section-deckbuilder .deck-card-remove:focus-visible {
        outline: 3px solid #f6e27a;
        outline-offset: -3px;
        border-radius: 6px;
    }

    .collection-section-deckbuilder .deck-builder-deck > .deck-builder-section-header {
        grid-column: 1;
        grid-row: 1;
        display: block;
        align-items: center;
        margin: 0;
        min-width: 0;
    }

    .collection-section-deckbuilder .deck-builder-deck > .deck-builder-section-header h3 {
        position: absolute;
        width: 1px;
        height: 1px;
        padding: 0;
        margin: -1px;
        overflow: hidden;
        clip: rect(0 0 0 0);
        clip-path: inset(50%);
        white-space: nowrap;
        border: 0;
    }

    .collection-section-deckbuilder .deck-name-input {
        min-width: 0;
        width: 100%;
    }

    .collection-section-deckbuilder .deck-builder-deck-info {
        grid-column: 2;
        grid-row: 1;
        display: grid;
        grid-template-areas:
            "character count"
            "status status";
        grid-template-columns: minmax(0, 1fr) auto;
        align-items: center;
        gap: 2px 4px;
        min-width: 0;
        margin: 0;
        padding: 1px 2px;
        overflow: hidden;
    }

    .collection-section-deckbuilder #selected-character-display {
        grid-area: character;
        min-width: 0;
        max-width: 100%;
        margin-right: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .collection-section-deckbuilder .deck-card-count-summary {
        grid-area: count;
        white-space: nowrap;
    }

    .collection-section-deckbuilder .deck-status {
        grid-area: status;
        min-width: 0;
        margin-left: 0;
        overflow-wrap: anywhere;
        font-size: 9px;
        line-height: 1.05;
    }

    .collection-section-deckbuilder .deck-builder-actions {
        grid-column: 1 / -1;
        grid-row: 3;
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 4px;
        min-width: 0;
    }

    .collection-section-deckbuilder .deck-builder-actions .deck-btn {
        min-width: 0;
        min-height: var(--mf-touch, 44px);
        padding: 8px 4px;
        white-space: nowrap;
    }

    .collection-section-deckbuilder .deck-builder-collection > .deck-builder-section-header {
        display: grid;
        grid-template-columns: auto minmax(0, 1fr);
        align-items: center;
        gap: 2px;
        margin-bottom: 4px;
    }

    .collection-section-deckbuilder .deck-builder-tabs {
        min-width: 0;
        gap: 0;
        margin-bottom: 0;
    }

    .collection-section-deckbuilder .deck-builder-tabs .deck-tab {
        min-width: 0;
        padding: 4px 6px;
        font-size: 12px;
    }

    .collection-section-deckbuilder .deck-builder-collection .deck-builder-controls {
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        width: 100%;
        gap: 4px;
        margin: 0;
    }

    .collection-section-deckbuilder .deck-builder-collection :is(.deck-builder-search, .deck-filter) {
        width: 100%;
        min-width: 0;
    }

    .collection-section-deckbuilder .deck-builder-collection .deck-filter {
        overflow: hidden;
        text-overflow: ellipsis;
        font-size: 16px;
    }

    #filter-collection,
    #filter-mana,
    #sort-cards {
        min-width: 0;
    }

    .collection-section-deckbuilder .deck-builder-cards {
        --catalog-card-scale: var(--deckbuilder-fit-card-scale, 1.28);
        min-height: 0;
        overflow: hidden;
        padding: 0 4px;
        row-gap: 4px;
    }

    .collection-section-deckbuilder .deck-builder-cards .deck-card-item {
        min-width: 44px;
        /* A live viewport refit changes wrapper width/height. The legacy
           transition: all would animate those dimensions from the previous
           viewport and visibly push the second row through pagination. */
        transition: none;
    }

    .collection-section-deckbuilder .deck-builder-cards .deck-card-item .deck-builder-card {
        /* The shared physical card also transitions transform. Without this
           compact override, its pixels lag behind the already-refitted shell
           during a live viewport change and briefly cross the pager boundary. */
        transition: none;
    }

    .collection-section-deckbuilder .deck-builder-cards .deck-card-item.in-deck {
        /* The selected badge and inset outline already communicate state. The
           legacy outer glow enlarged scrollHeight by two painted pixels at the
           shortest anchors, creating phantom clipped overflow in a fixed
           canvas even though both physical rows fit. */
        box-shadow: none;
    }

    .collection-section-cardbacks {
        gap: 8px;
    }

    .collection-section-cardbacks .cardback-grid {
        align-items: start;
    }

    .cardback-item {
        padding: 4px;
        max-height: 100%;
        box-sizing: border-box;
    }

    .cardback-pagination {
        padding-top: 0;
    }

    /* ---------- Cardbacks: height-driven art keeps Equip above the fold -----
     * base .cardback-image-wrapper is width:100% + aspect-ratio:9/16, so the
     * card height is dictated by track width and a full 9/16 portrait card +
     * name + desc + Equip overflows a ~400px landscape viewport. Size the art
     * by available HEIGHT instead (aspect-ratio derives the width), drop the
     * tiny desc + section subtitle, and slim the section title. Modal chrome is
     * already slimmed by mobile.css's landscape block — not duplicated here. */
    .collection-cardbacks-subtitle {
        display: none;
    }

    .collection-cardbacks-header h3 {
        font-size: 1rem;
    }

    .cardback-desc {
        display: none;
    }

    .cardback-image-wrapper {
        /* Cap the art by height on a short viewport, but give it an EXPLICIT
           width — width:auto collapses to ~0 here (the child img is width:100%,
           so an auto-width wrapper shrink-wraps to nothing and the art
           vanishes). Explicit width + aspect-ratio derives the height. 46-54px
           wide → roughly 82-96px tall, which keeps the Equip action above a
           ~400px landscape fold (a 84px/150px card left it flush to the edge). */
        width: clamp(46px, 13vmin, 54px);
        height: auto;
        aspect-ratio: 9 / 16;
        margin: 0 auto 4px;
    }

    /* The desktop badge is wider than the height-driven 46–54px phone art and
       was clipped to “elected”. Keep the non-interactive status fully inside
       the physical cardback while preserving the artwork's exact aspect. */
    .cardback-selected-badge {
        top: 3px;
        right: 3px;
        max-width: calc(100% - 6px);
        padding: 2px 3px;
        box-sizing: border-box;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        font-size: clamp(8px, 2vmin, 10px);
    }

    .cardback-name {
        margin-bottom: 0;
        display: -webkit-box;
        min-height: 2.4em;
        max-height: 2.4em;
        -webkit-box-orient: vertical;
        -webkit-line-clamp: 2;
        line-clamp: 2;
        overflow: hidden;
    }

    .cardback-lock-overlay {
        padding: 4px;
        overflow: hidden;
    }

    .cardback-lock-overlay .lock-icon {
        display: none;
    }

    .cardback-lock-overlay .lock-hint {
        min-width: 0;
        max-width: 100%;
        max-height: 100%;
        overflow: hidden;
        overflow-wrap: anywhere;
        font-size: 0.65rem;
        line-height: 1.1;
    }

    /* ---------- Card Packs: fit the sealed pack in the short landscape stage --
     * The wide-but-short landscape stage collapses to ~120-150px; the vmin pack
     * (up to ~170px) plus the packFloat lift (translateY -8/-14px) overhangs the
     * top and .card-packs-stage{overflow:hidden} clips it. Give the stage a
     * little more of the spare vertical budget, cap the pack by HEIGHT (the 3/4
     * aspect-ratio derives the width), and neutralise the idle float/lift so the
     * centred pack has real clearance. The float is removed ONLY for the idle
     * sealed pack — the opened/disabled reveal transforms are excluded via :not
     * so the charge animation is untouched. */
    .card-packs-shell {
        padding: 8px;
        gap: 6px;
    }

    /* The tab and modal title already name this destination. Removing the
       decorative in-vault title lets all actionable pack state share one
       touch-height row and leaves the stage the rest of the bounded pane. */
    .card-packs-title {
        display: none;
    }

    /* The published odds are NOT part of that decorative block: the shortest
       layout we ship still has to state what the player is buying. */
    .card-packs-odds {
        font-size: 0.62rem;
        line-height: 1.3;
    }

    .card-packs-header-actions {
        display: grid;
        grid-template-columns: auto minmax(0, 1fr) 180px;
        width: 100%;
        min-width: 0;
        gap: 6px;
    }

    .card-packs-duplicates {
        display: grid;
        grid-template-columns: minmax(132px, 0.9fr) minmax(140px, 1.3fr);
        align-items: stretch;
        min-width: 0;
        flex-wrap: nowrap;
        gap: 6px;
        padding: 0 6px;
    }

    .card-packs-duplicates-btn,
    .card-packs-buy-btn {
        min-width: 0;
        white-space: normal;
        overflow-wrap: normal;
        word-break: normal;
        hyphens: none;
        line-height: 1.15;
    }

    .card-packs-duplicates-btn {
        width: 100%;
    }

    /* .deck-btn is declared later than the vault skin and removes borders.
       Restore the Exchange control's physical edge in its final state,
       including when insufficient copies legitimately disable it. */
    .card-packs-duplicates-btn,
    .card-packs-duplicates-btn:disabled {
        border: 1px solid rgba(151, 255, 235, 0.9);
        font-weight: 800;
        transition: filter 0.2s ease, background-color 0.2s ease,
            border-color 0.2s ease, box-shadow 0.2s ease;
    }

    .card-packs-stage {
        min-height: min(136px, 100%);
    }

    .card-packs-pack {
        width: auto;
        height: clamp(88px, 27svh, 150px);
    }

    /* The ribbon's base clamp already scales it down here; the widened
     * ::after hit area owns the touch floor. */
    .card-packs-pack-seal {
        font-size: 0.5rem;
    }

    .card-packs-pack:not(.card-packs-pack-opened):not(.card-packs-pack-disabled):not(.card-packs-pack-uncharged):not(.card-packs-pack-charging) {
        /* Half amplitude, not 30%: a charged pack needs a visible 4-7px breath
           on a phone while the stage's clearance still absorbs it. Inventory
           awaiting Charge is intentionally absent from the physical stage. */
        --pack-float: 0.5;
        transform: translateY(-2px);
    }

    /* ---------- Moon Exchange: one humane task at a time --------------------
     * Browse and Sell share a small switcher instead of permanent sidebars. The
     * active task receives the full landscape width and cursor pagination only
     * renders complete listing tiles; neither task introduces a scroll lane. */
    .collection-section-exchange {
        overflow: hidden;
        min-height: 0;
        gap: 4px;
        padding-bottom: 4px;
    }

    .moon-exchange-command-bar {
        position: relative;
        display: grid;
        grid-template-columns: auto minmax(96px, 1fr) auto auto;
        align-items: center;
        gap: 4px;
        min-width: 0;
        min-height: 44px;
        margin: 0;
        flex: 0 0 auto;
    }

    .moon-exchange-view-switcher {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, auto));
        gap: 2px;
        min-width: 0;
    }

    .moon-exchange-view-btn {
        border: none;
        border-bottom: 2px solid transparent;
        border-radius: 0;
        background: transparent;
        min-width: 0;
        min-height: var(--mf-touch, 44px);
        padding: 4px 6px;
        font-size: 11px;
        line-height: 1.05;
        white-space: nowrap;
    }

    .moon-exchange-view-btn.is-active {
        border-bottom-color: #d3b3ff;
        background: transparent;
    }

    .moon-exchange-grid {
        grid-template-columns: minmax(0, 1fr);
        grid-template-rows: minmax(0, 1fr);
        flex: 1 1 auto;
        min-height: 0;
        gap: 8px;
    }

    .moon-exchange-grid[data-mobile-pane="market"] .moon-exchange-sell,
    .moon-exchange-grid[data-mobile-pane="sell"] .moon-exchange-market {
        display: none;
    }

    /* Vertical padding is the most expensive thing on this breakpoint and the
       least useful. At the iPhone SE page box (667x275) the market panel is
       153px tall and 18px of that went to its own top/bottom padding, leaving
       the listing lane 95px — one pixel BELOW the 96px that two 46px listing
       rows and their 4px gap need. Reclaiming it is half the fix for the Buy
       control that was cut off there; the other half is the stray `.btn` margin
       removed below, which had inflated the row from 46px to 54px.
       Horizontal padding is kept; the frame is drawn by the border. */
    .moon-exchange-panel {
        min-height: 0;
        padding: 2px 8px;
    }

    /* Browse gets one deliberate toolbar row. Summary/Refresh and
       Search/Filters are peer cells, leaving a complete card row plus the
       pager instead of spending half the canvas on stacked chrome. */
    .moon-exchange-market {
        display: grid;
        grid-template-columns: minmax(0, 1fr);
        /* The pager buttons are mandatory 44px touch targets. Giving their
           track only 36px made the panel itself scroll by two rounded pixels
           at the 844x288 page box even though every visible child fit. */
        grid-template-rows: minmax(0, 1fr) var(--mf-touch, 44px);
        row-gap: 4px;
    }

    .moon-exchange-market .moon-exchange-panel-header,
    .moon-exchange-sell > .moon-exchange-panel-header {
        display: none;
    }

    .moon-exchange-panel-header {
        flex-wrap: nowrap;
        gap: 6px;
        margin-bottom: 6px;
    }

    .moon-exchange-panel-header h4 {
        flex: 0 0 auto;
        white-space: nowrap;
    }

    .moon-exchange-summary-actions {
        flex: 1 1 auto;
        min-width: 0;
        flex-wrap: nowrap;
        gap: 6px;
    }

    .moon-exchange-summary {
        display: none;
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        font-size: 0.68rem;
    }

    .moon-exchange-refresh {
        flex: 0 0 auto;
        min-height: var(--mf-touch, 44px);
        padding-inline: 8px;
    }

    /* THE COMPOSER ON A REAL PHONE PAGE BOX.
     *
     * A landscape iPhone hands this panel ~135-200pt, so three stacked 44pt
     * touch rows (132pt before gaps) cannot fit — and the layout that tried to
     * stack them is precisely why the Create button painted across both inputs
     * on every box under ~300pt. Price, quantity and the primary action now
     * share ONE input row, with their labels in a band directly above it and
     * the fee receipt beside them.
     *
     *   col:   preview | price     | quantity  | receipt / action
     *   row 1:    "    | card select           | receipt
     *   row 2:    "    | label     | label     | receipt
     *   row 3:    "    | input     | input     | Create Listing
     *
     * Row 2 is `minmax(min-content, 1fr)`. It must NEVER be `minmax(0, 1fr)`:
     * a 1fr track has a minimum of ZERO, so it collapsed under its own content
     * and the field stack spilled into the row below, where the later-painted
     * action button covered it. The receipt spans rows 1-2 so its wrapped prose
     * cannot drive that row's height either. */
    .moon-exchange-form {
        display: grid;
        grid-template-columns: 80px minmax(0, 1fr) minmax(0, 1fr) minmax(168px, 0.34fr);
        grid-template-rows:
            var(--mf-touch, 44px)
            minmax(min-content, 1fr)
            var(--mf-touch, 44px);
        flex: 1 1 auto;
        min-height: 0;
        gap: 4px 6px;
        overflow: hidden;
        padding-right: 0;
    }

    #moon-exchange-card-select {
        grid-column: 2 / 4;
        grid-row: 1;
    }

    /* The selected option already includes the owned count (for example x1),
       so the separate availability line is duplicate phone chrome. Keep the
       Card label available to assistive technology without spending a row. */
    .moon-exchange-form > .moon-exchange-label {
        position: absolute;
        width: 1px;
        height: 1px;
        padding: 0;
        margin: -1px;
        overflow: hidden;
        clip: rect(0 0 0 0);
        clip-path: inset(50%);
        white-space: nowrap;
        border: 0;
    }

    .moon-exchange-availability {
        display: none;
    }

    /* The preview owns the full height of the composer instead of a hardcoded
       112px that ignored how tall its rows actually were — that fixed height is
       what made the card hang out of the form and get scissored by the
       `overflow: hidden` above. The card scales itself to whatever slot it is
       given (moon-exchange.js fitCardInsidePreview), so a fluid slot simply
       shows a bigger card wherever the page box allows one. */
    .moon-exchange-sell-preview {
        display: flex;
        grid-column: 1;
        grid-row: 1 / -1;
        align-self: stretch;
        width: 80px;
        height: auto;
        min-height: 0;
    }

    /* Three wrappers dissolve so label, input and action can be placed against
       the composer's own rows: the label band and the input band are shared
       across both fields, which is what keeps two inputs of different label
       lengths on one baseline. */
    .moon-exchange-sell-row,
    .moon-exchange-sell-fields,
    .moon-exchange-field {
        display: contents;
    }

    .moon-exchange-field > .moon-exchange-label {
        grid-row: 2;
        align-self: end;
        display: flex;
        align-items: end;
        font-size: 11px;
        line-height: 1.1;
        letter-spacing: 0.04em;
    }

    .moon-exchange-field > .moon-exchange-input {
        grid-row: 3;
        align-self: stretch;
        min-height: var(--mf-touch, 44px);
    }

    .moon-exchange-field:first-child > .moon-exchange-label,
    .moon-exchange-field:first-child > .moon-exchange-input {
        grid-column: 2;
    }

    .moon-exchange-field:last-child > .moon-exchange-label,
    .moon-exchange-field:last-child > .moon-exchange-input {
        grid-column: 3;
    }

    /* On the short phone lane, one concise receipt is enough: the fee policy
       and final amount remain visible, while the redundant gross row goes.
       Spanning rows 1-2 keeps its wrapped prose from setting the label band's
       height, so the band stays as short as the labels themselves. */
    .moon-exchange-proceeds {
        grid-column: 4;
        grid-row: 1 / 3;
        align-self: stretch;
        align-content: center;
        gap: 2px;
        padding: 4px 6px;
        font-size: 11px;
    }

    .moon-exchange-create-btn {
        grid-column: 4;
        grid-row: 3;
        align-self: stretch;
        justify-self: stretch;
        min-height: var(--mf-touch, 44px);
        height: 44px;
        margin: 0;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        line-height: 1.15;
    }

    .moon-exchange-proceeds-row:first-child {
        display: none;
    }

    .moon-exchange-fee-policy {
        font-size: 11px;
        line-height: 1.1;
    }

    .moon-exchange-status:empty {
        display: none;
    }

    .moon-exchange-sell-empty {
        flex: 1 1 auto;
        min-height: 0;
        padding: 10px 8px;
    }

    /* The locked gate replaces the composer, so it inherits the composer's
       budget: a landscape page box gives this panel roughly 135-200pt and the
       desktop padding alone spends 56 of it. Without this the explanation is
       scissored by its own scroller — 242pt of panel painting 135 — which
       would hide the very sentence telling the player how to unlock. */
    .moon-exchange-sell-locked {
        flex: 1 1 auto;
        min-height: 0;
        padding: 10px 8px;
        gap: 4px;
        overflow-y: auto;
    }

    .moon-exchange-sell-locked .moon-exchange-sell-empty-icon {
        font-size: 1.4rem;
        line-height: 1;
    }

    .moon-exchange-sell-locked .moon-exchange-sell-empty-hint {
        line-height: 1.3;
    }

    .moon-exchange-sell-empty-hint {
        line-height: 1.3;
    }

    /* Search stays visible; ownership/sort live in the single Filters popover. */
    .moon-exchange-market-controls {
        display: grid;
        grid-template-columns: auto minmax(96px, 1fr) auto auto;
        min-width: 0;
        gap: 4px;
        margin: 0;
    }

    .moon-exchange-command-bar .moon-exchange-view-switcher {
        grid-column: 1;
    }

    .moon-exchange-command-bar #moon-exchange-search {
        grid-column: 2;
        min-width: 0;
    }

    .moon-exchange-command-bar .moon-exchange-filters-toggle {
        display: inline-flex;
        grid-column: 3;
        min-width: 0;
        padding-inline: 8px;
    }

    .moon-exchange-command-bar .moon-exchange-refresh {
        grid-column: 4;
    }

    .moon-exchange-command-bar .moon-exchange-filter-menu {
        position: absolute;
        z-index: 30;
        top: calc(100% + 4px);
        right: 0;
        display: grid;
        grid-template-columns: repeat(2, minmax(148px, 1fr));
        gap: 8px;
        min-width: min(100%, 340px);
        padding: 10px;
        border: 1px solid rgba(238, 219, 241, 0.3);
        border-radius: 12px;
        background: rgba(15, 11, 29, 0.98);
        box-shadow: 0 14px 34px rgba(0, 0, 0, 0.5);
    }

    .moon-exchange-command-bar .moon-exchange-filter-menu[hidden] {
        display: none;
    }

    /* `grid-auto-rows: 1fr` + `overflow: hidden` is the reflow itself: row
       heights are derived from the CONTAINER, so anything that changes the
       container changes every row — and on iOS a scroll gesture collapses the
       URL bar, which changes the viewport height mid-gesture. With nothing
       able to scroll, the player's swipe produced a board that reorganised
       into a different number of rows instead of moving. Reported as "very
       brittle from scrolling: it doesn't scroll but reorganises the board to
       have fewer rows".

       The declared design for this surface is PAGINATION, not a nested scroll
       lane (tests/unit/styles/storeExchangeLandscapeComposition.test.js), and
       that policy is kept. What changes is the row sizing: rows measured from
       their own CONTENT hold still when the container moves, so the same
       listing stays the same size and the grid stops re-flowing under the
       finger. The page stays paginated and the box stays clipped. */
    .moon-exchange-listings {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        grid-auto-rows: minmax(44px, max-content);
        grid-column: 1 / -1;
        grid-row: 1;
        flex: 1 1 auto;
        min-height: 0;
        overflow: hidden;
        padding: 0;
        gap: 4px;
    }

    .moon-exchange-paint-stage {
        inset: 0;
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        grid-auto-rows: minmax(44px, 1fr);
        gap: 4px;
    }

    .moon-exchange-listing {
        grid-template-columns: minmax(0, 1fr) auto;
        gap: 4px;
        min-height: 0;
        padding: 0 4px;
        border-style: solid;
        background: rgba(12, 10, 24, 0.76);
        background-image: none;
        overflow: hidden;
    }

    .moon-exchange-listing-mine {
        border-style: solid;
    }

    .moon-exchange-skeleton {
        grid-template-columns: minmax(0, 1fr) auto;
        gap: 4px;
        min-height: 0;
        padding: 0 4px;
        overflow: hidden;
        /* A translated loading row temporarily enlarges scrollHeight by 4px,
           which makes the first paint look scrollable before it settles. */
        animation: me-listings-reveal 0.2s ease-out;
    }

    .moon-exchange-card,
    .moon-exchange-skeleton-card {
        gap: 5px;
    }

    .moon-exchange-card-preview,
    .moon-exchange-skeleton-thumb {
        width: 28px;
        height: 44px;
    }

    .moon-exchange-card-preview .deck-builder-card {
        /* Keep the shared renderer intact while fitting its physical pixels in
           the same bounded transaction row as the input and action. The pixel
           floor keeps a complete card legible at the 667x275 page box after
           the honest 44px pager track takes its share of the canvas. */
        --moon-exchange-card-scale: 0.45;
        --card-unit: max(
            calc(var(--exchange-card-unit, 1vmin) * var(--moon-exchange-card-scale)),
            1.8px
        );
    }

    .moon-exchange-card-info {
        display: flex;
        flex-wrap: wrap;
        align-content: center;
        column-gap: 4px;
        min-height: 0;
    }

    .moon-exchange-card-name {
        flex-basis: 100%;
        margin-bottom: 0;
        font-size: 11px;
        line-height: 1.05;
    }

    .moon-exchange-card-meta,
    .moon-exchange-card-seller {
        font-size: 10px;
        line-height: 1.05;
    }

    .moon-exchange-buy {
        /* NO SHARE AT ALL AT THIS SIZE, and both ways of keeping it were
           measured and rejected. Its own ROW makes `.moon-exchange-buy` 100px
           tall inside a lane that allots ~47.5px per listing, and every card,
           description and control spills out of it. Its own COLUMN takes 44px
           plus a gap out of the card side of a ~399px row, whose meta and
           seller lines then wrap and push the row from 47.5px to 65px — the
           same spill. Ellipsising those lines instead trades it for clipped
           translated text, which `exchange.real.spec.js` forbids outright and
           is right to. Share is one tap away: the row opens the listing view,
           which has it. Reversible — restore a cell here if the lane ever gets
           taller. */
        grid-template-areas:
            "qty action";
        grid-template-columns: 44px minmax(58px, auto);
        gap: 4px;
    }

    /* The generic `.btn` carries `margin: 4px`. In this compact transaction row
       that is not decoration, it is EIGHT PIXELS OF ROW HEIGHT: it made the buy
       control 52px tall instead of 44, so a listing row that everything else
       calls "44px" actually cost 54px. The lane was sized for 44px rows, so the
       second row's Buy control was cut off at the landscape page boxes. The
       touch target itself is untouched — the button stays 44px; only its
       outside margin goes. */
    .moon-exchange-buy .btn {
        /* All of it, not just the block axis: this is a grid with its own 4px
           gap, so an inline margin on the item double-counts the spacing the
           grid already owns. */
        margin: 0;
    }

    .moon-exchange-buy-qty {
        width: 44px;
    }

    .moon-exchange-buy-total,
    .moon-exchange-skeleton-price {
        display: none;
    }

    .moon-exchange-listing-mine .moon-exchange-buy {
        /* Your own listing has no qty field, and Share is hidden here, so
           Cancel is the whole column. */
        grid-template-areas: "action";
        grid-template-columns: minmax(62px, auto);
        width: auto;
    }

    .moon-exchange-skeleton-lines {
        gap: 5px;
    }

    .moon-exchange-skeleton-btn {
        width: 62px;
        height: 44px;
    }

    .moon-exchange-pagination {
        grid-column: 1 / -1;
        grid-row: 2;
        flex-direction: row;
        align-items: center;
        margin-top: 0;
        gap: 6px;
    }

    .moon-exchange-pagination .btn {
        flex: 0 0 auto;
        min-width: var(--mf-touch, 44px);
    }
}

@media (orientation: landscape) and (max-height: 560px) and (max-width: 720px) {
    .collection-section-deckbuilder .deck-builder-collection > .deck-builder-section-header {
        grid-template-columns: minmax(0, 1fr);
        grid-template-rows: auto auto;
    }

    .collection-section-deckbuilder .deck-builder-collection .deck-builder-controls {
        grid-template-columns: minmax(0, 1fr) auto;
    }
}

@media (orientation: portrait) and (max-width: 768px) {
    .collection-section-exchange {
        gap: 8px;
        padding-bottom: 4px;
        overflow: hidden;
    }

    .moon-exchange-view-switcher {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 6px;
        flex: 0 0 auto;
    }

    .moon-exchange-view-btn {
        min-height: var(--mf-touch, 44px);
    }

    .moon-exchange-view-btn.is-active {
        border-color: rgba(238, 219, 241, 0.78);
        background: rgba(116, 89, 200, 0.48);
    }

    .moon-exchange-grid {
        grid-template-columns: minmax(0, 1fr);
        grid-template-rows: minmax(0, 1fr);
        flex: 1 1 auto;
        min-height: 0;
        overflow: hidden;
    }

    .moon-exchange-grid[data-mobile-pane="market"] .moon-exchange-sell,
    .moon-exchange-grid[data-mobile-pane="sell"] .moon-exchange-market {
        display: none;
    }

    .moon-exchange-panel {
        min-height: 0;
        height: 100%;
        overflow: hidden;
    }

    .moon-exchange-listings {
        flex: 1 1 auto;
        min-height: 0;
        overflow: hidden;
    }

    .moon-exchange-form {
        min-height: 0;
        overflow: visible;
    }

    .moon-exchange-card-preview .deck-builder-card {
        --moon-exchange-card-scale: 0.65;
    }
}

@media (orientation: portrait) and (max-width: 480px) {
    .moon-exchange-card-preview .deck-builder-card {
        --moon-exchange-card-scale: 1;
    }
}

@media (orientation: portrait) and (max-width: 768px) and (max-height: 700px) {
    .moon-exchange-sell-preview {
        display: none;
    }

    .moon-exchange-sell-row {
        grid-template-columns: minmax(0, 1fr);
    }
}

/* Final selected-state authority: later legacy card rules used a tiny circular
   check and dimmed the entire choice. Keep the physical card bright, add a
   visible text badge, and retain the high-contrast ring on every viewport. */
.deck-builder-cards .deck-card-item.in-deck {
    opacity: 1;
}

.deck-builder-cards .deck-card-item .deck-card-check {
    min-width: 24px;
    width: auto;
    height: 24px;
    padding: 0 7px;
    border-radius: 999px;
    background: rgba(26, 124, 68, 0.96);
    color: #fff !important;
    font-size: 0.7rem;
    line-height: 1;
    z-index: 20;
}

@media (pointer: coarse),
       (orientation: portrait) and (max-width: 768px),
       (orientation: landscape) and (max-height: 560px) {
    /* The base saved-deck action rule appears after the earlier touch rule.
       Restate visibility here, at the final cascade boundary, so Delete is
       discoverable without hover on every phone and fine-pointer emulator. */
    .deck-list-actions {
        opacity: 1;
    }

    .deck-action-btn {
        min-width: var(--mf-touch, 44px);
        min-height: var(--mf-touch, 44px);
    }

    .deck-builder-cards .deck-card-item:hover {
        z-index: auto !important;
        transform: none;
    }

    .deck-builder-cards .deck-card-item:hover .deck-builder-card {
        filter: none;
        --card-hover-transform: scale(1);
    }
}

/* Desktop packs historically treated the physical pack as the action target.
   Keep the semantic seal as the only gesture owner in the canonical phone
   viewport, including fine-pointer emulation, without changing mobile layout. */
@media (pointer: fine) and (orientation: landscape) and (min-height: 561px),
       (pointer: fine) and (orientation: portrait) and (min-width: 769px) {
    .card-packs-pack:not(.card-packs-pack-disabled):not(.card-packs-pack-uncharged):not(.card-packs-pack-opened):not(.card-packs-pack-charging) {
        cursor: grab;
    }
}

/* A short landscape phone has enough horizontal space for the established
   side-by-side card detail. Override the generic <=900px vertical fallback so
   normal, Stellar, spell, and Foundry recipe variants fit without a scroll
   lane or hiding their actions. */
@media (orientation: landscape) and (max-height: 560px) {
    .collection-card-detail-header {
        gap: 6px;
        min-height: var(--mf-touch, 44px);
        padding: 2px 8px;
    }

    .collection-card-detail-back {
        min-width: var(--mf-touch, 44px);
        min-height: var(--mf-touch, 44px);
        padding: 4px 10px;
    }

    .collection-card-detail-title {
        font-size: 0.9rem;
    }

    .collection-card-detail-variant {
        padding: 3px 8px;
        font-size: 0.62rem;
        letter-spacing: 0.08em;
    }

    .collection-card-detail-body {
        flex-direction: row;
        align-items: stretch;
        gap: 8px;
        padding: 2px 8px 6px;
        overflow: hidden;
    }

    .collection-card-detail-left {
        flex: 0 0 auto;
        min-height: 0;
        padding: 0;
    }

    .collection-card-detail-card .deck-builder-card {
        /* The detail hero must not become smaller than an ordinary phone card.
           Its 1.25x desktop intent survives until the phone floor takes over. */
        --card-unit: max(1.25vmin, 4.6px);
        --card-width: calc(12.29 * var(--card-unit));
        --card-height: calc(22.2 * var(--card-unit));
    }

    .collection-card-detail-right {
        min-height: 0;
        width: auto;
        gap: 4px;
        padding: 0;
        overflow: hidden;
        justify-content: stretch;
    }

    .collection-card-detail-info {
        display: flex;
        flex: 1 1 auto;
        min-height: 0;
        min-width: 0;
        flex-direction: row;
        align-items: center;
        justify-content: center;
        gap: 4px;
        padding: 0;
        overflow: hidden;
    }

    .collection-card-detail-info > .collection-card-detail-status,
    .collection-card-detail-info > .collection-card-detail-meta,
    .collection-card-detail-info > .collection-recipe-section {
        flex: 1 1 0;
        min-width: 0;
        max-height: 100%;
    }

    /* `max-height: 100%` bounds the BOX, not the content inside it, and these
       panels kept `overflow: visible` — so a long recipe spilled straight out
       of the information lane and the lane's own `overflow: hidden` cut it off
       with nothing to scroll. Measured on a 5-parent Moon Foundry recipe at
       844x280: the section wanted 118px of a 74px lane, and 44px of parent
       cards were simply gone. Each panel now owns its overflow, so the lane
       itself still never scrolls and no recipe row becomes unreachable. */
    .collection-card-detail-info > .collection-card-detail-status,
    .collection-card-detail-info > .collection-card-detail-meta,
    .collection-card-detail-info > .collection-recipe-section {
        overflow-y: auto;
        overflow-x: hidden;
        -webkit-overflow-scrolling: touch;
        overscroll-behavior: contain;
        /* pinch-zoom too: the recipe section holds the parent minis, and a
           pinch that touches a card is the phone's magnifier. */
        touch-action: pan-y pinch-zoom;
    }

    .collection-card-detail-actions {
        gap: 4px;
        flex: 0 0 auto;
        flex-wrap: nowrap;
        min-width: 0;
        justify-content: center;
    }

    .collection-card-detail-actions > .deck-btn {
        flex: 1 1 0;
        min-width: 0;
        max-width: 220px;
    }

    .collection-craft-btn,
    .collection-list-exchange-btn {
        padding: 5px 10px;
        font-size: 0.76rem;
        line-height: 1.15;
    }

    .collection-craft-btn {
        min-height: var(--mf-touch, 44px);
    }

    .collection-list-exchange-btn {
        min-height: var(--mf-touch, 44px);
    }

    .collection-card-detail-meta {
        gap: 3px 10px;
        padding: 4px 8px;
        border-radius: 8px;
    }

    .collection-card-detail-meta-item,
    .collection-card-detail-meta-line,
    .collection-card-detail-owner {
        font-size: 0.72rem;
    }

    .collection-card-detail-meta-item .meta-label {
        font-size: 0.62rem;
    }

    .collection-card-detail-status {
        gap: 2px;
        padding: 4px 8px;
        border-radius: 8px;
    }

    .collection-card-detail-status-text {
        font-size: 0.72rem;
    }

    .collection-recipe-title {
        margin-bottom: 2px;
    }

    .collection-recipe-formula {
        gap: 4px;
        flex-wrap: nowrap;
    }

    .collection-recipe-parent {
        padding: 2px;
    }

    .collection-recipe-parent-name {
        margin-top: 0;
        font-size: 0.6rem;
    }
}

/* Landscape tablet Deck Builder is a command workspace, not a stretched
   desktop list. The 2x2 summoner canvas and six-row saved-deck rail consume the
   available height together, keeping identity art and actions comfortably
   readable without adding a nested scroller. Wide/short laptops and phones do
   not match this band. */
@media (orientation: landscape) and (min-width: 960px) and (min-height: 561px) and (max-width: 1560px) and (max-aspect-ratio: 17/10) {
    .collection-modal.collection-deckbuilder-active > .collection-container > .deck-builder-header {
        padding: clamp(14px, 1.5vw, 20px) clamp(16px, 1.8vw, 24px) 8px;
    }

    .collection-modal.collection-deckbuilder-active .collection-tabs .deck-tab {
        min-width: 44px;
        min-height: 44px;
        padding: 10px clamp(8px, 1vw, 14px);
        font-size: 16px;
    }

    .collection-modal.collection-deckbuilder-active .collection-content {
        min-height: 0;
        padding: clamp(10px, 1.2vw, 16px);
        overflow: hidden;
    }

    .deck-builder-container.deck-builder-embedded.deck-builder-root-mode {
        display: grid;
        grid-template-columns: minmax(0, 1.7fr) minmax(300px, 0.9fr);
        grid-template-rows: minmax(0, 1fr);
        gap: clamp(12px, 1.4vw, 18px);
        height: 100%;
        min-height: 0;
        overflow: hidden;
    }

    .deck-builder-root-mode .deck-builder-character-select {
        display: grid;
        grid-template-rows: minmax(0, 1fr) auto;
        grid-column: 1;
        grid-row: 1;
        width: auto;
        min-width: 0;
        min-height: 0;
        padding: 0;
        overflow: hidden;
    }

    .deck-builder-root-mode .db-character-grid {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        grid-template-rows: repeat(2, minmax(0, 1fr));
        gap: clamp(10px, 1.2vw, 16px);
        /* The 16:9 wells are intrinsically sized from the column width. A
           fixed row gutter gives both rows the last fractional pixels they
           need at wide/short tablet ratios without shrinking or clipping art. */
        row-gap: 10px;
        width: 100%;
        height: 100%;
        min-width: 0;
        min-height: 0;
        padding: 0;
        align-items: center;
        overflow: hidden;
    }

    .deck-builder-root-mode .db-character-card {
        min-width: 0;
        min-height: 0;
        height: auto;
        align-self: center;
        border-color: rgba(238, 219, 241, 0.2);
        background:
            radial-gradient(circle at 20% 0%, rgba(124, 92, 184, 0.2), transparent 46%),
            rgba(22, 16, 38, 0.92);
        box-shadow: 0 12px 28px rgba(0, 0, 0, 0.24);
    }

    .deck-builder-root-mode .db-char-image-wrapper {
        aspect-ratio: 16 / 9;
        height: auto;
        flex: 0 0 auto;
        min-height: 0;
    }

    .deck-builder-root-mode .db-char-info {
        flex: 0 0 auto;
        min-height: 66px;
        padding: 10px 14px 12px;
    }

    .deck-builder-root-mode .db-char-name {
        font-size: clamp(18px, 1.6vw, 22px);
        line-height: 1.2;
    }

    .deck-builder-root-mode .db-char-desc {
        max-height: 2.8em;
        font-size: clamp(14px, 1.15vw, 16px);
        line-height: 1.4;
    }

    .deck-builder-root-mode .db-character-pagination {
        margin-top: 8px;
        padding: 0;
    }

    .deck-builder-root-mode .db-character-pagination .deck-btn,
    .deck-builder-root-mode .deck-saved-pagination .deck-btn {
        min-width: 44px;
        min-height: 44px;
        font-size: 16px;
    }

    .deck-builder-root-mode .deck-builder-saved {
        grid-column: 2;
        grid-row: 1;
        display: grid;
        grid-template-rows: auto minmax(0, 1fr) auto;
        gap: 8px;
        min-width: 0;
        min-height: 0;
        padding: clamp(12px, 1.4vw, 18px);
        overflow: hidden;
        border: 1px solid rgba(238, 219, 241, 0.2);
        border-radius: 14px;
        background:
            radial-gradient(circle at 100% 0%, rgba(115, 85, 168, 0.2), transparent 45%),
            rgba(13, 9, 25, 0.78);
        box-shadow: inset 0 1px rgba(255, 255, 255, 0.04);
    }

    .deck-builder-root-mode .deck-builder-saved h3 {
        margin: 0;
        font-size: clamp(20px, 1.6vw, 24px);
        line-height: 1.2;
    }

    .deck-builder-root-mode .deck-builder-saved .deck-list {
        display: grid;
        grid-template-columns: minmax(0, 1fr);
        grid-template-rows: repeat(6, minmax(72px, 1fr));
        gap: 8px;
        width: 100%;
        height: 100%;
        min-width: 0;
        min-height: 0;
        padding: 0;
        overflow: hidden;
    }

    .deck-builder-root-mode .deck-list-item {
        width: 100%;
        min-width: 0;
        height: auto;
        min-height: 72px;
        border-radius: 10px;
    }

    /* Tablet saved decks use a compact command-row composition: the complete
       16:9 summoner art owns the right identity panel while deck text keeps a
       dedicated left lane. This avoids both a portrait-shaped crop and the
       empty side bars produced by containing landscape art across a 4:1 row. */
    .deck-builder-root-mode .deck-list-portrait-bg {
        inset: 0 0 0 auto;
        width: 44%;
        height: 100%;
        object-fit: contain;
        object-position: center;
        border-left: 1px solid rgba(238, 219, 241, 0.12);
    }

    .deck-builder-root-mode .deck-list-name-bar {
        justify-content: center;
        padding: 8px 12px;
        padding-right: calc(44% + 10px);
        background: linear-gradient(
            to right,
            rgba(8, 4, 18, 0.96),
            rgba(8, 4, 18, 0.88) 52%,
            rgba(8, 4, 18, 0.2) 72%,
            transparent
        );
    }

    .deck-builder-root-mode .deck-list-name {
        font-size: clamp(15px, 1.25vw, 17px);
    }

    .deck-builder-root-mode .deck-list-summoner {
        font-size: clamp(13px, 1.05vw, 15px);
    }

    .deck-builder-root-mode .deck-list-actions {
        opacity: 1;
    }

    .deck-builder-root-mode .deck-action-btn {
        min-width: 44px;
        min-height: 44px;
    }
}

/* At the narrow edge of the tablet band the complete localized section rail
   cannot share one line with the title and currency actions. Give navigation
   the full second row instead of shrinking labels or introducing a hidden
   horizontal scroll gesture. */
@media (orientation: landscape) and (min-width: 960px) and (min-height: 561px) and (max-width: 1180px) and (max-aspect-ratio: 17/10) {
    .collection-modal.collection-deckbuilder-active > .collection-container > .deck-builder-header {
        grid-template-columns: minmax(0, 1fr) auto;
        grid-template-rows: 44px 44px;
        column-gap: 8px;
        row-gap: 0;
        padding: 0 12px;
    }

    .collection-modal.collection-deckbuilder-active > .collection-container > .deck-builder-header > .collection-heading {
        grid-column: 1;
        grid-row: 1;
        min-width: 0;
        align-self: center;
    }

    .collection-modal.collection-deckbuilder-active > .collection-container > .deck-builder-header > .collection-heading > .collection-subtitle {
        display: none;
    }

    .collection-modal.collection-deckbuilder-active > .collection-container > .deck-builder-header > .collection-tabs {
        grid-column: 1 / -1;
        grid-row: 2;
        justify-content: center;
        width: 100%;
        min-width: 0;
        height: 44px;
        gap: clamp(4px, 1vw, 10px);
        padding-inline: 4px;
        overflow-x: hidden;
    }

    .collection-modal.collection-deckbuilder-active .collection-tabs .deck-tab {
        padding-inline: 6px;
    }

    .collection-modal.collection-deckbuilder-active > .collection-container > .deck-builder-header > .collection-header-actions {
        grid-column: 2;
        grid-row: 1;
        justify-self: end;
    }

    .collection-modal.collection-deckbuilder-active .collection-stardust {
        min-height: 44px;
        flex-direction: row;
        gap: 6px;
        padding: 4px 10px;
    }
}

/* The lower edge of the tablet band cannot physically hold six 72px rows plus
   the heading and pager. Page four substantial rows instead of clipping two
   inaccessible decks behind the rail's intentional overflow boundary. */
@media (orientation: landscape) and (min-width: 960px) and (min-height: 561px) and (max-height: 700px) and (max-width: 1560px) and (max-aspect-ratio: 17/10) {
    .collection-modal.collection-deckbuilder-active .collection-content {
        padding: 6px 12px;
    }

    .deck-builder-container.deck-builder-embedded.deck-builder-root-mode {
        gap: 10px;
    }

    .deck-builder-root-mode .db-character-pagination {
        margin-top: 4px;
    }

    /* Two 16:9 summoner wells and two text blocks cannot honestly fit beside
       the 44px pager at this height. Preserve the authored art and primary
       name, and remove the redundant subtitle rather than clipping the grid. */
    .deck-builder-root-mode .db-char-info {
        min-height: 40px;
        padding: 4px 10px;
        justify-content: center;
    }

    .deck-builder-root-mode .db-char-desc {
        display: none;
    }

    .deck-builder-root-mode .deck-builder-saved {
        gap: 6px;
        padding: 12px;
    }

    .deck-builder-root-mode .deck-builder-saved .deck-list {
        grid-template-rows: repeat(4, minmax(64px, 1fr));
        gap: 6px;
        overflow: hidden;
    }

    .deck-builder-root-mode .deck-list-item {
        min-height: 64px;
    }
}

/* Official starter templates are reference decks. The viewer removes every
   editing surface while preserving card inspection and responsive paging. */
.deck-builder-container.deck-builder-readonly .deck-builder-view-switcher,
.deck-builder-container.deck-builder-readonly .deck-builder-collection {
    display: none !important;
}

.deck-builder-container.deck-builder-readonly .deck-builder-content {
    grid-template-columns: minmax(280px, 560px);
    justify-content: center;
}

.deck-builder-container.deck-builder-readonly .deck-builder-deck {
    width: 100%;
    max-width: 560px;
    justify-self: center;
    border: 1px solid rgba(137, 199, 255, 0.24);
    background: linear-gradient(145deg, rgba(18, 28, 49, 0.94), rgba(10, 8, 23, 0.94));
}

.deck-builder-container.deck-builder-readonly .deck-name-input[readonly] {
    border-color: rgba(137, 199, 255, 0.42);
    background: rgba(34, 59, 85, 0.42);
    color: #e4f4ff;
    cursor: default;
}

.deck-builder-container.deck-builder-readonly .deck-status.readonly {
    color: #a9ddff;
}

.deck-builder-container.deck-builder-readonly .deck-card-mini-readonly {
    cursor: default;
}

.deck-builder-container.deck-builder-readonly .deck-card-mini-readonly:hover {
    border-color: rgba(137, 199, 255, 0.22);
    background: rgba(16, 13, 31, 0.88) !important;
    box-shadow: inset 0 1px rgba(255, 255, 255, 0.035), 0 2px 7px rgba(0, 0, 0, 0.16);
}

@media (max-width: 768px) {
    .collection-modal .deck-builder-container.deck-builder-readonly .deck-builder-content {
        grid-template-columns: minmax(0, 1fr);
    }
}

/* ---------------------------------------------------------------------------
   COLLECTION ON A LANDSCAPE PHONE (<=360pt page box)

   Measured at an 844x288 page box: header 72 + filter row 52 + pagination 56
   left the card grid **67px** — under a quarter of the screen for the one thing
   the screen is for. The fit maths reacted by shrinking cards to 0.372 (15x38px
   slivers whose Stellar ribbons were larger than the cards themselves); with a
   legibility floor in place instead, exactly ONE card fitted.

   So buy the grid its height back out of the chrome — but only out of PADDING.
   Every control keeps the 44px tap target the mobile contract requires
   (`MIN_TAP_PX` in tests/e2e-playwright/mobile-screens.spec.js); an earlier
   pass took them to 32px and that spec correctly rejected it. Nothing here
   touches the cards, which keep the shared anatomy that has to render
   identically on every surface.
   --------------------------------------------------------------------------- */
@media (orientation: landscape) and (max-height: 360px) {
    #collection-modal .deck-builder-header {
        padding-block: 0;
        row-gap: 0;
    }

    #collection-modal .collection-tabs.deck-builder-tabs {
        padding-block: 0;
        gap: 2px;
    }

    #collection-modal .collection-tabs .deck-tab,
    #collection-modal .collection-header-actions > * {
        min-height: 44px;
        padding-block: 2px;
        font-size: 12px;
    }

    #collection-cards-browser > .collection-controls-row {
        gap: 4px;
        font-size: 12px;
    }

    #collection-cards-browser > .collection-controls-row :is(input, select),
    #collection-cards-browser > .collection-controls-row button {
        min-height: 44px;
        padding-block: 2px;
    }

    #collection-cards-browser > .collection-controls-row :is(input, select) {
        /* iOS focuses and magnifies any editable/select control below 16px.
           This late compact-height tier wins the cascade, so it must carry the
           floor itself rather than relying on an earlier generic phone rule. */
        font-size: 16px;
    }

    #collection-cards-browser > .collection-controls-row button {
        font-size: 12px;
    }

    #collection-cards-grid {
        padding-block: 2px;
    }

    #collection-cards-pagination {
        padding-block: 0;
        gap: 4px;
    }

    #collection-cards-pagination button,
    #collection-cards-pagination .page-indicator {
        min-height: 44px;
        padding-block: 2px;
        font-size: 12px;
    }
}

/* Quantity is useful desktop hover chrome, but on a phone it sits directly
   over the physical card's rules text. Ownership is already communicated by
   the absence of the lock and by the detail view; do not cover the face. */
@media (any-pointer: coarse), (max-width: 1024px) {
    .collection-card-owned-quantity {
        display: none;
    }

    .collection-card-item {
        --tile-bottom-line-max: calc(100% - 12px);
    }
}

/* Under ~320pt the three control strips (tabs, filters, pagination) plus their
   44px tap targets still left the grid 83px — short of one legible card row, so
   a floored card would have been clipped by its own grid. Padding is exhausted
   at this point; the only thing left is a strip. Pagination is the narrowest of
   the three, so fold it up beside the filters and hand the whole reclaimed row
   to the cards. Tap targets are untouched. */
@media (orientation: landscape) and (max-height: 320px) {
    #collection-cards-browser {
        grid-template-rows: auto minmax(0, 1fr);
        grid-template-columns: minmax(0, 1fr) auto;
        column-gap: 8px;
    }

    #collection-cards-browser > .collection-controls-row {
        grid-row: 1;
        grid-column: 1;
        min-width: 0;
    }

    #collection-cards-browser > #collection-cards-pagination {
        grid-row: 1;
        grid-column: 2;
        justify-content: flex-end;
        flex-wrap: nowrap;
    }

    #collection-cards-browser > #collection-cards-grid {
        grid-row: 2;
        grid-column: 1 / -1;
    }
}

/* ---------------------------------------------------------------------------
   CARD PACK ON A LANDSCAPE PHONE

   `--pack-stage-h: clamp(300px, 50svh, 430px)` has a 300px FLOOR, and 50svh of
   a 288pt page box is 144px — so the clamp returned 300px, a stage taller than
   the entire screen. The pack is a flex item, so it was shrunk to fit whatever
   was left: roughly 50x61px, with the ribbon that is the whole interaction
   invisible and the "Pull the ribbon down" hint printing across the art.

   On a phone the stage is exactly what the page box has, the pack refuses to
   shrink, and the nested panel chrome stops charging rent for borders around a
   pack that IS the screen.
   --------------------------------------------------------------------------- */
@media (orientation: landscape) and (max-height: 400px) {
    /* The shell's middle row carried a 280px minimum and the stage a 300px
       floor — both taller than the 173px this page box hands the shell — so the
       row overflowed, the stage collapsed to 64px, and the pack (sized from an
       unreachable budget) was drawn outside its own container with the reveal
       fan at y=-34. Everything here yields its fixed heights to the stage. */
    /* One surface, not three. The shell painted a bordered vault, the stage
       painted another bordered box inside it, and the pack sat inside that —
       "useless nested windows limiting the total size of the cards". On a phone
       the pack IS the screen, so the frames go and their padding with them. */
    /* Side rail, not a stack. The counters, the odds line and the two action
       buttons were a horizontal band across the top, and on a 288pt page box
       they took roughly half the height before the pack got any — "decrease
       ingame bars, move buttons off the way". Landscape has width to spare and
       no height at all, so the chrome moves into a narrow column beside the
       stage and the pack takes the FULL height. */
    .card-packs-shell {
        /* The weighted-value disclosure is contract copy, not flavor that may
           be clipped. At 250px the German sentence needs seven lines, which
           pushes the 44px Buy control below a 667x275 page box. Give the rail
           enough width to wrap every active locale within the fixed vertical
           budget. Capping it at 300px also preserves the separate hint lane
           beside the sealed wrapper; contract copy may not buy space by
           painting over the interaction it explains. */
        grid-template-columns: minmax(0, clamp(280px, 35%, 300px)) minmax(0, 1fr);
        grid-template-rows: minmax(0, 1fr);
        gap: 6px;
        padding: 2px 4px;
        border: none;
        border-radius: 0;
        /* Fully transparent, not merely unbordered. The gradient still painted
           a distinct panel behind the pack — a "background window layer" that
           reads as chrome and boxes the stage in without earning any of the
           space it occupies. On a phone the modal's own backdrop is the only
           surface there should be. */
        background: none;
    }

    /* The oversized texture layers (inset:-30%) inflate scrollHeight, which is
       what put a scrollbar on a screen with nothing to scroll — and on iOS a
       scroll collapses the URL bar, changing every size derived from the
       viewport mid-gesture. That is the reported "scrolling changed card
       sizes". */
    .card-packs-shell::before,
    .card-packs-shell::after,
    .card-packs-stage::before,
    .card-packs-stage::after {
        display: none;
    }

    /* The section and the scroll frame around it are chrome too. */
    #collection-modal .collection-section-packs {
        background: none;
        border: none;
        padding: 0;
        /* Nothing on this screen scrolls, so nothing may TRY to. On iOS a page
           scroll collapses the URL bar, the viewport height changes, and every
           size derived from it is recomputed mid-gesture — including this
           screen's card unit, which is measured in cqh against the stage. The
           player's swipe therefore re-laid-out the pack instead of moving it:
           "scrolling breaks that screen as well, reorganises it". The surface
           already fits; clamping it to the space it has means the gesture has
           nothing to act on and the URL bar never moves. */
        overflow: hidden;
        min-height: 0;
        max-height: 100%;
    }

    /* The rail: everything that is not the pack, in the left column.
       Placement only — the `#collection-modal` rule below owns its layout, and
       every flex declaration that used to live here was overridden by it. */
    .card-packs-header {
        grid-column: 1;
        grid-row: 1;
        min-height: 0;
        margin: 0;
        padding: 0;
    }

    /* THE RAIL IS A STACK OF ROWS, NOT AN OVERLAY.
     *
     * The two purchase disclosures and the action stack used to share ONE grid
     * cell — `grid-row: 1` for all three, the odds lifted over the buttons with
     * `z-index: 1` and held to the cell's top by `align-self: start`. That was
     * sound while the disclosure was the 37-character `Per card: Stellar 0.2%
     * · Normal 99.8%`, which is exactly one line in the 202px lane it was given.
     * `7d5f34c1` deliberately merged it into a full sentence, and a sentence does
     * not fit a 15px lane: measured on the real surface at 844x280 the odds box
     * runs 105 -> 140.7 while the Exchange button's box starts at 120, so the
     * legal line paints over the top 20px of a button in EVERY locale. German is
     * merely the one whose third line reaches a sampled centre pixel; English
     * overlaps by 8.8px and passes a point probe only because its glyphs stop
     * short. Reported from a real iPhone as "card pack odds text is covered by
     * exchange button".
     *
     * `#card-packs-price-note` made it worse and was never reported, because it
     * is empty until the reference rate loads: it carries the `.card-packs-odds`
     * class and had no rules of its own anywhere in this sheet, so it was pinned
     * to the identical anchor and painted straight onto the odds' first line.
     * That is why this is THREE rows rather than two — the price note needs a
     * seat of its own, not merely an eviction from the buttons.
     *
     * Overlaying was only ever a height optimisation, and the rail does not need
     * it: three honest rows are ~205px in the worst locale against the 271px the
     * smallest landscape page box (667x275) gives this column, and full width
     * costs LESS height than the overlay did — German drops from three lines to
     * two. The disclosures go FIRST, which keeps the guarantee that made the
     * overlay attractive (the odds are read before either purchase action) and
     * keeps them out of reach of the bottom safe-area inset that clipped them
     * when they sat after the buttons. `align-content: center` keeps the whole
     * stack off both edges. */
    #collection-modal .collection-section-packs .card-packs-header {
        display: grid;
        grid-template-rows: auto auto auto;
        grid-template-columns: minmax(0, 1fr);
        align-content: center;
        gap: 4px;
    }

    #collection-modal .collection-section-packs .card-packs-odds {
        grid-column: 1;
        grid-row: 1;
        text-align: start;
    }

    #collection-modal .collection-section-packs .card-packs-price-note {
        grid-row: 2;
    }

    /* The pill hugs its own label rather than stretching the rail, which is what
       made an overlaid odds line read as text dropped onto a border at a random
       offset. It no longer shares its row, so it no longer has to reserve width
       for anything beside it. */
    #collection-modal .collection-section-packs .card-packs-count {
        justify-self: start;
        align-self: center;
        width: auto;
        max-width: 100%;
        /* `max-width` clamps the BOX; a `nowrap` label longer than it (Portuguese
           "Pacotes") still paints its ink outside the rail without this. */
        overflow: hidden;
        white-space: nowrap;
    }

    /* 52px for a row whose tallest child is a 44px button is 8px of padding the
       pack could be using. The buttons keep their tap floor; the box around
       them does not get to charge rent. */
    .card-packs-header-actions {
        grid-column: 1;
        grid-row: 3;
        align-items: center;
        padding: 0;
        margin: 0;
        min-height: 0;
    }

    .card-packs-count {
        font-size: 11px;
        padding: 0 6px;
    }

    /* The desktop inventory chip's 0.18em tracking is more width than a 280px
       rail can spare for one word — Portuguese "Pacotes" used to run into the
       legal odds back when the two shared a row. Keep the small-caps rhythm,
       spend no more width than the label needs. */
    .card-packs-count {
        letter-spacing: 0.1em;
    }

    .card-packs-duplicates {
        padding: 0;
        border: none;
        background: none;
    }

    /* 44px stays: these are the only two controls on the screen and the tap
       floor is not negotiable for a pack the player is paying for. */
    .card-packs-duplicates-btn,
    .card-packs-buy-btn {
        min-height: var(--mf-touch, 44px);
        padding: 0 10px;
        font-size: 11px;
        white-space: nowrap;
    }

    /* The status line is empty most of the time; it must not hold a row open. */
    .card-packs-status {
        font-size: 11px;
        min-height: 0;
        line-height: 1.1;
    }
    .card-packs-status:empty {
        display: none;
    }

    /* ONE column, explicitly, at every level of the rail. The 560px tier gives
       the actions a three-track grid with a fixed 180px column, and the
       duplicates block a two-track grid with a 272px minimum — both far wider
       than this 208px rail, which is where the horizontal overflow was really
       coming from. Stating the tracks here beats inheriting someone else's
       horizontal layout and then fighting it with min-width and clipping. */
    .card-packs-header-actions,
    .card-packs-duplicates {
        display: grid;
        grid-template-columns: minmax(0, 1fr);
        grid-auto-rows: max-content;
        align-items: stretch;
        gap: 2px;
        margin: 0;
        min-width: 0;
        width: 100%;
        /* border-box, or a border on a 100%-wide child overflows by its own
           width — which is exactly the 4px that was left. */
        box-sizing: border-box;
    }

    .card-packs-header-actions > *,
    .card-packs-duplicates > * {
        box-sizing: border-box;
        max-width: 100%;
    }

    /* Everything in the rail fits the rail. Flex children default to their
       content's min-width, so the buy button kept its one-line width (270px in
       a 208px column) and pushed 212px of horizontal overflow into a screen
       that must not scroll at all. Labels wrap instead. */
    .card-packs-header > *,
    .card-packs-header-actions > *,
    .card-packs-duplicates {
        min-width: 0;
        max-width: 100%;
    }

    .card-packs-header .btn,
    .card-packs-header .deck-btn {
        width: 100%;
        white-space: normal;
        overflow-wrap: anywhere;
        line-height: 1.15;
    }

    /* The rail owns its width, and so does every box inside it. Wrapping
       handles the labels, but scrollWidth propagates: a descendant that is
       still too wide widens its parent's scroll area, and one horizontal
       scroll anywhere puts the whole screen in play — on iOS any scroll
       collapses the URL bar and re-flows everything. Measured after the wrap
       rules: the actions column was 208 wide and scrolled to 282, so the cap
       goes on each nesting level rather than only the outermost one. */
    /* BOTH axes. Setting only `overflow-x: hidden` makes the used value of
       `overflow-y` compute to `auto` — so the rule meant to stop a horizontal
       scroll quietly created a vertical one, on the very screen whose whole
       problem is that a scroll re-flows it. */
    /* The outer action grid must not clip vertically. With the weighted-value
       disclosure, Firefox and Chrome can assign its constrained grid track
       3-4px less than the max-content rows at 667x275; overflow:hidden then
       shears the Buy button even though the complete control still fits the
       viewport. Width containment comes from the explicit tracks, border-box,
       min-width, wrapping, and zero button margins above, and the real-browser
       contract verifies that the rail has no horizontal overflow. */
    .card-packs-header-actions {
        overflow: visible;
    }

    /* This inner box owns only the duplicate counter and Exchange button. It
       may keep its containment guard because neither child can legitimately
       paint outside the max-content rows. */
    .card-packs-duplicates {
        overflow: hidden;
    }

    .card-packs-header-actions > .btn,
    .card-packs-duplicates > .btn {
        /* `margin-inline` left the inherited block margins live for a frame
           while this breakpoint settled after a resize. Geometry must snap;
           only paint/compositor feedback may interpolate. */
        margin: 0;
        transition:
            background-color 0.2s ease,
            border-color 0.2s ease,
            color 0.2s ease,
            box-shadow 0.2s ease,
            filter 0.2s ease,
            transform 0.2s ease;
    }

    .card-packs-count,
    .card-packs-odds {
        white-space: normal;
        overflow-wrap: anywhere;
    }

    /* Seven-line French and German contract copy needs an honest text rhythm,
    not an eighth row's worth of leading. Keep the normal 1.2 readability
       floor while recovering the pixels the two 44px actions may never
       surrender. */
    .card-packs-odds {
        line-height: 1.2;
    }

    .card-packs-stage {
        /* Second COLUMN now: the rail holds the chrome, the stage holds the
           pack, and the pack gets the whole height rather than what a
           horizontal band of buttons left over. */
        grid-column: 2;
        grid-row: 1;
        /* The stage IS the free cell; no floor of its own to fight it. */
        --pack-stage-h: 100%;
        --pack-hint-lane: 0px;
        min-height: 0;
        padding: 0;
        border: none;
        border-radius: 0;
        background: none;
        /* Size containment so the reveal fan can measure the stage it lives in
           (cqh below). The stage's own size comes from the grid row, never
           from its contents, so containment costs nothing here. */
        container-type: size;
    }

    /* The fan was sized from the viewport, not from the stage, so its 153px
       cards overhung a 113px stage and lost their top third to overflow:hidden
       — five half-cards, which is what made the reveal unreadable. Derive the
       card unit from the stage's own height instead: 22.2 units of card plus
       the 1.46 fan allowance must equal 100cqh, so one unit is 100/(22.2*1.46)
       = 3.08cqh. */
    .card-packs-cards {
        /* 1.24 of fan allowance instead of the desktop 1.46: the reveal lifts
           18px and tilts 8deg, which needs roughly a quarter of a card, not a
           half. The freed height goes straight into the cards. */
        --card-unit: 3.63cqh;
        --card-width: calc(12.29 * var(--card-unit));
        --card-height: calc(22.2 * var(--card-unit));
        --card-spread: calc(var(--card-width) * 0.5);
        height: 100%;
        bottom: 0;
    }

    /* Auto width in the corner: stretched edge-to-edge it lay straight across
       the revealed cards. */
    /* "You packed the charge pack too close to the cards." It sat in the stage
       corner with no clearance, touching the revealed fan. Give it real
       breathing room and lift it clear of the card tops. */
    .card-packs-open-btn {
        left: auto;
        right: 6px;
        top: 6px;
        bottom: auto;
        width: auto;
        min-height: var(--mf-touch, 44px);
        padding: 0 12px;
        margin: 0;
        z-index: 3;
    }

    .card-packs-pack {
        /* Never flex-shrunk: the pack is the hero, and everything else on this
           screen yields to it. aspect-ratio still derives the width. */
        flex: 0 0 auto;
        min-height: 0;
        height: 100%;
        max-height: 100%;
        /* The idle -8px lift plus the float animation pushed the crimped top
           edge out of a stage that no longer has spare room, so the LIFT stays
           gone here. What replaced it must never need clearance either: a
           breath that only ever scales DOWN can't reach an edge it isn't
           already touching. A pack that holds perfectly still reads as a
           sticker, which is the whole complaint this answers. */
        transform: none;
        animation: none;
    }

    /* Scoped exactly like the ≤560px float override: only the idle sealed pack
       breathes. The drag owns `scale:` while charging, and the tear owns the
       whole element after — neither may be fought by an idle loop declared
       later in the sheet than they are. */
    .card-packs-pack:not(.card-packs-pack-dragging):not(.card-packs-pack-tearing):not(.card-packs-pack-opened):not(.card-packs-pack-disabled):not(.card-packs-pack-uncharged):not(.card-packs-pack-charging) {
        animation: packSealedBreath var(--pack-breath-dur, 3.2s) ease-in-out infinite;
        /* PRE-EXISTING CLIP, fixed here. The ≤560px tier lifts the sealed pack
           with `translateY(-2px)` from a `:not():not()` selector, which
           outranks this tier's plain `.card-packs-pack { transform: none }`.
           So the phone pack was lifted 2px while sized at the FULL stage
           height, and `overflow:hidden` cut its crimped top edge — measured at
           1.15-2.00px above the stage on every frame, in all three engines.
           Matching the chain's specificity is what actually lands the reset. */
        transform: none;
        /* The pack stands on the stage floor here — the contact shadow is
           `display: none` in this tier because there is no surface under it to
           catch one — so it should breathe from its base like a standing
           object, not pulse from its middle like a heartbeat. That is also
           what buys the amplitude: a centre origin splits the scale between
           both edges (2.79px each at 0.965), while a bottom origin spends the
           whole 5.59px on the top edge, which is the one with room. The travel
           still only ever moves AWAY from the top rail this tier had to fix a
           clip against, so the change cannot reintroduce it. Scoped to the
           idle: the drag and the tear own the origin they need, and at the
           moment either takes over, scale is 1 and origin cannot show. */
        transform-origin: bottom center;
    }

    /* ...and the calm has to be declared HERE, not five thousand lines up.
       The sheet-level `@media (prefers-reduced-motion: reduce)` block calms the
       identical state-exclusion chain, but equal specificity is decided by SOURCE
       ORDER and it sits above this tier — so on the one page box where the pack
       is the whole screen, a player who asked for stillness watched it breathe
       anyway (measured: 3.12px of travel, in all three engines). Nesting the
       override inside the tier puts it after the rule it answers, permanently. */
    @media (prefers-reduced-motion: reduce) {
        .card-packs-pack:not(.card-packs-pack-dragging):not(.card-packs-pack-tearing):not(.card-packs-pack-opened):not(.card-packs-pack-disabled):not(.card-packs-pack-uncharged):not(.card-packs-pack-charging) {
            animation: none;
            scale: 1;
        }
    }

    /* No surface under the pack to catch a shadow: it IS the stage here. */
    .card-packs-pack-glow {
        display: none;
    }

    /* 0.62rem resolved to 9.9px — under the 11px readability floor every other
       phone control is held to. The corner has room; it was the type that was
       too small, not the button. */
    .card-packs-open-btn {
        top: 4px;
        right: 4px;
        padding: 6px 12px;
        font-size: 0.72rem;
    }

    /* Beside the pack, not across it. Centred under the pack it still landed
       ON the artwork, because the pack is the full height of the stage —
       "text in card pack screen should not overlap pack, move it to side".
       There is a whole empty column to the left of a centred pack on this page
       box, so the hint lives there, vertically centred and wrapped. */
    .card-packs-pack-hint {
        position: absolute;
        left: 0;
        bottom: auto;
        top: 50%;
        transform: translateY(-50%);
        /* The localized contract rail leaves this stage slightly narrower than
           the old 250px layout. A 34% lane touched the wrapper by 2–5px at the
           measured phone boxes; 31.5% keeps a real gap through subpixel font
           shaping while retaining enough width for the instruction. */
        width: min(31.5%, 220px);
        max-width: 31.5%;
        text-align: left;
        margin: 0;
        pointer-events: none;
        font-size: 11px;
        line-height: 1.25;
    }

    /* With no charged pack the transparent phone stage otherwise reads as a
       failed render: a tiny instruction floating in a black void. Keep the
       object layer genuinely empty, but let the stage itself hold a calm pool
       of moonlight and centre its only instruction. This is static paint—no
       fake wrapper, no idle animation, and no extra interaction target. */
    .card-packs-stage.card-packs-stage-vacant {
        background:
            radial-gradient(ellipse at 50% 52%, rgba(183, 154, 242, 0.16), rgba(75, 54, 112, 0.06) 34%, transparent 68%),
            linear-gradient(145deg, rgba(19, 13, 34, 0.46), rgba(6, 8, 17, 0.2));
        box-shadow: inset 0 0 54px rgba(211, 190, 255, 0.05);
    }

    .card-packs-stage-vacant .card-packs-pack-hint {
        left: 50%;
        top: 52%;
        bottom: auto;
        width: min(62%, 360px);
        max-width: calc(100% - 132px);
        transform: translate(-50%, -50%);
        color: rgba(238, 219, 241, 0.76);
        font-size: clamp(12px, 3.5vmin, 15px);
        line-height: 1.35;
        letter-spacing: 0.035em;
        text-align: center;
    }

    /* And the pack takes the room that frees up: it is the hero of this screen
       and was rendering at about a third of the height available to it. */
    .card-packs-stage {
        padding-top: 0;
        padding-bottom: 0;
    }

    /* The rail may wrap at spaces, but never through a word. A broader rule
       above permits emergency breaks for passive disclosure copy; carrying
       that policy into the counter and its two actions made the 288px phone
       contract report (correctly) that functional labels could split at any
       character. These three controls have enough inline room to wrap only at
       authored boundaries in every supported locale. */
    #card-packs-count,
    #card-packs-duplicates-btn,
    #card-packs-buy-btn {
        overflow-wrap: normal;
        word-break: normal;
        hyphens: none;
    }
}

/* The Collection modal's own chrome charged 60px of header, 18px of section
   padding and 10px of content padding on a 288pt page box — a third of the
   screen before any surface drew anything. Tap targets are untouched; only the
   padding around them yields. ------------------------------------------------ */
@media (orientation: landscape) and (max-height: 360px) {
    /* Same story one level up: the modal header measured 60px around a 44px tab
       strip. The tabs are how the player leaves this screen, so they keep their
       height — the padding around them does not. */
    #collection-modal .deck-builder-header {
        padding: 0;
        margin: 0;
        row-gap: 0;
        column-gap: 8px;
        min-height: 0;
        align-items: center;
    }

    #collection-modal .collection-content {
        padding-block: 0;
        gap: 2px;
        /* Same reason: the modal body must not become the page's scroller on a
           surface that fits, or the URL bar collapse re-flows everything. */
        overflow: hidden;
        min-height: 0;
    }

    #collection-modal .collection-section {
        padding-bottom: 0;
        gap: 4px;
    }

    /* The tab strip already says where you are; a heading and a subtitle
       repeating it cost more of this page box than the pack stage can spare.
       Kept in the accessibility tree — the dialog is still labelled by it. */
    #collection-modal .collection-heading {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }

    /* The 667x275 page box has room for two 44px filter rows, not four. Use
       the available landscape width instead of keeping the generic 420px
       drawer cap that forced all controls into one column at <=700px. */
    .collection-modal .collection-filter-popover {
        position: fixed;
        top: 116px;
        right: calc(var(--mf-safe-right) + 12px);
        left: calc(var(--mf-safe-left) + 12px);
        width: auto;
        max-width: none;
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    .collection-modal .collection-filter-popover #collection-filter-spell-owner {
        grid-column: auto;
    }

    /* Five desktop-style vertical cardback tiles need ~177px before their
       Equip action; the measured 844x288 page box gives the grid ~116px.
       Landscape has spare width, so compose each tile horizontally: complete
       art on the left, identity and the 44px action on the right. No content
       is clipped and the page remains a fixed, paginated canvas. */
    .collection-modal .cardback-grid {
        grid-template-columns: repeat(5, minmax(0, 1fr));
        grid-auto-rows: minmax(0, 1fr);
        align-items: stretch;
        gap: 4px;
    }

    .cardback-item {
        display: grid;
        grid-template-columns: auto minmax(0, 1fr);
        grid-template-rows: minmax(0, 1fr) var(--mf-touch, 44px);
        column-gap: 4px;
        width: 100%;
        height: 100%;
        max-width: none;
        min-height: 0;
        padding: 2px;
        margin: 0;
        overflow: hidden;
        text-align: left;
    }

    .cardback-image-wrapper {
        grid-column: 1;
        grid-row: 1 / -1;
        align-self: center;
        width: clamp(46px, 13vmin, 54px);
        margin: 0;
    }

    .cardback-info {
        grid-column: 2;
        grid-row: 1;
        min-width: 0;
        align-self: center;
    }

    .cardback-name {
        min-height: 0;
        font-size: 0.7rem;
        line-height: 1.2;
    }

    .cardback-item .select-cardback-btn {
        grid-column: 2;
        grid-row: 2;
        width: 100%;
        min-width: 0;
        min-height: var(--mf-touch, 44px);
        margin: 0;
        padding: 4px 2px;
        /* The browser resolves 0.66rem to 10.56px at the default root size.
           Preserve the compact lane while keeping localized actions legible. */
        font-size: max(11px, 0.66rem);
    }

    .cardback-item.locked {
        grid-template-rows: minmax(0, 1fr);
    }


    .cardback-item.locked .cardback-lock-overlay .lock-hint {
        display: none;
    }

    .cardback-item.locked .cardback-lock-copy {
        display: block;
        margin-top: 3px;
        overflow-wrap: anywhere;
        color: #ddd;
        font-size: 0.68rem;
        font-style: italic;
        line-height: 1.15;
    }
}

/* ---- Catalog "Selected" badge on a phone -----------------------------------
   Measured on a 288pt page box: the card is 52px wide and the "✓ Selected"
   pill is 60px, anchored to the card's right edge — so it hung 8px off the
   LEFT and printed over the neighbouring card, which is what made a grid of
   selections unreadable. At this size the tick IS the message; the word is
   what does not fit. ------------------------------------------------------- */
@media (orientation: landscape) and (max-height: 400px) {
    .deck-builder-cards .deck-card-item .deck-card-check,
    .deck-card-item .deck-card-check {
        /* ...and the tick still may not sit on the NAME. At the card's
           top-right corner an 18px tick covered 14x4px of "Enchanted Log" on a
           844x280 page box. It takes the art well's bottom-right, the same
           corner the desktop pill uses, opposite the Stellar badge. */
        top: auto;
        bottom: 31%;
        right: 9.5%;
        min-width: 0;
        width: 18px;
        height: 18px;
        padding: 0;
        font-size: 0;
        line-height: 0;
        border-radius: 999px;
        /* The label is dropped visually but kept for assistive tech. */
        overflow: hidden;
        color: transparent !important;
    }

    .deck-builder-cards .deck-card-item .deck-card-check::before,
    .deck-card-item .deck-card-check::before {
        content: "✓";
        font-size: 12px;
        line-height: 1;
        color: #fff;
    }
}

/* ---------- Per-collection pack wrappers --------------------------------
 * A sealed pack is drawn from ONE collection, and the wrapper is how the
 * player finds out which. Every layer that keys off the art has to move
 * together — the background, the alpha mask the charge sheen sweeps through,
 * and the two tear halves — or the sheen lights the wrong silhouette and the
 * pack rips along the previous wrapper's seam.
 *
 * Each wrapper also pins its OWN aspect-ratio: the trimmed assets are not all
 * 460/765, and a shared ratio would stretch them. Ratios come from the
 * generator's own output (scripts/generateCollectionPackArt.js). */

.card-packs-stage[data-pack-collection="classic"] .card-packs-pack {
    aspect-ratio: 460 / 795;
}
.card-packs-stage[data-pack-collection="classic"] .card-packs-pack-art,
.card-packs-stage[data-pack-collection="classic"] .card-packs-pack-tearing .card-packs-pack-art::before,
.card-packs-stage[data-pack-collection="classic"] .card-packs-pack-tearing .card-packs-pack-art::after {
    background: url('../assets/images/packs/card-pack-classic.webp') center / 100% 100% no-repeat;
}
.card-packs-stage[data-pack-collection="classic"] .card-packs-pack-art::after {
    -webkit-mask: url('../assets/images/packs/card-pack-classic.webp') center / 100% 100% no-repeat;
    mask: url('../assets/images/packs/card-pack-classic.webp') center / 100% 100% no-repeat;
}

.card-packs-stage[data-pack-collection="mythos"] .card-packs-pack {
    aspect-ratio: 460 / 761;
}
.card-packs-stage[data-pack-collection="mythos"] .card-packs-pack-art,
.card-packs-stage[data-pack-collection="mythos"] .card-packs-pack-tearing .card-packs-pack-art::before,
.card-packs-stage[data-pack-collection="mythos"] .card-packs-pack-tearing .card-packs-pack-art::after {
    background: url('../assets/images/packs/card-pack-mythos.webp') center / 100% 100% no-repeat;
}
.card-packs-stage[data-pack-collection="mythos"] .card-packs-pack-art::after {
    -webkit-mask: url('../assets/images/packs/card-pack-mythos.webp') center / 100% 100% no-repeat;
    mask: url('../assets/images/packs/card-pack-mythos.webp') center / 100% 100% no-repeat;
}

.card-packs-stage[data-pack-collection="anime-archetypes"] .card-packs-pack {
    aspect-ratio: 460 / 764;
}
.card-packs-stage[data-pack-collection="anime-archetypes"] .card-packs-pack-art,
.card-packs-stage[data-pack-collection="anime-archetypes"] .card-packs-pack-tearing .card-packs-pack-art::before,
.card-packs-stage[data-pack-collection="anime-archetypes"] .card-packs-pack-tearing .card-packs-pack-art::after {
    background: url('../assets/images/packs/card-pack-anime-archetypes.webp') center / 100% 100% no-repeat;
}
.card-packs-stage[data-pack-collection="anime-archetypes"] .card-packs-pack-art::after {
    -webkit-mask: url('../assets/images/packs/card-pack-anime-archetypes.webp') center / 100% 100% no-repeat;
    mask: url('../assets/images/packs/card-pack-anime-archetypes.webp') center / 100% 100% no-repeat;
}

.card-packs-stage[data-pack-collection="public-domain"] .card-packs-pack {
    aspect-ratio: 460 / 766;
}
.card-packs-stage[data-pack-collection="public-domain"] .card-packs-pack-art,
.card-packs-stage[data-pack-collection="public-domain"] .card-packs-pack-tearing .card-packs-pack-art::before,
.card-packs-stage[data-pack-collection="public-domain"] .card-packs-pack-tearing .card-packs-pack-art::after {
    background: url('../assets/images/packs/card-pack-public-domain.webp') center / 100% 100% no-repeat;
}
.card-packs-stage[data-pack-collection="public-domain"] .card-packs-pack-art::after {
    -webkit-mask: url('../assets/images/packs/card-pack-public-domain.webp') center / 100% 100% no-repeat;
    mask: url('../assets/images/packs/card-pack-public-domain.webp') center / 100% 100% no-repeat;
}

.card-packs-stage[data-pack-collection="cute-creatures"] .card-packs-pack {
    aspect-ratio: 460 / 768;
}
.card-packs-stage[data-pack-collection="cute-creatures"] .card-packs-pack-art,
.card-packs-stage[data-pack-collection="cute-creatures"] .card-packs-pack-tearing .card-packs-pack-art::before,
.card-packs-stage[data-pack-collection="cute-creatures"] .card-packs-pack-tearing .card-packs-pack-art::after {
    background: url('../assets/images/packs/card-pack-cute-creatures.webp') center / 100% 100% no-repeat;
}
.card-packs-stage[data-pack-collection="cute-creatures"] .card-packs-pack-art::after {
    -webkit-mask: url('../assets/images/packs/card-pack-cute-creatures.webp') center / 100% 100% no-repeat;
    mask: url('../assets/images/packs/card-pack-cute-creatures.webp') center / 100% 100% no-repeat;
}

.card-packs-stage[data-pack-collection="fun"] .card-packs-pack {
    aspect-ratio: 460 / 764;
}
.card-packs-stage[data-pack-collection="fun"] .card-packs-pack-art,
.card-packs-stage[data-pack-collection="fun"] .card-packs-pack-tearing .card-packs-pack-art::before,
.card-packs-stage[data-pack-collection="fun"] .card-packs-pack-tearing .card-packs-pack-art::after {
    background: url('../assets/images/packs/card-pack-fun.webp') center / 100% 100% no-repeat;
}
.card-packs-stage[data-pack-collection="fun"] .card-packs-pack-art::after {
    -webkit-mask: url('../assets/images/packs/card-pack-fun.webp') center / 100% 100% no-repeat;
    mask: url('../assets/images/packs/card-pack-fun.webp') center / 100% 100% no-repeat;
}

.card-packs-stage[data-pack-collection="brainrot"] .card-packs-pack {
    aspect-ratio: 460 / 731;
}
.card-packs-stage[data-pack-collection="brainrot"] .card-packs-pack-art,
.card-packs-stage[data-pack-collection="brainrot"] .card-packs-pack-tearing .card-packs-pack-art::before,
.card-packs-stage[data-pack-collection="brainrot"] .card-packs-pack-tearing .card-packs-pack-art::after {
    background: url('../assets/images/packs/card-pack-brainrot.webp') center / 100% 100% no-repeat;
}
.card-packs-stage[data-pack-collection="brainrot"] .card-packs-pack-art::after {
    -webkit-mask: url('../assets/images/packs/card-pack-brainrot.webp') center / 100% 100% no-repeat;
    mask: url('../assets/images/packs/card-pack-brainrot.webp') center / 100% 100% no-repeat;
}

/* Collection-specific wrapper art is declared after the base tear rule and
   otherwise wins the cascade, repainting one intact wrapper underneath the two
   flying halves. Keep the collection image on the pseudos above, but remove it
   from the base layer for every real tear frame. */
.card-packs-stage[data-pack-collection] .card-packs-pack-tearing .card-packs-pack-art {
    background: none;
}

/* The saved-deck tile is bottom-anchored, so when the name + summoner + count
   stack outgrows it the TITLE slides off the top — photographed as the second
   deck's name cut mid-glyph, worst with the incomplete-deck badge ("incomplete
   decks render badly... also the default decks"). Every line is single-line
   ellipsized and the stack fits the tile. Appended after the restoration
   contract's canonical name-bar rule on purpose. */
@media (orientation: portrait) and (max-width: 768px),
       (orientation: landscape) and (max-height: 560px) {
    /* The bar holds THREE lines on a predefined deck — name, summoner, and the
       "Starter" badge — and at the 844x288 page box its content box is 33.5px,
       enough for two. Flex shrank the last two instead of the last one, so the
       summoner rendered 4px tall for 14px of text and the badge 8px for 13.7:
       two unreadable slivers rather than one dropped line. The badge moves
       BESIDE the summoner instead, which fits all three in two rows and hides
       nothing. */
    .deck-builder-root-mode .deck-list-name-bar {
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        align-content: center;
        justify-items: start;
        align-items: center;
        gap: 2px 6px;
        padding: 6px 10px;
    }

    .deck-builder-root-mode .deck-list-name-bar > .deck-list-name {
        grid-column: 1 / -1;
    }

    /* A line may not be squeezed below the box its own text needs. */
    .deck-builder-root-mode .deck-list-name-bar > * {
        min-height: min-content;
    }

    .deck-builder-root-mode .deck-list-name,
    .deck-builder-root-mode .deck-list-summoner {
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        max-width: 100%;
        line-height: 1.15;
    }

    .deck-builder-root-mode .deck-list-name {
        font-size: 15px;
    }

    .deck-builder-root-mode .deck-list-summoner {
        font-size: 12px;
    }

    .deck-builder-root-mode .deck-list-count {
        font-size: 11px;
        line-height: 1.2;
        padding: 2px 8px;
    }
}

/* Viewing a deck on a phone cut the mini grid mid-row with no way to reach
   the rest (photographed on a predefined read-only deck). The grid becomes
   the screen's one intentional scroll lane; the document itself stays locked.

   The selector used to be `.deck-builder-root-mode .deck-builder-deck-cards`,
   and OPENING a deck is precisely what removes `deck-builder-root-mode`
   (toggleDeckSections(false)) — so the rule never once applied to the screen it
   was written for. A starter's 15 cards were a 686px column inside a 72px
   `overflow: hidden` window with "Back to decks" across the cut. The grid only
   exists inside the deck panel, so it needs no mode prefix at all. */
@media (orientation: portrait) and (max-width: 768px),
       (orientation: landscape) and (max-height: 560px) {
    .deck-builder-deck-cards {
        overflow-y: auto;
        -webkit-overflow-scrolling: touch;
        overscroll-behavior: contain;
        /* pinch-zoom too: the lane holds cards (touch-policy.css). */
        touch-action: pan-y pinch-zoom;
    }

    /* …and a scroll lane still has to be worth scrolling. The read-only viewer
       spent 62px on a "Current Deck" label and a text box the player cannot
       type in, above an info line that already names the summoner, the count
       and the read-only state. The label goes, the name stays as the heading it
       actually is, and the panel's padding tightens — all of it bought for the
       cards, none of it out of the 44px "Back to decks" target. */
    .deck-builder-container.deck-builder-readonly .deck-builder-deck {
        padding: 8px 10px;
    }

    .deck-builder-container.deck-builder-readonly .deck-builder-deck
        > .deck-builder-section-header {
        margin-bottom: 4px;
    }

    .deck-builder-container.deck-builder-readonly .deck-builder-deck
        > .deck-builder-section-header > h3 {
        display: none;
    }

    .deck-builder-container.deck-builder-readonly .deck-name-input[readonly] {
        min-height: 0;
        padding: 2px 6px;
        border-color: transparent;
        background: transparent;
        font-size: 15px;
        font-weight: 600;
    }

    .deck-builder-container.deck-builder-readonly .deck-builder-deck-info {
        margin-bottom: 4px;
    }

    .deck-builder-container.deck-builder-readonly .deck-builder-deck-cards {
        margin-bottom: 2px;
    }

    /* The dialog's own title strip was 73px of a 275px page box — a quarter of
       the screen restating a heading the panel below already carries. Its close
       control keeps its 44px target; only the padding and the type shrink. */
    .deck-builder-container.deck-builder-readonly > .deck-builder-header {
        padding: 2px 12px;
    }

    .deck-builder-container.deck-builder-readonly > .deck-builder-header h2 {
        font-size: 1.05rem;
    }
}

/* ============================================================================
   Press feedback on touch: the summoner cards and the saved-deck rows.

   Both carried a `:hover` state and nothing else. On a coarse pointer that is
   two separate failures at once: there is no press feedback while the finger is
   down (the control reads as dead — "it's still not possible to trigger tap
   anims on summoner icons or decks"), and WebKit LATCHES the hover after the
   tap so the last thing touched stays lit until something else is.

   So: hover is fine-pointer only, and every pointer gets a real `:active`.
   Transform-only, on the compositor, and skipped under reduced motion.
   ========================================================================== */
@media (hover: none) {
    .db-character-card:hover,
    .deck-list-item:hover {
        background: revert;
        box-shadow: revert;
        border-color: revert;
    }

    /* The actions row is revealed by hover on a mouse; on touch it has to be
       reachable without one, or the row's own controls are unusable. */
    .deck-list-item .deck-list-actions {
        opacity: 1;
    }
}

.db-character-card:active,
.deck-list-item:active {
    transform: scale(0.972);
    transition-duration: 0.06s;
}

.db-character-card,
.deck-list-item {
    transition-property: background-color, border-color, box-shadow, transform;
    -webkit-tap-highlight-color: transparent;
}

@media (prefers-reduced-motion: reduce) {
    .db-character-card:active,
    .deck-list-item:active {
        transform: none;
        /* The press must still be legible without motion. */
        filter: brightness(1.18);
    }
}

@media (max-height: 460px) and (orientation: landscape) {
    /* See the note on the narrow grid above: the row cannot carry a third
       control without either spilling the lane or clipping translated text. */
    .moon-exchange-buy .moon-exchange-share { display: none; }
}


/* ===========================================================================
   THE MOON EXCHANGE UNLOCK, PINNED IN THE QUEST RAIL

   Same row geometry as every other quest — it inherits `.lobby-quest-item`, so
   it cannot change the rail's height, spacing or wrapping. Only the accent
   differs, and it is deliberately a colder blue than the invitation's violet
   so two pinned rows never read as one repeated thing.

   Both pinned rows are counted in BOTH halves of the rail's fraction, so
   neither costs a rolling-quest slot. Worst case the rail holds five rows:
   three rotating quests, the invitation, and this. That is the number the
   phone layout has to survive, and it is why nothing here adds height.
   =========================================================================== */
.lobby-quest-item--exchange {
    border-color: rgba(122, 192, 255, 0.40);
    background:
        linear-gradient(135deg, rgba(58, 122, 196, 0.20), transparent 62%),
        rgba(14, 22, 38, 0.72);
}

.lobby-quest-item--exchange .lobby-quest-bar-fill {
    background: linear-gradient(90deg, #7ac0ff, #b9e6ff);
}

.lobby-quest-item--exchange .lobby-quest-progress {
    color: #bfe0ff;
}

.lobby-quest-exchange-reward {
    grid-column: 1 / -1;
    color: #9fd4ff;
    font-size: 0.76rem;
    font-weight: 700;
    letter-spacing: 0.03em;
}

/* A phone rail is short and may now hold five rows. The reward caption is the
   one line that carries no number, so it is what yields first — the row keeps
   its target size and the rail keeps its scroll-free height. */
@media (orientation: landscape) and (max-height: 560px) {
    .lobby-quest-exchange-reward,
    .lobby-quest-invite-reward {
        display: none;
    }
}
