/* ==========================================================================
   STON.fi Cross-chain — Flight Deals
   ============================================================================
   Reward-shop board where users spend campaign miles on limited weekly
   deals. Visual register: a departures terminal — terminal-mono labels,
   bracketed CTAs, perforation strips, live "X / Y seats" counters. Cards
   stay in the existing boarding-pass family established by the profile
   pop-up so the campaign reads as one product, not several.

   Composes the design system (.section, .container, .btn, .pill, .eyebrow)
   from base.css / components.css / layout.css. Page-specific selectors
   are namespaced .deals-* / .deal__* to avoid bleeding into other pages.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Page hero — compact intro (no shader, no big breathing room).
   The deals grid is the product on this page; the hero just frames it,
   so we shave the default .section vertical padding and skip the dark
   atmospheric backdrop the home hero uses.
   -------------------------------------------------------------------------- */
.deals-hero {
    padding-top: var(--sp-9);
    padding-bottom: var(--sp-6);
    background:
        radial-gradient(60% 40% at 50% 0%,
            rgba(64, 209, 253, 0.06),
            transparent 70%);
}

.deals-hero .section__header { margin-bottom: 0; }

.missions__phase-preview {
    position: relative;
    margin: var(--sp-5) 0 0;
    padding: var(--sp-4) var(--sp-5);
    padding-left: calc(var(--sp-5) + 10px);
    background:
        linear-gradient(90deg, rgba(232, 76, 76, 0.14), rgba(232, 76, 76, 0.04) 55%, var(--color-bg-2));
    border: 1px dashed rgba(232, 76, 76, 0.72);
    border-radius: var(--radius-sharp);
    box-shadow:
        0 0 0 1px rgba(232, 76, 76, 0.16),
        0 0 28px rgba(232, 76, 76, 0.10);
    text-align: left;
}
.missions__phase-preview::before {
    content: "";
    position: absolute;
    inset: 0 auto 0 0;
    width: 8px;
    border-radius: var(--radius-sharp) 0 0 var(--radius-sharp);
    background: repeating-linear-gradient(
        -45deg,
        var(--color-danger) 0 5px,
        rgb(17, 17, 19) 5px 10px
    );
}
.missions__phase-preview-label {
    margin: 0 0 var(--sp-3);
    font-family: var(--font-mono);
    font-size: 12px;
    line-height: 1.4;
    letter-spacing: 0.03em;
    color: var(--color-fg-2);
}
.missions__phase-preview-badge {
    display: inline-block;
    margin-right: 0.55em;
    padding: 2px 7px;
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--color-bg);
    background: var(--color-danger);
    border-radius: var(--radius-sharp);
    vertical-align: 1px;
}
.missions__phase-preview-label strong {
    color: var(--color-danger);
    font-weight: 600;
}
.missions__phase-preview-live {
    display: inline-block;
    margin-left: 0.5em;
    white-space: nowrap;
    opacity: 0.75;
}
@media (max-width: 640px) {
    .missions__phase-preview-live {
        display: block;
        margin-left: 0;
        margin-top: 4px;
    }
}
.missions__phase-preview-nav {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}
.missions__phase-preview-link {
    font-family: var(--font-mono);
    font-size: 11px;
    line-height: 1;
    letter-spacing: 0.04em;
    text-transform: lowercase;
    text-decoration: none;
    color: var(--color-fg-2);
    padding: 6px 10px;
    border: 1px solid rgba(232, 76, 76, 0.28);
    border-radius: var(--radius-sharp);
    transition: border-color 160ms ease, color 160ms ease, background-color 160ms ease;
}
.missions__phase-preview-link:hover,
.missions__phase-preview-link:focus-visible {
    color: var(--color-danger);
    border-color: var(--color-danger);
}
.missions__phase-preview-link:focus-visible {
    outline: 2px solid var(--color-danger);
    outline-offset: 2px;
}
.missions__phase-preview-link.is-active {
    color: var(--color-bg);
    background: var(--color-danger);
    border-color: var(--color-danger);
}
.missions__phase-preview-link--clear {
    margin-left: auto;
}

/* --------------------------------------------------------------------------
   2. Balance bar — terminal status strip pinned between the hero and the
   grid. Renders on every page-load and gets reskinned by JS as the
   wallet connects (see flight-deals.js — flips [data-conn-state]).

   The bar is full-bleed: a faint top/bottom hairline and a subtle blue
   wash so it visually anchors as a "live data row" between the hero and
   the catalogue, the way an airport's monitor strip sits between
   marketing and the actual board.
   -------------------------------------------------------------------------- */
.deals-balance {
    border-top:    var(--hairline);
    border-bottom: var(--hairline);
    background:
        linear-gradient(180deg,
            rgba(64, 209, 253, 0.04) 0%,
            rgba(64, 209, 253, 0.00) 100%),
        rgba(255, 255, 255, 0.015);
}

.deals-balance__inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-6);
    padding-top: 18px;
    padding-bottom: 18px;
    flex-wrap: wrap;
}

.deals-balance__primary {
    display: inline-flex;
    align-items: baseline;
    gap: 12px;
    flex-wrap: wrap;
}

.deals-balance__label {
    font-family: var(--font-mono);
    font-size: 11px;
    line-height: 1;
    letter-spacing: 0.06em;
    text-transform: lowercase;
    color: var(--color-fg-4);
}
.deals-balance__label::before {
    content: "// ";
    color: var(--color-cyan);
    opacity: 0.7;
}

.deals-balance__value {
    display: inline-flex;
    align-items: baseline;
    gap: 8px;
    font-family: var(--font-display);
    font-weight: 500;
    font-size: 16px;
    line-height: 1.2;
    color: var(--color-fg-2);
}

.deals-balance__value-pre {
    font-family: var(--font-body);
    font-size: 14px;
    color: var(--color-fg-3);
}

.deals-balance__value strong {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 24px;
    line-height: 1;
    letter-spacing: -0.5px;
    /* Same cyan→blue gradient text used in the profile spendable-miles
       cell — visually links the two surfaces so users recognise their
       balance at a glance regardless of where they're looking. */
    background: linear-gradient(180deg,
        var(--color-cyan-glow) 0%,
        var(--color-cyan) 60%,
        var(--color-blue-bright) 100%);
    -webkit-background-clip: text;
            background-clip: text;
    -webkit-text-fill-color: transparent;
            color: transparent;
    font-variant-numeric: tabular-nums;
}

.deals-balance__value-unit {
    font-family: var(--font-mono);
    font-size: 12px;
    letter-spacing: 0.04em;
    text-transform: lowercase;
    color: var(--color-fg-3);
}

.deals-balance__value--idle {
    font-family: var(--font-body);
    font-style: italic;
    font-size: 14px;
    color: var(--color-fg-3);
}

/* State machine for the balance bar — same pattern wallet.js uses on
   the .step host (see home.css §step state machine). */
.deals-balance__inner[data-conn-state="idle"]      [data-deals-show="connected"] { display: none; }
.deals-balance__inner[data-conn-state="connected"] [data-deals-show="idle"]      { display: none; }

.deals-balance__meta {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-6);
    flex-wrap: wrap;
}

.deals-balance__meta-cell {
    display: inline-flex;
    flex-direction: column;
    gap: 2px;
}

.deals-balance__meta-label {
    font-family: var(--font-mono);
    font-size: 10px;
    line-height: 1;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--color-fg-4);
}

.deals-balance__meta-value {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 16px;
    line-height: 1;
    letter-spacing: 0.02em;
    color: var(--color-fg);
    text-transform: lowercase;
    font-variant-numeric: tabular-nums;
}

.deals-balance__meta-divider,
.deals-balance__meta-total {
    color: var(--color-fg-3);
    font-weight: 500;
}

.deals-balance__meta-value--accent { color: var(--color-cyan); }

.deals-balance__earn {
    font-family: var(--font-mono);
    font-size: 12px;
    letter-spacing: 0.04em;
    text-transform: lowercase;
    color: var(--color-cyan);
    text-decoration: none;
    border-bottom: 1px dashed rgba(64, 209, 253, 0.30);
    padding-bottom: 1px;
    transition:
        color var(--dur-hover) var(--ease),
        border-color var(--dur-hover) var(--ease);
}
.deals-balance__earn:hover {
    color: var(--color-cyan-glow);
    border-bottom-color: var(--color-cyan);
}

@media (max-width: 720px) {
    .deals-balance__inner {
        flex-direction: column;
        align-items: flex-start;
        gap: var(--sp-4);
    }
    .deals-balance__meta {
        gap: var(--sp-5);
    }
}

/* --------------------------------------------------------------------------
   3. Deals section frame
   -------------------------------------------------------------------------- */
.deals-section {
    position: relative;
    padding-top: var(--sp-7);
    background-image:
        radial-gradient(45% 35% at 90%  10%, rgba(64, 209, 253, 0.06), transparent 70%),
        radial-gradient(45% 35% at  8% 90%, rgba(36, 141, 255, 0.05), transparent 70%);
    background-repeat: no-repeat;
}

.deals-section__inner {
    position: relative;
}

.deals-section__footnote {
    margin: var(--sp-7) 0 0;
    text-align: center;
    font-family: var(--font-mono);
    font-size: 11px;
    line-height: 1.5;
    letter-spacing: 0.04em;
    text-transform: lowercase;
    color: var(--color-fg-4);
}

/* --------------------------------------------------------------------------
   4. Grid — 2 columns desktop, 1 mobile.
   Updated from the previous 3-col layout so each card has room to fit
   the side-by-side text + image upper region introduced with the prize-
   image redesign (see §5). Single column kicks in at the same 600px
   breakpoint the rest of the site uses for phone widths.
   -------------------------------------------------------------------------- */
.deals-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--sp-5);
    align-items: stretch;
}

@media (max-width: 600px) {
    .deals-grid { grid-template-columns: 1fr; }
}

/* --------------------------------------------------------------------------
   5. Deal card — the workhorse
   --------------------------------------------------------------------------
   The card is a CSS grid with four named row areas:
     "text image"   — upper region: prose left, optional image flush-right
     "perf perf"    — boarding-pass perforation strip (full width)
     "foot foot"    — price/seats row + two-button action row (full width)
     "details details" — collapsible disclosure panel (full width)

   The text row absorbs extra vertical space (`grid-template-rows: 1fr auto
   auto auto`) so cards of varying description lengths still keep their
   foot rows aligned across the grid — same property the old flex-column
   layout achieved via `margin-top: auto` on the perf strip.

   When the deal has no image, .deal--no-image collapses to a single
   column. On mobile (≤600px) every card stacks the image above the
   text. State variants come in via [data-status] (server-side) and
   [data-affordable] (JS-derived); composing them lets us express
   every visible state without one class per state.
   -------------------------------------------------------------------------- */
.deal {
    position: relative;
    display: grid;
    grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
    grid-template-areas:
        "text image"
        "perf perf"
        "foot foot";
    grid-template-rows: 1fr auto auto;
    column-gap: 16px;
    row-gap: 12px;
    padding: 18px 18px 16px;
    background-color: var(--color-bg-2);
    background-image: var(--grad-card-glow);
    border: var(--hairline);
    border-radius: var(--radius-sharp);
    overflow: hidden;
    /* No hover transform / lift — the whole card has no click target
       (only the two buttons inside are clickable), so signalling
       "tappable" at the card level would mislead the user. */
}

/* Single-column variant when the deal has no image. The card still
   uses grid (not flex) so the perf-strip + foot rows behave the same
   way as the two-col variant. */
.deal--no-image {
    grid-template-columns: 1fr;
    grid-template-areas:
        "text"
        "perf"
        "foot";
}

/* Sealed partner cargo uses the same boarding-pass grid as a live
   deal. Extra row height goes into the crate (`1fr`), not a hollow
   foot — the lock-note foot stays `auto`. Compact only on phones,
   where cards stack and matching a neighbour does not apply. */
.deal--teaser .deal__foot {
    grid-template-columns: minmax(0, 1fr);
}
.deal--teaser .deal__lock-note {
    grid-column: 1;
}

/* --- Text side --------------------------------------------------------- */
.deal__text {
    grid-area: text;
    display: flex;
    flex-direction: column;
    gap: 10px;
    min-width: 0;   /* let flex children shrink instead of overflowing */
}

/* --- Media side -------------------------------------------------------
   Right-side image wrapper. Negative top/right margins cancel the card's
   own padding so the image clips to the card's rounded top-right corner
   (.deal has overflow: hidden). The wrapper is the grid cell; the
   <img> inside fills it and uses object-fit: cover so any aspect ratio
   reads cleanly. Pill positioning is owned by .deal__pill itself
   (absolute relative to .deal — see below). */
.deal__media-side {
    grid-area: image;
    align-self: stretch;
    margin-right: -18px;
    margin-top:   -18px;
    /* Cancel the .deal grid's row-gap (12px) on the image side so the
       image bottom edge sits flush against the perforation strip. The
       text side keeps the natural gap because text content is shorter
       and the gap reads as breathing room there. */
    margin-bottom: -12px;
}

.deal__media {
    position: relative;
    width: 100%;
    height: 100%;
    aspect-ratio: 1 / 1;   /* square in the 2-col layout */
    min-height: 160px;
    overflow: hidden;
    background: rgba(255, 255, 255, 0.02);
}

/* Reset when the media box is the lightbox opener (single-shot deals). */
.deal__media--zoom {
    display: block;
    padding: 0;
    border: 0;
    color: inherit;
    font: inherit;
    text-align: inherit;
    cursor: zoom-in;
}
.deal__media--zoom:focus-visible {
    outline: 2px solid var(--color-cyan);
    outline-offset: -2px;
}

/* Corner-expand hint — top-left so it clears status pills (top-right)
   and carousel nav (bottom). Hidden until hover on pointer devices;
   always visible on touch so the affordance isn't a secret. */
.deal__media-expand {
    position: absolute;
    top: 8px;
    left: 8px;
    z-index: 1;
    width: 26px;
    height: 26px;
    display: grid;
    place-items: center;
    border: 1px solid rgba(255, 255, 255, 0.18);
    border-radius: var(--radius-sharp);
    background: rgba(10, 12, 16, 0.62);
    color: var(--color-fg);
    pointer-events: none;
    opacity: 0;
    transition: opacity var(--dur-hover, 160ms) var(--ease, ease);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
}
.deal__media-expand svg {
    width: 12px;
    height: 12px;
}
.deal__media:hover .deal__media-expand,
.deal__media:focus-visible .deal__media-expand {
    opacity: 1;
}
@media (hover: none) {
    .deal__media-expand { opacity: 0.9; }
}

.deal__media img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* Multi-shot prize carousel. The media box stays the same square (or
   16:9 on phone). Extra shots are sibling files (`-back`, `-worn`, …)
   discovered at render time. Track is scroll-snap so swipe works
   without JS; prev/next + runway-light dots are the JS-enhanced
   controls. Nav sits at the bottom so it clears the status pills. */
.deal__media--carousel {
    position: relative;
    display: flex;
    flex-direction: column;
}

.deal__carousel-track {
    display: flex;
    flex: 1 1 auto;
    min-height: 0;
    height: 100%;
    cursor: zoom-in;
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;
    -webkit-overflow-scrolling: touch;
    touch-action: pan-x pan-y;
    scrollbar-width: none;
}
.deal__carousel-track::-webkit-scrollbar { display: none; }

.deal__carousel-slide {
    flex: 0 0 100%;
    width: 100%;
    height: 100%;
    margin: 0;
    scroll-snap-align: start;
    scroll-snap-stop: always;
}

.deal__carousel-slide img {
    pointer-events: none;
    user-select: none;
}

.deal__carousel-nav {
    position: absolute;
    left: 8px;
    right: 8px;
    bottom: 8px;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    pointer-events: none;
}

.deal__carousel-btn {
    pointer-events: auto;
    flex: 0 0 auto;
    width: 26px;
    height: 26px;
    padding: 0;
    display: grid;
    place-items: center;
    border: 1px solid rgba(255, 255, 255, 0.18);
    border-radius: var(--radius-sharp);
    background: rgba(10, 12, 16, 0.62);
    color: var(--color-fg);
    font-size: 18px;
    line-height: 1;
    cursor: pointer;
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
}
.deal__carousel-btn:hover {
    border-color: rgba(64, 209, 253, 0.55);
    color: var(--color-cyan);
}
.deal__carousel-btn:focus-visible {
    outline: 2px solid var(--color-cyan);
    outline-offset: 2px;
}

.deal__carousel-dots {
    pointer-events: auto;
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 4px 8px;
    border-radius: var(--radius-pill);
    background: rgba(10, 12, 16, 0.55);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
}

.deal__carousel-dot {
    width: 6px;
    height: 6px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.32);
    cursor: pointer;
}
.deal__carousel-dot.is-active {
    background: var(--color-cyan);
    box-shadow: 0 0 8px rgba(64, 209, 253, 0.55);
}
.deal__carousel-dot:focus-visible {
    outline: 2px solid var(--color-cyan);
    outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
    .deal__carousel-track { scroll-behavior: auto; }
}

/* Prize-photo lightbox. Wider than the default 480px modal so the
   1200 WebP can actually be inspected. Image uses contain (not cover)
   so engraving / sleeve type is not cropped. */
.modal__panel--photo {
    width: min(680px, calc(100vw - 40px), calc(100vh - 160px));
    max-height: 100%;
    overflow: hidden;
}

.deal-photo-modal {
    padding: 0 0 16px !important;
}

.deal-photo-modal__stage {
    position: relative;
    aspect-ratio: 1 / 1;
    background: rgb(10, 10, 12);
    overflow: hidden;
}

.deal-photo-modal__track {
    display: flex;
    height: 100%;
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;
    -webkit-overflow-scrolling: touch;
    touch-action: pan-x pan-y;
    scrollbar-width: none;
}
.deal-photo-modal__track::-webkit-scrollbar { display: none; }

.deal-photo-modal__slide {
    flex: 0 0 100%;
    width: 100%;
    height: 100%;
    margin: 0;
    scroll-snap-align: start;
    scroll-snap-stop: always;
}

.deal-photo-modal__slide img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
    user-select: none;
    pointer-events: none;
}

.deal-photo-modal__nav {
    position: absolute;
    left: 10px;
    right: 10px;
    bottom: 10px;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
}
/* Author `display: flex` otherwise overrides the UA [hidden] rule. */
.deal-photo-modal__nav[hidden] { display: none; }

@media (prefers-reduced-motion: reduce) {
    .deal-photo-modal__track { scroll-behavior: auto; }
}

/* Status pill stack — absolute container in the card's top-right corner.
   Holds the server-rendered pill (if any) plus any JS-added pills (need
   more miles, etc) in a vertical stack so multiple pills can coexist
   without overlapping. Always present in the DOM even when empty so
   flight-deals.js has a stable [data-deal-pill-stack] hook to append
   into. z-index: 2 keeps the stack above the prize image. */
.deal__pill-stack {
    position: absolute;
    top: 12px;
    right: 12px;
    z-index: 2;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 6px;
    pointer-events: none;   /* pills are decorative; clicks pass through */
}
.deal__pill-stack .deal__pill {
    pointer-events: auto;
}

/* Pill variants — same set the old card had:
     · hot      → still available but flagged ("almost gone", "new")
     · soldout  → no inventory
     · locked   → unavailable yet
     · claimed  → user already redeemed
     · cancelled → claim was reversed
     · need     → JS-applied when balance < price (added by flight-deals.js) */
.deal__pill {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    height: 22px;
    padding: 0 10px;
    font-family: var(--font-mono);
    font-weight: 500;
    font-size: 10px;
    line-height: 1;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    border-radius: var(--radius-pill);
    background: rgba(255, 255, 255, 0.04);
    border: 1px solid var(--color-stroke-2);
    color: var(--color-fg-2);
    white-space: nowrap;
    /* Subtle backdrop so the pill stays legible regardless of what's
       behind it (image, gradient, dark card). */
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
}
.deal__pill--hot {
    color: var(--color-cyan);
    background: rgba(64, 209, 253, 0.06);
    border-color: rgba(64, 209, 253, 0.32);
}
.deal__pill--soldout {
    color: rgb(255, 138, 138);
    background: rgba(255, 80, 80, 0.06);
    border-color: rgba(255, 80, 80, 0.32);
}
.deal__pill--locked {
    color: var(--color-fg-3);
    background: rgba(255, 255, 255, 0.04);
    border-color: var(--color-stroke-2);
}
.deal__pill--claimed {
    color: var(--color-success);
    background: rgba(4, 218, 141, 0.08);
    border-color: rgba(4, 218, 141, 0.40);
}
/* Cancelled — either-side cancellation surfaces identically. Same muted
   palette as the locked pill (user can't act on it) but with a faintly
   warmer tint (rose-stoneware) to distinguish from "not yet released". */
.deal__pill--cancelled {
    color: var(--color-fg-3);
    background: rgba(255, 153, 153, 0.05);
    border-color: rgba(255, 153, 153, 0.28);
}
.deal__pill--need {
    color: var(--color-fg-3);
    background: rgba(255, 255, 255, 0.04);
    border-color: var(--color-stroke-2);
}
.deal__pill--claimed::before {
    content: "✓";
    font-size: 11px;
}

/* --- Title block ------------------------------------------------------- */
.deal__name {
    margin: 0;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 18px;
    line-height: 1.25;
    letter-spacing: -0.2px;
    color: var(--color-fg);
    /* Reserve a sliver of right padding on cards that show the status
       pill so the pill never overlaps the name on no-image cards
       (where the pill floats top-right over the bare card). */
}
.deal--no-image .deal__name { padding-right: 86px; }

.deal__lede {
    margin: 0;
    font-family: var(--font-body);
    font-size: 13px;
    line-height: 1.5;
    color: var(--color-fg-2);
    /* No line-clamp now that descriptions live in their own column
       and the foot is anchored by the grid (not a height-driven flex
       layout). Long copy is welcome — it just makes the card taller,
       which align-items: stretch on .deals-grid still reconciles. */
}

/* --- Perforation strip (boarding-pass family) ------------------------- */
.deal__perf {
    grid-area: perf;
    display: block;
    width: calc(100% + 36px);
    margin: 0 -18px;
    height: 1px;
    background-image: linear-gradient(
        to right,
        transparent 0,
        transparent 6px,
        var(--color-stroke) 6px,
        var(--color-stroke) 12px
    );
    background-size: 12px 1px;
    background-repeat: repeat-x;
    position: relative;
    /* No margin-top: auto — the grid's row template (1fr auto auto auto)
       already gives the text row all the slack, pushing perf/foot/details
       to the card foot the same way the old flex layout did. */
}
.deal__perf::before,
.deal__perf::after {
    content: "";
    position: absolute;
    top: -3px;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--color-bg);
    border: 1px solid var(--color-stroke);
}
.deal__perf::before { left: -1px; }
.deal__perf::after  { right: -1px; }

/* --- Card foot --------------------------------------------------------- */
/* The foot is a 2-col CSS grid:
     col 1: `auto` — sized to the wider of (price content, View details
            button content). Stays constant across all cards on the
            grid because the View details label is always the same
            string.
     col 2: `minmax(0, 1fr)` — takes the remaining width. The Claim
            button lives here and can grow as long as the text needs
            (e.g., "Claim" vs "Need 6,700 more miles").

   .deal__meta and .deal__actions are subgrid rows so the price/seats
   cells in the upper row line up exactly with the View details / Claim
   cells below. Subgrid is supported across all modern browsers we care
   about (Chrome 117+, Safari 16+, Firefox 71+). */
.deal__foot {
    grid-area: foot;
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: 10px;
    row-gap: 12px;
}

.deal__meta {
    grid-column: 1 / -1;
    display: grid;
    grid-template-columns: subgrid;
    align-items: center;
}
.deal__meta .deal__price {
    justify-self: start;
}
.deal__meta .deal__seats {
    min-width: 0;
}

/* Lock note + post-claim flash messages span both columns. */
.deal__lock-note,
.deal__helper {
    grid-column: 1 / -1;
}

.deal__price {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 6px;
}
.deal__price-row {
    display: inline-flex;
    align-items: baseline;
    gap: 6px;
}

.deal__price-value {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 28px;
    line-height: 1;
    letter-spacing: -0.5px;
    color: var(--color-fg);
    font-variant-numeric: tabular-nums;
    background: linear-gradient(180deg,
        var(--color-cyan-glow) 0%,
        var(--color-cyan) 60%,
        var(--color-blue-bright) 100%);
    -webkit-background-clip: text;
            background-clip: text;
    -webkit-text-fill-color: transparent;
            color: transparent;
}

.deal__price-unit {
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.06em;
    text-transform: lowercase;
    color: var(--color-fg-3);
}

/* Minimum flight class under the miles price — scannable gate signal. */
.deal__class {
    margin: 0;
    display: inline-flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 6px;
    font-family: var(--font-mono);
    font-size: 11px;
    line-height: 1.2;
    letter-spacing: 0.04em;
    text-transform: lowercase;
    color: var(--color-fg-3);
}
.deal__class-label {
    color: var(--color-fg-4);
}
.deal__class-value {
    font-weight: 600;
    color: var(--color-cyan);
    text-transform: none;
}
.deal__class-plus {
    color: var(--color-fg-4);
}

/* --- Seats row + bar --------------------------------------------------- */
.deal__seats {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.deal__seats-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
}

.deal__seats-label {
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--color-fg-4);
}

.deal__seats-count {
    font-family: var(--font-mono);
    font-size: 12px;
    letter-spacing: 0.02em;
    color: var(--color-fg-2);
    font-variant-numeric: tabular-nums;
}
.deal__seats-count strong {
    color: var(--color-fg);
    font-weight: 500;
}
.deal__seats-divider,
.deal__seats-total { color: var(--color-fg-3); }

.deal__seats-bar {
    width: 100%;
    height: 4px;
    background: rgba(255, 255, 255, 0.06);
    border-radius: 2px;
    overflow: hidden;
}
.deal__seats-bar-fill {
    --progress-fill: 0;
    height: 100%;
    width: calc(var(--progress-fill) * 1%);
    background-image: var(--grad-progress);
    background-repeat: no-repeat;
    background-position: 0 0;
    background-size:
        calc(10000% / max(var(--progress-fill), 0.001)) 100%;
    border-radius: inherit;
    transition: width var(--dur-cosmic) var(--ease);
}

/* --- Lock note --------------------------------------------------------- */
.deal__lock-note {
    margin: 0;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.04em;
    text-transform: lowercase;
    color: var(--color-fg-3);
}
.deal__lock-note span[aria-hidden] {
    color: var(--color-cyan);
    font-size: 13px;
}

/* --- Actions row + CTAs -----------------------------------------------
   Two-button footer: View details (ghost) + Claim (primary). Inherits
   the foot's subgrid columns so View details sits flush below the price
   cell and Claim sits flush below the seats cell — same left/right
   alignment across the two rows. View details has a fixed text label so
   the col-1 width is stable; the Claim button takes the remainder, with
   enough room for either "Claim" or "Need 6,700 more miles". The
   .deal__details-spacer keeps col 1 occupied on deals that don't render
   a View details button, so Claim doesn't slide left. */
.deal__actions {
    grid-column: 1 / -1;
    display: grid;
    grid-template-columns: subgrid;
    align-items: stretch;
}

.deal__cta {
    /* Reuses .btn / .btn--primary from components.css. The previous
       layout used .btn--block (100% width); the new subgrid does the
       width allocation, so we don't need block. The button text may
       run longer than "Claim" (e.g., "Need 6,700 more miles") — we
       drop the fixed horizontal padding so the button stays snug
       around its label even when the label is short, and let the
       subgrid column size to whichever is wider across the rendered
       cards. Disabled state reads as "informational" rather than
       "broken". */
    text-transform: lowercase;
    letter-spacing: 0.02em;
    padding: 0 16px;
    white-space: nowrap;   /* keep the diff label on a single line */
}
.deal__cta[disabled] {
    cursor: not-allowed;
    opacity: 0.45;
    background: rgba(255, 255, 255, 0.04);
    color: var(--color-fg-3);
    border-color: var(--color-stroke);
    box-shadow: none;
    /* Override .btn--primary's gradient so the disabled state isn't a
       half-faded blue blob. */
    background-image: none;
}

/* View-details disclosure button — ghost styling, with a small eye
   icon to the left of the label. Sits in the left half of the
   actions grid; the bracket wrapping comes from .btn--ghost::before
   / ::after in components.css so the text reads "[ View details ]"
   like every other text button on the site. */
.deal__details-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 0 14px;
    text-transform: lowercase;
    letter-spacing: 0.02em;
    cursor: pointer;
}
.deal__details-eye {
    display: inline-flex;
    width: 16px;
    height: 16px;
    color: currentColor;
    opacity: 0.85;
}
.deal__details-eye svg {
    width: 100%;
    height: 100%;
}
/* When a deal has no prose to disclose, render an invisible spacer
   in the actions grid's left cell so the Claim button stays in its
   right-hand slot (consistent placement across the catalogue). */
.deal__details-spacer {
    display: block;
    min-height: 1px;
}

/* Post-claim flash slot — used by flight-deals.js to surface
   state-transition messages from /api/flight-deals/claim errors
   (sold_out, deal_not_claimable). Affordability copy now lives in
   the Claim button label itself, so this slot is empty on first
   render and stays hidden until JS writes into it. The [hidden]
   override below cancels the display: flex so an empty helper
   produces NO visual artifact — the user only sees a callout
   when there's an actual flash message to show. */
.deal__helper {
    margin: 0;
    padding: 10px 12px;
    display: flex;
    align-items: flex-start;
    gap: 8px;
    font-family: var(--font-body);
    font-size: 12.5px;
    line-height: 1.4;
    color: var(--color-fg-2);
    background: rgba(64, 209, 253, 0.05);
    border: 1px solid rgba(64, 209, 253, 0.22);
    border-radius: var(--radius-sharp);
    text-align: left;
}
.deal__helper[hidden] {
    /* Cancel the display: flex so [hidden]'s default display: none
       actually wins — without this override the helper renders as
       an empty cyan box on every card. */
    display: none;
}
.deal__helper::before {
    content: "⚠";
    color: var(--color-cyan);
    font-size: 13px;
    line-height: 1.3;
    flex-shrink: 0;
}
.deal__helper strong {
    color: var(--color-cyan);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

/* --- Details modal ----------------------------------------------------
   View details on each card opens a shared modal (#deal-details — see
   flight-deals.php) that displays three icon-prefixed prose rows
   (Unlock conditions / Delivery / Restrictions) plus an optional
   "you need X more miles" callout. The row styles below are scoped
   to .deal__details-row and reused inside the modal — the icon box
   matches the dimensions of the dropped .deal__glyph from the pre-
   redesign card so the visual register stays consistent.
   --------------------------------------------------------------------- */
.deal-details-modal__rows {
    display: flex;
    flex-direction: column;
    gap: 14px;
}

.deal__details-row {
    display: grid;
    grid-template-columns: 36px minmax(0, 1fr);
    gap: 12px;
    align-items: start;
}
.deal__details-row[hidden] {
    display: none !important;
}

.deal__details-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    color: var(--color-cyan);
    background: rgba(64, 209, 253, 0.06);
    border: 1px solid rgba(64, 209, 253, 0.30);
    border-radius: var(--radius-sharp);
    flex-shrink: 0;
}
.deal__details-icon svg {
    width: 20px;
    height: 20px;
}

.deal__details-body {
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 1px;
}
/* .modal__body p { margin-bottom: 12px } is more specific than
   .deal__details-label / __prose, so it opened a fat gap under
   every header. Cancel it inside this modal only. */
.deal-details-modal p {
    margin: 0;
}

.deal__details-label {
    margin: 0;
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 13px;
    line-height: 1.3;
    letter-spacing: -0.1px;
    color: var(--color-fg);
}

.deal__details-prose {
    margin: 0;
    font-family: var(--font-body);
    font-size: 13px;
    line-height: 1.5;
    color: var(--color-fg-2);
}

/* --------------------------------------------------------------------------
   6. State variants per [data-status]
   -------------------------------------------------------------------------- */

/* Sold out — strikethrough price, muted card, kept visible (FOMO). */
.deal[data-status="sold-out"] {
    opacity: 0.82;
}
.deal[data-status="sold-out"] .deal__price-value {
    text-decoration: line-through;
    text-decoration-thickness: 1.5px;
    text-decoration-color: rgba(255, 80, 80, 0.55);
}
.deal[data-status="sold-out"] .deal__seats-bar-fill {
    background: linear-gradient(90deg, rgb(255, 138, 138), rgba(255, 80, 80, 0.65));
}

/* Locked — greyed out; the lock note in the card foot tells the user
   what condition unlocks the deal, and the details panel (if opened)
   has the unlock_conditions prose for more context. */
.deal[data-status="locked"] {
    opacity: 0.86;
}
.deal[data-status="locked"] .deal__price-value {
    background: none;
    -webkit-text-fill-color: var(--color-fg-3);
            color: var(--color-fg-3);
}

/* Claimed — price stays in the success register. The CTA is a
   disabled green `[ claimed ]` (no top-right pill; the button is
   the confirmation). Overrides both .btn[disabled] greying and
   the generic .deal__cta[disabled] mute so it still reads as
   earned, just not actionable. */
.deal[data-status="claimed"] .deal__price-value {
    background: none;
    -webkit-text-fill-color: var(--color-success);
            color: var(--color-success);
}
.deal[data-status="claimed"] .deal__cta[disabled] {
    cursor: default;
    opacity: 0.72;
    background: rgba(4, 218, 141, 0.08);
    background-image: none;
    border: 1px solid rgba(4, 218, 141, 0.32);
    color: var(--color-success);
    box-shadow: none;
    text-shadow: none;
    pointer-events: none;
}

/* Affordability — JS-applied when balance < price on otherwise-available
   deals. Uses the same disabled-CTA treatment but keeps the price + seats
   in full colour so the card still reads as "you could earn more and come
   back" rather than "this is broken". */
.deal[data-status="available"][data-affordable="false"] .deal__cta {
    cursor: not-allowed;
    opacity: 0.45;
    background: rgba(255, 255, 255, 0.04);
    color: var(--color-fg-3);
    border-color: var(--color-stroke);
    box-shadow: none;
    background-image: none;
}

/* --------------------------------------------------------------------------
   7. Responsive — mobile single-col stacks image above text
   --------------------------------------------------------------------------
   At ≤600px the .deals-grid drops to one column; each card also
   reshapes from a 2-col upper region (text + image) to a vertical
   stack (image on top, text below). The image goes back to a
   landscape 16:9 like the pre-redesign layout — it reads better as
   a header strip on narrow widths than as a constrained square.
   -------------------------------------------------------------------------- */
@media (max-width: 600px) {
    .deal--has-image {
        grid-template-columns: 1fr;
        grid-template-areas:
            "image"
            "text"
            "perf"
            "foot";
        grid-template-rows: auto 1fr auto auto;
    }
    .deal--teaser {
        align-self: start;
        align-content: start;
    }
    .deal--teaser.deal--has-image {
        grid-template-rows: auto auto auto auto;
    }
    .deal--has-image .deal__media-side {
        /* Full-bleed top; no negative bottom margin needed here because
           the row layout is stacked and the next row (text) sits flush
           below the image without an extra gap. */
        margin: -18px -18px 0;
    }
    .deal--has-image .deal__media {
        aspect-ratio: 16 / 9;
        min-height: 0;
    }
    /* Drop subgrid on phone widths and pick layouts that suit each
       row independently:
         - .deal__meta keeps a side-by-side price + seats (auto / 1fr
           sized so the price stays snug and seats stretches);
         - .deal__actions stacks the two CTAs so the primary "Claim"
           always sits on its own row with full thumb reach. */
    .deal__meta {
        grid-template-columns: auto minmax(0, 1fr);
        column-gap: 14px;
    }
    .deal__actions {
        grid-template-columns: 1fr;
        /* Stack the two CTAs vertically with the same 10px spacing
           the desktop layout uses between View details and Claim
           horizontally — without this they end up flush against
           each other when the subgrid is overridden. */
        row-gap: 10px;
    }
    /* Tighter details rows inside the modal on phone widths. */
    .deal__details-row {
        grid-template-columns: 32px minmax(0, 1fr);
        gap: 10px;
    }
    .deal__details-icon {
        width: 32px;
        height: 32px;
    }
    .deal__details-icon svg {
        width: 18px;
        height: 18px;
    }
}

/* --------------------------------------------------------------------------
   8. (removed) Connect banner & guest blocking layer
   --------------------------------------------------------------------------
   The deals-connect sticky banner + the [data-conn-state="idle"] grid blur
   used to gate the catalogue behind a "Connect a wallet to spend miles"
   CTA. Both were removed so guests can browse the full Flight Deals
   inventory unblocked — per the 29-05 campaign plan §9 (Flight Deals are
   the visible weekly reward shop; gating disclosure behind a wallet hurts
   FOMO, the whole point of weekly inventory). Per-deal CTAs still flip to
   "Connect to claim" (disabled) for guests via flight-deals.js, which is
   informational and non-blocking. The .deals-stage wrapper element was
   also removed from the HTML; .deals-grid now sits directly inside
   .deals-section__inner.
   -------------------------------------------------------------------------- */
