/* ==========================================================================
   STON.fi Cross-chain — Components
   Reusable building blocks. Every page composes these. No page-specific
   selectors live here — those go in the page's own stylesheet.

   Recipes follow docs/design-system.md §5.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Eyebrow
   `// products`, `// 01 — trade` — mono, lowercase, slash-prefixed.

   First-level (section above-headline) eyebrow only. The whole label
   — slashes and text — is filled with --grad-eyebrow (soft cyan → pale
   desaturated blue). Card-internal "// step 1 of 2" style sub-eyebrows
   have their own selectors (.step__index, .cat__index, …) and keep
   the solid cyan-slash + grey-text grammar so the two levels read as
   a clear hierarchy.

   `color: transparent` is the fallback for browsers that ignore
   `-webkit-text-fill-color`; the explicit fallback also keeps the
   text from flashing solid white during initial paint before the
   background-clip mask kicks in.
   -------------------------------------------------------------------------- */
.eyebrow {
    /* display: inline-block is load-bearing — `background-clip: text`
       clips the gradient to glyph shapes, but the gradient itself is
       sized to the ELEMENT BOX. As a default block-level <p>, .eyebrow
       stretches to its parent's width (~1130px), so the text would
       only reveal the leftmost ~13% of --grad-eyebrow (purely blue —
       the cyan mid and warm-haze end live way to the right of any
       glyph). Inline-block shrinks the box to the text width, so the
       gradient's 0%→100% span is anchored to the visible text. Also
       happens to center cleanly inside `.hero { text-align: center }`
       and similar contexts without needing margin: auto. */
    display: inline-block;
    margin: 0 0 var(--sp-4);
    font-family: var(--font-mono);
    font-weight: 400;
    font-size: 11px;
    line-height: 1.4;
    letter-spacing: 0.06em;
    text-transform: lowercase;
    color: transparent;
    background-image: var(--grad-eyebrow);
    -webkit-background-clip: text;
            background-clip: text;
    -webkit-text-fill-color: transparent;
}
.eyebrow::before {
    content: "// ";
}

/* --------------------------------------------------------------------------
   Buttons
   Primary CTA = `--grad-cta` (one per fold).
   All text-buttons render as `[ label ]` via pseudo-elements + lowercase.
   Heights 38 / 53 / 68 px (sm / md / lg).
   -------------------------------------------------------------------------- */
.btn {
    --btn-h: 38px;

    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--sp-2);
    height: var(--btn-h);
    padding: 0 18px;
    font-family: var(--font-mono);
    font-weight: 400;
    font-size: 14px;
    line-height: 1;
    text-align: center;
    text-decoration: none;
    text-transform: lowercase;
    color: var(--color-fg);
    background: transparent;
    border: 1px solid transparent;
    border-radius: var(--radius-sharp);
    white-space: nowrap;
    cursor: pointer;
    transition:
        opacity      var(--dur-hover) var(--ease),
        border-color var(--dur-hover) var(--ease),
        color        var(--dur-hover) var(--ease),
        background   var(--dur-hover) var(--ease),
        transform    80ms             var(--ease);
}

.btn:hover    { opacity: 0.85; }
.btn:active   { transform: scale(0.98); }
.btn[disabled],
.btn[aria-disabled="true"] {
    opacity: 0.4;
    cursor: not-allowed;
    pointer-events: none;
}

/* Square-bracket wrapping for text buttons — `[ label ]`.
   Skipped on icon-only buttons (`.btn--icon`) and `.btn--bare`. */
.btn--primary::before,
.btn--secondary::before,
.btn--ghost::before {
    content: "[\00a0";
    opacity: 0.7;
}
.btn--primary::after,
.btn--secondary::after,
.btn--ghost::after {
    content: "\00a0]";
    opacity: 0.7;
}

/* Sizes */
.btn--sm { --btn-h: 38px; padding: 0 16px; }
.btn--lg { --btn-h: 53px; padding: 0 28px; }
.btn--xl { --btn-h: 68px; padding: 0 36px; }
.btn--block { display: flex; width: 100%; }

/* Variants */
.btn--primary {
    color: #081020;
    background: var(--grad-cta);
    border-color: transparent;
    box-shadow: var(--shadow-glow-blue);
    font-weight: 500;
}
.btn--primary:hover { opacity: 0.92; }

.btn--secondary {
    color: var(--color-fg);
    background: transparent;
    border-color: var(--color-stroke);
}
.btn--secondary:hover {
    opacity: 1;
    border-color: var(--color-stroke-2);
}

.btn--ghost {
    color: var(--color-fg-2);
    background: transparent;
    border-color: var(--color-stroke);
}
.btn--ghost:hover {
    opacity: 1;
    color: var(--color-fg);
    border-color: var(--color-stroke-2);
}

/* Icon-only square button */
.btn--icon {
    width: 40px;
    height: 40px;
    padding: 0;
    border-color: var(--color-stroke);
    background: rgba(255, 255, 255, 0.02);
}
.btn--icon::before,
.btn--icon::after { content: none; }
.btn--icon:hover {
    opacity: 1;
    border-color: var(--color-stroke-2);
}

/* --------------------------------------------------------------------------
   Card
   2px corners · hairline border · radial cyan-tint background.
   `.card--inv` flips the radial for adjacent-card rhythm.
   -------------------------------------------------------------------------- */
.card {
    position: relative;
    padding: var(--sp-7);
    color: var(--color-fg);
    background-color: var(--color-bg-2);
    background-image: var(--grad-card-glow);
    border: var(--hairline);
    border-radius: var(--radius-sharp);
    transition: border-color var(--dur-hover) var(--ease);
}
.card:hover { border-color: var(--color-stroke-2); }

.card--inv      { background-image: var(--grad-card-glow-inv); }
.card--flat     { background-image: none; background-color: var(--color-bg-2); }
.card--accent   { border-color: rgba(64, 209, 253, 0.40); }

.card__title {
    margin: 0 0 var(--sp-3);
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 24px;
    line-height: 1.2;
    color: var(--color-fg);
    letter-spacing: 0;
}

.card__body {
    margin: 0;
    color: var(--color-fg-2);
    font-size: 16px;
    line-height: 1.55;
}

/* --------------------------------------------------------------------------
   Pill (status chip — small inline indicator)
   `live` (green dot · 12% bg · 40% border), `cyan`, `mute`.
   -------------------------------------------------------------------------- */
.pill {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 5px 11px;
    font-family: var(--font-mono);
    font-weight: 400;
    font-size: 11px;
    line-height: 1;
    letter-spacing: 0.06em;
    text-transform: lowercase;
    color: var(--color-fg-2);
    background: rgba(255, 255, 255, 0.04);
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-pill);
    white-space: nowrap;
}
/* Author `display: inline-flex` otherwise overrides the UA [hidden]
   rule (same failure mode as .task__stoncat) — e.g. "view passenger"
   stayed visible before completion. */
.pill[hidden] {
    display: none !important;
}

.pill--live {
    color: var(--color-success);
    background: rgba(4, 218, 141, 0.12);
    border-color: rgba(4, 218, 141, 0.40);
}
.pill--live::before {
    content: "";
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--color-success);
    box-shadow: 0 0 8px rgba(4, 218, 141, 0.85);
    animation: pill-pulse 2.4s ease-in-out infinite;
}
@keyframes pill-pulse {
    0%, 100% { opacity: 1; transform: scale(1); }
    50%      { opacity: 0.6; transform: scale(0.85); }
}

.pill--cyan {
    color: var(--color-cyan);
    background: rgba(64, 209, 253, 0.10);
    border-color: rgba(64, 209, 253, 0.40);
}

.pill--mute {
    color: var(--color-fg-3);
    background: rgba(255, 255, 255, 0.02);
    border-color: var(--color-stroke);
}

/* --------------------------------------------------------------------------
   Interactive pill — for tile-level affordances like "enter code", "verify",
   "open ↗", "recheck". Stays inside the .pill family visually (small chip,
   mono lowercase) but reads as actionable thanks to the cyan border tint
   and hover lift.

   The chip is small enough to fit alongside the existing `done` / `locked`
   pills in .task__meta without crowding the reward stamp on the right.

   Used by [data-task-verify-open] buttons to open the reusable
   task-verify modal in code-entry mode. Other patterns (external_visit
   verify, recheck, etc.) layer their own copy on the same .pill--action
   base.
   -------------------------------------------------------------------------- */
.pill--action {
    color: var(--color-cyan);
    background: rgba(64, 209, 253, 0.06);
    border-color: rgba(64, 209, 253, 0.40);
    cursor: pointer;
    appearance: none;
    -webkit-appearance: none;
    font: inherit;
    font-family: var(--font-mono);
    font-weight: 400;
    font-size: 11px;
    line-height: 1;
    letter-spacing: 0.06em;
    text-transform: lowercase;
    transition:
        color        var(--dur-hover) var(--ease),
        background   var(--dur-hover) var(--ease),
        border-color var(--dur-hover) var(--ease),
        transform    var(--dur-hover) var(--ease);
}
.pill--action:hover {
    color: var(--color-cyan);
    background: rgba(64, 209, 253, 0.12);
    border-color: rgba(64, 209, 253, 0.65);
    transform: translateY(-1px);
}
.pill--action:focus-visible {
    outline: none;
    border-color: var(--color-cyan);
    box-shadow: 0 0 0 2px rgba(64, 209, 253, 0.18);
}
/* Busy state — the verifier is in flight. The .pill--action becomes
   non-interactive and shows a small pulsing dot prefix to read as
   "working on it". Driven by [aria-busy="true"] so screen-reader UA
   already picks up the semantic. */
.pill--action[aria-busy="true"] {
    cursor: progress;
    color: var(--color-fg-3);
    background: rgba(255, 255, 255, 0.03);
    border-color: var(--color-stroke);
    transform: none;
}
.pill--action[aria-busy="true"]:hover {
    color: var(--color-fg-3);
    background: rgba(255, 255, 255, 0.03);
    border-color: var(--color-stroke);
    transform: none;
}
.pill--action[aria-busy="true"]::before {
    content: "";
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--color-cyan);
    box-shadow: 0 0 6px rgba(64, 209, 253, 0.65);
    animation: pill-pulse 1.0s ease-in-out infinite;
}
/* Cooldown — used by the recheck pill after a "still pending" response.
   Same disabled visual as busy but without the pulsing dot, since the
   pill text itself shows a countdown ("wait 9s"). Click-gated by the
   binder reading the same data-cooldown attribute. */
.pill--action[data-cooldown="1"] {
    cursor: not-allowed;
    color: var(--color-fg-4);
    background: rgba(255, 255, 255, 0.03);
    border-color: var(--color-stroke);
    transform: none;
    pointer-events: none;
}
.pill--action[data-cooldown="1"]:hover {
    color: var(--color-fg-4);
    background: rgba(255, 255, 255, 0.03);
    border-color: var(--color-stroke);
    transform: none;
}

/* --------------------------------------------------------------------------
   Chip — bigger interactive capsule (chain selector, filter)
   -------------------------------------------------------------------------- */
.chip {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 9px 18px;
    font-family: var(--font-mono);
    font-weight: 400;
    font-size: 14px;
    line-height: 1;
    text-transform: lowercase;
    color: var(--color-fg);
    background: rgba(255, 255, 255, 0.02);
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-pill);
    transition:
        border-color var(--dur-hover) var(--ease),
        color        var(--dur-hover) var(--ease);
}
.chip:hover {
    color: var(--color-fg);
    border-color: var(--color-stroke-2);
}
.chip::before {
    content: "";
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--color-cyan);
    box-shadow: 0 0 6px rgba(64, 209, 253, 0.6);
}

/* --------------------------------------------------------------------------
   Tag (square label inside cards — eg. blog category)
   -------------------------------------------------------------------------- */
.tag {
    display: inline-block;
    padding: 3px 10px;
    font-family: var(--font-mono);
    font-weight: 400;
    font-size: 11px;
    line-height: 1.4;
    letter-spacing: 0.06em;
    text-transform: lowercase;
    color: var(--color-fg-2);
    background: rgba(255, 255, 255, 0.04);
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-sharp);
}

/* --------------------------------------------------------------------------
   Field — text input with hairline + cyan focus halo
   53px tall · mono 14 · transparent bg · 2px corners.
   -------------------------------------------------------------------------- */
.field {
    display: block;
    margin-bottom: var(--sp-5);
}
.field:last-child { margin-bottom: 0; }

.field__label {
    display: block;
    margin-bottom: var(--sp-3);
    font-family: var(--font-mono);
    font-weight: 400;
    font-size: 14px;
    line-height: 1;
    color: var(--color-fg-2);
    text-transform: lowercase;
}
.field__required {
    margin-left: 4px;
    color: var(--color-cyan);
}
.field__optional {
    margin-left: 4px;
    color: var(--color-fg-4);
    font-weight: 400;
    text-transform: lowercase;
}

.field__input,
.field__textarea {
    display: block;
    width: 100%;
    height: 53px;
    padding: 0 16px;
    font-family: var(--font-mono);
    font-weight: 400;
    font-size: 14px;
    line-height: 1;
    color: var(--color-fg);
    background: rgba(255, 255, 255, 0.02);
    border: var(--hairline);
    border-radius: var(--radius-sharp);
    transition:
        border-color var(--dur-state) var(--ease),
        box-shadow   var(--dur-state) var(--ease),
        background   var(--dur-state) var(--ease);
    appearance: none;
    -webkit-appearance: none;
}
.field__textarea {
    height: auto;
    min-height: 120px;
    padding: 14px 16px;
    line-height: 1.5;
    resize: vertical;
}
.field__input::placeholder,
.field__textarea::placeholder {
    color: var(--color-fg-4);
    text-transform: lowercase;
}
.field__input:hover,
.field__textarea:hover {
    border-color: var(--color-stroke-2);
}
.field__input:focus,
.field__textarea:focus {
    outline: none;
    border-color: var(--color-cyan);
    background: rgba(64, 209, 253, 0.04);
    box-shadow: 0 0 0 3px rgba(64, 209, 253, 0.12);
}

.field__helper {
    margin: var(--sp-3) 0 0;
    font-family: var(--font-mono);
    font-weight: 400;
    font-size: 11px;
    line-height: 1.4;
    letter-spacing: 0.06em;
    color: var(--color-fg-4);
    text-transform: lowercase;
}

/* --------------------------------------------------------------------------
   Placeholder — generic stand-in for missing assets (logos, images, icons).
   Looks intentional rather than skeleton-grey.
   -------------------------------------------------------------------------- */
.placeholder {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: var(--sp-3) var(--sp-4);
    font-family: var(--font-mono);
    font-weight: 400;
    font-size: 11px;
    letter-spacing: 0.06em;
    text-transform: lowercase;
    color: var(--color-fg-4);
    background: rgba(255, 255, 255, 0.02);
    border: var(--hairline);
    border-radius: var(--radius-sharp);
    user-select: none;
}

.placeholder--logo {
    min-width: 130px;
    height: 44px;
    padding: 8px 14px;
}

.placeholder--icon {
    width: 18px;
    height: 18px;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--color-fg-3);
    font-size: 10px;
}

/* --------------------------------------------------------------------------
   Progress — thin status bar (boarding count, transaction status, etc.)
   2px corners · 5-stop blue→cyan→tan→yellow→orange fill · subtle track.

   The fill is driven by `--progress-fill` (unitless 0..100) on the
   element itself, e.g. `<span class="progress__fill"
   style="--progress-fill: 31.7"></span>`. Both width AND background-
   size are computed from that one value:

     width            = --progress-fill * 1%
     background-size  = (10000% / --progress-fill) ×  100%

   The background-size math expands the gradient's "100%" to match the
   FULL track width, regardless of how filled the bar is. Net effect:
   the gradient stops sit at fixed positions across the whole track, so
   a 31.7% bar shows only the blue→cyan slice, and the orange tip only
   appears as the bar approaches 100%. That's what makes the colour an
   independent progress signal on top of the bar length.

   The glow uses color-mix() to walk from cyan-blue (empty) to a warm
   orange (full) along with the leading edge — keyed off the same
   --progress-fill so the two cues stay in sync.

   max(…, 0.001) guards the divide-by-zero edge case at 0% fill (the
   bar is invisible at that point anyway, but the calc still has to
   produce a finite background-size).
   -------------------------------------------------------------------------- */
.progress {
    position: relative;
    width: 100%;
    height: 6px;
    background: rgba(255, 255, 255, 0.06);
    border-radius: var(--radius-sharp);
    overflow: hidden;
}
.progress__fill {
    height: 100%;
    width: calc(var(--progress-fill, 0) * 1%);
    background-image: var(--grad-progress);
    background-repeat: no-repeat;
    background-position: 0 0;
    background-size:
        calc(10000% / max(var(--progress-fill, 0.001), 0.001)) 100%;
    border-radius: var(--radius-sharp);
    box-shadow: 0 0 12px color-mix(
        in oklab,
        rgba(80, 167, 255, 0.45)
            calc((100 - var(--progress-fill, 0)) * 1%),
        rgba(208, 98, 51, 0.55));
    transition:
        width      var(--dur-cosmic) var(--ease),
        box-shadow var(--dur-cosmic) var(--ease);
}

/* --------------------------------------------------------------------------
   Disclaimer / fine print — mono 11, 30% white, always at the bottom.
   -------------------------------------------------------------------------- */
.disclaimer {
    margin: 0;
    font-family: var(--font-mono);
    font-weight: 400;
    font-size: 11px;
    line-height: 1.5;
    letter-spacing: 0.06em;
    color: var(--color-fg-4);
}

/* --------------------------------------------------------------------------
   Utility: gradient text (cyan→blue). Use sparingly on a single accent word.
   -------------------------------------------------------------------------- */
.text-gradient {
    background: linear-gradient(90deg,
        var(--color-cyan) 0%,
        var(--color-blue-bright) 80%);
    -webkit-background-clip: text;
            background-clip: text;
    -webkit-text-fill-color: transparent;
            color: transparent;
}

/* ==========================================================================
   Wallet hub — single pill that opens a panel with one section per chain
   ==========================================================================
   The hub is one DOM container with one trigger:
     .wallet-hub__pill                              — always visible
     [data-panel-open]                              — drops the panel below
     [data-wallet-section="ton|evm"][data-state]    — flips between the
       'idle' (picker entry) and 'connected' (account info) layouts
       within each section.
   The pill sits inside `.site-header__actions` so it inherits header
   z-index. The panel uses `--shadow-elev-1` and a hairline border
   consistent with cards on the page.
   -------------------------------------------------------------------------- */

.wallet-hub {
    position: relative;
    display: inline-flex;
    align-items: center;
}

/* --- Pill -----------------------------------------------------------------
   Reuses the secondary-button visual register so the header stays calm.
   The dot row signals which chains are connected (cyan = on, dim = off)
   without taking up horizontal space. The label morphs between
   "connect wallet" / a truncated address / "2 wallets" via JS. */
.wallet-hub__pill {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 10px;
    height: 38px;
    padding: 0 12px 0 10px;
    font-family: var(--font-mono);
    font-size: 13px;
    font-weight: 400;
    color: var(--color-fg);
    background: rgba(255, 255, 255, 0.02);
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-sharp);
    cursor: pointer;
    transition:
        border-color var(--dur-hover) var(--ease),
        background   var(--dur-hover) var(--ease),
        color        var(--dur-hover) var(--ease);
}
.wallet-hub__pill:hover {
    border-color: var(--color-stroke-2);
    background: rgba(255, 255, 255, 0.04);
}
.wallet-hub[data-panel-open] .wallet-hub__pill {
    color: var(--color-cyan);
    border-color: rgba(64, 209, 253, 0.45);
    background: rgba(64, 209, 253, 0.06);
}

/* Wallet glyph — hidden on desktop (the label carries the meaning);
   shown inside the 40px square trigger below 960px. */
.wallet-hub__icon {
    display: none;
    width: 18px;
    height: 18px;
    flex: 0 0 auto;
}

.wallet-hub__dots {
    display: inline-flex;
    gap: 3px;
}
.wallet-hub__dot {
    width: 4px;
    height: 4px;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.18);
    transition: background var(--dur-hover) var(--ease),
                box-shadow var(--dur-hover) var(--ease);
}
.wallet-hub__dot.is-on {
    background: var(--color-cyan);
    box-shadow: 0 0 6px rgba(64, 209, 253, 0.6);
}

.wallet-hub__pill-label {
    /* Stable width across all label states — "connect wallet" (idle, 14
       mono chars) is the longest; addresses ("UQ…XX4e" / "0x…aB7d", 11
       chars) and "2 wallets" (9 chars) sit centred inside the same slot.
       Keeps the pill, the chevron, and the panel anchor from jittering
       as state transitions during page-load. */
    min-width: 14ch;
    text-align: center;
    letter-spacing: 0.02em;
}

.wallet-hub__chevron {
    width: 10px;
    height: 10px;
    color: var(--color-fg-3);
    transition: transform var(--dur-hover) var(--ease);
}
.wallet-hub[data-panel-open] .wallet-hub__chevron {
    transform: rotate(180deg);
    color: var(--color-fg);
}

/* --- Drop-down panel ------------------------------------------------------
   Hidden by default; shown when the hub host carries [data-panel-open],
   which the pill toggles. */
.wallet-hub__panel {
    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    z-index: 40;
    width: 320px;
    padding: 14px;
    background: var(--color-bg-2);
    background-image: var(--grad-card-glow);
    /* Cyan border + blue glow makes the floating panel pop against the
       page when it slides in. Uses the same accent tokens as .btn--primary
       (--shadow-glow-blue) so the dropdown reads as part of the brand
       blue language rather than a generic surface. */
    border: 1px solid var(--color-cyan);
    border-radius: var(--radius-sharp);
    box-shadow:
        var(--shadow-elev-1),
        var(--shadow-glow-blue);
    display: flex;
    flex-direction: column;
    gap: 14px;
}
.wallet-hub:not([data-panel-open]) > [data-wallet-panel] { display: none; }

.wallet-hub__guide {
    margin: 0;
    padding-top: 10px;
    border-top: 1px solid rgba(255, 255, 255, 0.06);
}
a.wallet-hub__link {
    display: inline;
}

/* --- Section ---------------------------------------------------------------
   One per chain. Flips via [data-state]:
     idle       → connect button (picker entry)
     connected  → account (name + addr + actions)
     locked     → muted row (EVM/TRON before TON + Telegram pair)
   All layouts are in the DOM; CSS hides the ones that don't match. */
.wallet-hub__section {
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.wallet-hub__section[data-state="idle"]      [data-wallet-section-show="connected"],
.wallet-hub__section[data-state="idle"]      [data-wallet-section-show="locked"]    { display: none; }
.wallet-hub__section[data-state="connected"] [data-wallet-section-show="idle"],
.wallet-hub__section[data-state="connected"] [data-wallet-section-show="locked"]    { display: none; }
.wallet-hub__section[data-state="locked"]    [data-wallet-section-show="idle"],
.wallet-hub__section[data-state="locked"]    [data-wallet-section-show="connected"] { display: none; }

.wallet-hub__panel-eyebrow {
    margin: 0;
    font-family: var(--font-mono);
    font-size: 11px;
    line-height: 1;
    letter-spacing: 0.06em;
    text-transform: lowercase;
    color: var(--color-fg-4);
}
.wallet-hub__panel-eyebrow::before { content: ""; }

.wallet-hub__option {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px;
    text-align: left;
    color: var(--color-fg);
    background: rgba(255, 255, 255, 0.02);
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-sharp);
    cursor: pointer;
    transition:
        border-color var(--dur-hover) var(--ease),
        background   var(--dur-hover) var(--ease);
}
.wallet-hub__option:hover {
    border-color: rgba(64, 209, 253, 0.35);
    background: rgba(64, 209, 253, 0.04);
}
.wallet-hub__option[hidden] { display: none !important; }
.wallet-hub__option--locked {
    cursor: default;
    opacity: 0.55;
    pointer-events: none;
}
.wallet-hub__option--locked:hover {
    border-color: var(--color-stroke);
    background: rgba(255, 255, 255, 0.02);
}
.wallet-hub__option--locked .wallet-hub__option-glyph {
    color: var(--color-fg-3);
    background: rgba(255, 255, 255, 0.03);
    border-color: var(--color-stroke);
}
.wallet-hub__option--locked .wallet-hub__option-title {
    color: var(--color-fg-3);
}
.wallet-hub__option--menu {
    cursor: default;
}
.wallet-hub__option--menu:hover {
    border-color: var(--color-stroke);
    background: rgba(255, 255, 255, 0.02);
}

.wallet-hub__option-glyph {
    flex: 0 0 auto;
    width: 36px;
    height: 36px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--color-cyan);
    background: rgba(64, 209, 253, 0.08);
    border: 1px solid rgba(64, 209, 253, 0.30);
    border-radius: var(--radius-pill);
}
.wallet-hub__option-icon {
    display: block;
    width: 18px;
    height: 18px;
    background-color: currentColor;
    -webkit-mask: var(--wallet-icon) center / contain no-repeat;
            mask: var(--wallet-icon) center / contain no-repeat;
}

.wallet-hub__option-body {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
}
.wallet-hub__option-title {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 14px;
    line-height: 1.2;
    color: var(--color-fg);
}
/* Nowrap/ellipsis truncation is scoped to CHOOSER PROVIDER rows so
   the static hub rows (Connect TON wallet, Connect EVM wallet, etc.)
   remain free to wrap gracefully if localization ever produces a
   longer label. Dynamic EIP-6963 provider rows (`.wallet-hub__provider`
   descendants inside `[data-evm-chooser]`) still get the overflow
   guard for hostile/unbroken announced wallet names —
   normalizeEip6963Provider() already truncates to 40 chars but an
   unspaced 40-char string could push the mobile fixed panel wider
   than the viewport (390px – gutters). */
.wallet-hub__provider .wallet-hub__option-title {
    min-width: 0;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.wallet-hub__option-meta {
    font-family: var(--font-mono);
    font-size: 11px;
    line-height: 1.3;
    letter-spacing: 0.04em;
    color: var(--color-fg-3);
    text-transform: lowercase;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* --- Account panel rows --------------------------------------------------- */
.wallet-hub__account {
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: 12px;
    background: rgba(255, 255, 255, 0.02);
    border: var(--hairline);
    border-radius: var(--radius-sharp);
}

.wallet-hub__account-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
}

.wallet-hub__account-name {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 14px;
    line-height: 1.2;
    color: var(--color-fg);
}

.wallet-hub__account-addr {
    font-family: var(--font-mono);
    font-size: 12px;
    line-height: 1;
    letter-spacing: 0.02em;
    color: var(--color-fg-2);
    background: transparent;
    padding: 0;
}

.wallet-hub__link {
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.04em;
    text-transform: lowercase;
    color: var(--color-fg-3);
    background: transparent;
    border: 0;
    padding: 0;
    cursor: pointer;
    transition: color var(--dur-hover) var(--ease);
}
.wallet-hub__link:hover { color: var(--color-cyan); }
.wallet-hub__link[hidden] { display: none; }

.wallet-hub__option-links {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
}
.wallet-hub__option-links .wallet-hub__link {
    white-space: nowrap;
}

/* --- EVM provider chooser ------------------------------------------------
   A compact nested manifest: detected extensions first, QR transport last. */
.wallet-hub__chooser {
    display: flex;
    flex-direction: column;
    gap: 8px;
    min-width: 0;
}
.wallet-hub__chooser[hidden] { display: none; }

.wallet-hub__chooser-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
}

.wallet-hub__chooser-label,
.wallet-hub__provider-status {
    font-family: var(--font-mono);
    font-size: 10px;
    line-height: 1.3;
    letter-spacing: 0.06em;
    text-transform: lowercase;
}
.wallet-hub__chooser-label { color: var(--color-fg-4); }

.wallet-hub__provider-list {
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-width: 0;
}
.wallet-hub__provider {
    width: 100%;
    min-width: 0;
}
/* Provider icon renders in one of two shapes and we want both to
   read as the same "chip in the glyph slot":
     - <img class="wallet-hub__provider-icon"> when EIP-6963 announced
       a validated data: URI. Renders in FULL COLOR — the wallet's
       actual logo. `object-fit: contain` keeps aspect for landscape
       marks; `display: block` kills the img-baseline gap.
     - <span class="wallet-hub__option-icon wallet-hub__provider-icon">
       when no safe icon was announced. Reuses the mask machinery
       from .wallet-hub__option-icon (currentColor tint on the local
       fallback SVG), so it inherits its color from the glyph parent.
   Both share width/height/radius so the glyph slot is visually stable. */
.wallet-hub__provider-icon {
    width: 24px;
    height: 24px;
    border-radius: 7px;
}
img.wallet-hub__provider-icon {
    display: block;
    object-fit: contain;
}
.wallet-hub__provider-status {
    margin-left: auto;
    color: var(--color-cyan);
}

.wallet-hub__option:focus-visible,
.wallet-hub__link:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px rgba(64, 209, 253, 0.40);
}

@media (max-width: 959px) {
    /* Square trigger matching .profile-hub__trigger / .hamburger so
       the header chrome is three equal icon buttons. Label + chevron
       hide; the wallet glyph and the TON/EVM status dots remain. */
    .wallet-hub__pill {
        width: 40px;
        height: 40px;
        padding: 0;
        justify-content: center;
        gap: 0;
    }
    .wallet-hub__icon { display: block; }
    .wallet-hub__pill-label,
    .wallet-hub__chevron { display: none; }
    .wallet-hub__dots {
        position: absolute;
        top: 5px;
        right: 5px;
        gap: 2px;
    }

    /* Same WebKit backdrop-filter containing-block trap as
       .profile-panel (see that comment): `position: fixed` here is
       against the header inner, not the viewport, so we anchor `top`
       and size with max-height / dvh instead of `bottom`. */
    .wallet-hub__panel {
        position: fixed;
        top: calc(var(--header-offset) + var(--header-h) + 8px);
        right: 16px;
        left: 16px;
        width: auto;
        max-height: calc(100vh - var(--header-offset) - var(--header-h) - 24px);
        max-height: calc(100dvh - var(--header-offset) - var(--header-h) - 24px);
        overflow-y: auto;
        overscroll-behavior: contain;
    }
}

/* ==========================================================================
   Wallet card — connected-state inline card used inside the form steps
   ==========================================================================
   Same visual register as `.wallet-hub__account`, but stretches the full
   width of the step card and pads at button-row size so it visually
   replaces the secondary button it succeeds.
   -------------------------------------------------------------------------- */

.wallet-card {
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 14px 18px;
    background:
        linear-gradient(135deg,
            rgba(64, 209, 253, 0.06) 0%,
            rgba(36, 141, 255, 0.02) 100%),
        var(--color-bg);
    border: 1px solid rgba(64, 209, 253, 0.30);
    border-radius: var(--radius-sharp);
}

.wallet-card__dot {
    flex: 0 0 auto;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: var(--color-success);
    box-shadow: 0 0 8px rgba(4, 218, 141, 0.55);
}

.wallet-card__body {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
    flex: 1 1 auto;
}

.wallet-card__name {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 15px;
    line-height: 1.2;
    color: var(--color-fg);
}

.wallet-card__addr {
    font-family: var(--font-mono);
    font-size: 12px;
    line-height: 1;
    letter-spacing: 0.02em;
    color: var(--color-fg-2);
    background: transparent;
    padding: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.wallet-card__actions {
    display: inline-flex;
    align-items: center;
    gap: 14px;
    flex: 0 0 auto;
}

@media (max-width: 540px) {
    /* Keep copy/disconnect in a compact column on the right — wrapping
       them onto a full-width row made the TON card much taller than
       the Telegram paired card (which has no actions). */
    .wallet-card__actions {
        flex-direction: column;
        align-items: flex-end;
        gap: 6px;
    }
}

/* ==========================================================================
   Profile hub — passenger account pop-up
   ==========================================================================
   The profile hub lives in `.site-header__actions` next to the wallet hub
   and acts as the user's main campaign account view (no separate /dashboard
   page). The trigger is a 38px square icon button matching the wallet
   pill's height; the panel anchors bottom-right under the trigger and
   pins to the bottom of the viewport on phones, so neither the trigger
   row nor the panel jitters as state changes during page-load.

   Visual register: a stack of "boarding pass stubs" separated by a
   .profile-stub__perf perforation strip — extends the campaign's
   aviation language without becoming a literal ticket. Mileage cells
   echo the cyan→blue glow used by the seat counter and progress bars
   so the panel reads as a member of the same family of "filled lines"
   used across the page.
   -------------------------------------------------------------------------- */

.profile-hub {
    position: relative;
    display: inline-flex;
    align-items: center;
}

/* --- Trigger ------------------------------------------------------------ */
.profile-hub__trigger {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    padding: 0;
    color: var(--color-fg);
    background: rgba(255, 255, 255, 0.02);
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-sharp);
    cursor: pointer;
    transition:
        border-color var(--dur-hover) var(--ease),
        background   var(--dur-hover) var(--ease),
        color        var(--dur-hover) var(--ease);
}
@media (max-width: 959px) {
    .profile-hub__trigger {
        width: 40px;
        height: 40px;
    }
}
.profile-hub__trigger:hover {
    color: var(--color-cyan);
    border-color: var(--color-stroke-2);
    background: rgba(64, 209, 253, 0.04);
}
.profile-hub__trigger svg {
    width: 18px;
    height: 18px;
}

/* Status dot — sits at the upper-right of the trigger and signals how
   "wired up" the passenger is. The dot uses three states, computed from
   two attributes on .profile-hub:
     · [data-ton-state]  = "idle" | "connected"  (TON wallet — set by JS,
                           server-renders the best guess from $profile)
     · [data-tg-state]   = "unpaired" | "paired" (Telegram pairing — set
                           by JS observing the waitlist form's telegram
                           step, falls back to the server-rendered value)

   The three resulting visuals:
     · RED   — nothing connected (no TON, no TG). The default visual the
               first-time visitor sees. Reads as "you haven't done the
               required steps yet — open the panel to start."
     · CYAN  — partial (one of TON/TG connected, the other not). Reads
               as "in progress — finish the other step." This is also
               the visual for any state that isn't explicitly red/green.
     · GREEN — both connected (TON wallet AND Telegram paired). Reads
               as "you're fully on the manifest — nothing required."

   See main.js bindProfileWalletSync() for the JS that flips the two
   attributes once STONFI_WALLET hydrates and the form step transitions. */
.profile-hub__trigger-dot {
    position: absolute;
    top: 5px;
    right: 5px;
    width: 4px;
    height: 4px;
    border-radius: 50%;
    /* Cyan default = the "partial / in-progress" visual; red and green
       are explicit overrides for the two extremes below. */
    background: var(--color-cyan);
    box-shadow: 0 0 6px rgba(64, 209, 253, 0.65);
    transition:
        background var(--dur-hover) var(--ease),
        box-shadow var(--dur-hover) var(--ease);
}
.profile-hub[data-ton-state="idle"][data-tg-state="unpaired"]
    .profile-hub__trigger-dot {
    background: var(--color-danger);
    box-shadow: 0 0 6px rgba(232, 76, 76, 0.60);
}
.profile-hub[data-ton-state="connected"][data-tg-state="paired"]
    .profile-hub__trigger-dot {
    background: var(--color-success);
    box-shadow: 0 0 6px rgba(4, 218, 141, 0.55);
}
.profile-hub[data-panel-open] .profile-hub__trigger {
    color: var(--color-cyan);
    border-color: rgba(64, 209, 253, 0.45);
    background: rgba(64, 209, 253, 0.06);
}

/* --- Panel ------------------------------------------------------------- */
.profile-panel {
    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    z-index: 50;          /* above wallet hub (40) so toggling between */
    width: 380px;         /* siblings doesn't fight z-stacking          */
    /* No bottom padding: the bottom-fade overlay (.profile-panel::after
       below) lives inside this scroll container and is clipped at its
       padding edge. Keeping the standard 18px bottom padding would
       leave the fade ending 18px above the panel's true bottom border
       — a visible gap. Setting bottom padding to 0 lets the sticky
       fade reach the actual border. The 18px of breathing room that
       the bottom padding used to provide is moved to the last stub
       via the rule below. */
    padding: 18px 18px 0;
    background: var(--color-bg-2);
    background-image:
        radial-gradient(120% 80% at 50% 0%,
            rgba(64, 209, 253, 0.06),
            transparent 70%),
        var(--grad-card-glow);
    /* Cyan border + blue glow makes the panel pop the moment it slides
       in — including when another page opens it without a click on
       the avatar. Matches .wallet-hub__panel and .btn--primary's
       --shadow-glow-blue so the surface reads as part of the brand
       accent language. The earlier 0 0 0 1px 0.04 inset ring is dropped:
       it was compensating for the muted hairline border and is redundant
       once the border itself is cyan. */
    border: 1px solid var(--color-cyan);
    border-radius: var(--radius-sharp);
    /* 1px outer ring: overflow-y:auto + the sticky bottom fade paint
       over the border on iOS, so the bottom edge vanished. Outer
       box-shadow is not clipped by this element's overflow. */
    box-shadow:
        0 0 0 1px var(--color-cyan),
        var(--shadow-elev-1),
        var(--shadow-glow-blue);
    display: flex;
    flex-direction: column;
    gap: 14px;
    /* Reveal animation — slides + fades in from above with a touch of
       perspective. Reduced-motion users skip it via base.css. */
    animation: profile-panel-in var(--dur-panel) var(--ease) forwards;
    transform-origin: top right;
    /* Clamp to the viewport: header + its top offset eat the room
       above, the calc tail leaves an 8px gap to the trigger and 16px
       breathing room at the bottom so the last stub doesn't kiss the
       viewport edge. When content exceeds this, the panel scrolls
       internally — see the body[data-overlay-open] rule below for the
       page-scroll lock that pairs with this. */
    max-height: calc(100vh - var(--header-h) - var(--header-offset) - 24px);
    overflow-y: auto;
    /* No horizontal scroll inside the panel: the layout is column-only
       and any rogue overflow (e.g. an ultra-long unbroken EVM address)
       should be clipped or wrapped, not produce a scrollbar that maps
       to nothing useful. */
    overflow-x: hidden;
    /* Don't bubble scroll out to the page when the user reaches the
       top/bottom of the panel — both prevents the locked page from
       wiggling and disables iOS rubber-band on the page underneath. */
    overscroll-behavior: contain;
}

/* Compensate for the missing bottom padding on .profile-panel — give
   the last visible stub the same 18px of breathing room from the
   panel border that it used to get from the panel's own padding.
   Pseudo elements (the fade ::after below) don't count for
   :last-child, so this targets the actual last DOM child.

   The tabpanel-specific rule beneath is load-bearing: with tabs in
   the DOM, the :last-child of .profile-panel is the claims tabpanel,
   which is `hidden` whenever Overview is active. A hidden element
   accepts margin but contributes zero layout space, so the
   visible-but-not-last overview tabpanel would have no breathing room.
   Applying margin-bottom to the active (non-hidden) tabpanel puts the
   18px where it actually paints. The legacy .profile-panel > :last-child
   rule above is kept as a safety net for any future layout that drops
   the tabs (e.g. unpaired-only collapsed view). */
.profile-panel > :last-child { margin-bottom: 18px; }
.profile-tabpanel:not([hidden]) { margin-bottom: 18px; }

/* Bottom fade-out — visual hint that the panel can be scrolled.
   Sticky bottom: 0 pins this to the visible bottom of the
   scrollport. With the panel's bottom padding zeroed out (above),
   the sticky's "bottom: 0" coincides with the panel's true border —
   so the fade reads as content dissolving INTO the panel border,
   no gap.

   Painted as an overlay on top of the preceding stubs (z-index: 1,
   pointer-events: none) so it actually obscures the text scrolling
   underneath — a layered background-image alone wouldn't do this,
   because backgrounds paint behind content.

   Negative margins do two jobs:
     · margin-top: -46px   — cancels the pseudo's own height (32px)
                             plus the flex gap (14px), so it adds
                             zero layout space to the stub stack.
     · margin: ... -18px ... — extends edge-to-edge across the
                             panel's horizontal padding so the
                             gradient reaches the side borders.

   JS toggles [data-scroll-more] on the panel when content extends
   below the visible area; without the attribute the fade stays at
   opacity 0 (so a panel that fits in one screen shows no hint). */
.profile-panel::after {
    content: "";
    flex: 0 0 auto;
    align-self: stretch;
    position: sticky;
    bottom: 1px; /* leave the cyan bottom border uncovered */
    height: 32px;
    margin: -46px -18px 0;
    background: linear-gradient(to bottom,
        rgba(21, 21, 23, 0) 0%,
        var(--color-bg-2) 100%);
    pointer-events: none;
    z-index: 1;
    opacity: 0;
    transition: opacity 180ms var(--ease, ease-out);
}
.profile-panel[data-scroll-more]::after { opacity: 1; }

.profile-hub:not([data-panel-open]) > [data-profile-panel] { display: none; }

@keyframes profile-panel-in {
    from { opacity: 0; transform: translateY(-6px) scale(0.985); }
    to   { opacity: 1; transform: translateY(0)     scale(1); }
}

/* Boarding-stub corner notches — purely decorative pinpricks at the top
   corners that pin the boarding-pass language without making the panel
   literal. Sized to read as registered crop marks rather than dots. */
.profile-panel__notch {
    position: absolute;
    top: -3px;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--color-bg);
    border: 1px solid var(--color-stroke);
}
.profile-panel__notch--tl { left: 14px; }
.profile-panel__notch--tr { right: 14px; }

/* --- Stub wrapper ------------------------------------------------------- */
.profile-stub {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

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

/* Perforation strip — dotted line evoking a tear-here boarding pass
   division between stubs. CSS-only with two background-image radials so
   the dots stay crisp at any zoom and don't depend on a font. */
.profile-stub__perf {
    display: block;
    width: calc(100% + 36px);
    margin: 4px -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;
    /* Half-circles at each end suggest the punch-out, completing the
       perforation read. The mask carves them out of the background. */
    position: relative;
}
.profile-stub__perf::before,
.profile-stub__perf::after {
    content: "";
    position: absolute;
    top: -3px;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--color-bg);
    border: 1px solid var(--color-stroke);
}
.profile-stub__perf::before { left: -1px; }
.profile-stub__perf::after  { right: -1px; }

/* --- Profile stub visibility ------------------------------------------

   Head, class, miles, manifest, crew invite, and the Flight Deals tab
   stay visible. Telegram pairing used to CSS-gate that stack; the hold
   is lifted.

   The Connect Telegram CTA is still gated: `/api/pairing-mint` 401s
   without a TON session, and a paired passenger does not need it.

     · [data-tg-state]   = "unpaired" | "paired"
     · [data-ton-state]  = "idle"     | "connected"

   Both are server-rendered from $profile and kept live by
   bindProfileTgSync + bindProfileWalletSync in main.js.
   --------------------------------------------------------------------- */
.profile-hub:not([data-ton-state="connected"]) .profile-stub__cta,
.profile-hub[data-tg-state="paired"]          .profile-stub__cta {
    display: none;
}

/* --- Stub 1: passenger header ------------------------------------------
   Single-column block now: just the eyebrow + callsign. The flight class
   used to live in the right corner here, but it grew enough metadata
   (benefit + progress + tooltip) that it earned its own stub
   below — see .profile-class. Keeping this stub tiny keeps the panel's
   visual rhythm: small head → meaty class card → meaty miles board → list
   stubs (manifest, crew invite). */
.profile-stub--head {
    gap: 4px;
}

.profile-stub__eyebrow {
    margin: 0;
    font-family: var(--font-mono);
    font-weight: 400;
    font-size: 11px;
    line-height: 1;
    letter-spacing: 0.06em;
    text-transform: lowercase;
    color: var(--color-fg-3);
}
.profile-stub__eyebrow::before {
    content: "// ";
    color: var(--color-cyan);
}

.profile-stub__name {
    margin: 0;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 18px;
    line-height: 1.15;
    letter-spacing: 0;
    color: var(--color-fg);
    text-transform: lowercase;
}

/* --- Stub 2: flight class ----------------------------------------------
   Boarding-pass "class chip" card. Visual register intentionally mirrors
   .profile-stub--miles below it — same outlined-with-cyan-tint card on a
   subtle glass tint — so the panel reads as a paired set of "what you
   are" / "what you have" boards stacked above the manifest list.

   Internal structure:
     .profile-class__head        — eyebrow on the left, tooltip "i" on the right
     .profile-class__name-row    — class name
     .profile-class__benefit     — short one-liner from the JSON (optional)
     .profile-class__progress    — labelled progress bar to next class
                                   OR .profile-class__topped (top-tier note) */
.profile-class {
    position: relative;
    padding: 14px;
    background:
        linear-gradient(135deg,
            rgba(64, 209, 253, 0.05) 0%,
            rgba(36, 141, 255, 0.02) 100%),
        rgba(255, 255, 255, 0.02);
    border: 1px solid rgba(64, 209, 253, 0.20);
    border-radius: var(--radius-sharp);
    gap: 8px;
}

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

.profile-class__name-row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    flex-wrap: wrap;
}

.profile-class__name {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 22px;
    line-height: 1.05;
    letter-spacing: -0.2px;
    color: var(--color-cyan);
    text-transform: uppercase;
}

/* Per-class accent — only the top tier gets the warmer "captain" treatment
   to make the ladder visibly rewarding. Standby drops the cyan glow to
   read as quieter / "not earning yet". The middle tiers stay on the
   default cyan defined above. */
.profile-class__name[data-class="first"] {
    background: linear-gradient(180deg, #ffe2a4 0%, #f6c25b 70%, #d99c2e 100%);
    -webkit-background-clip: text;
            background-clip: text;
    -webkit-text-fill-color: transparent;
            color: transparent;
    text-shadow: 0 0 14px rgba(246, 194, 91, 0.25);
}
.profile-class__name[data-class="standby"] {
    color: var(--color-fg-2);
}

.profile-class__benefit {
    margin: 0;
    font-family: var(--font-body);
    font-size: 12px;
    line-height: 1.45;
    color: var(--color-fg-2);
}

.profile-class__progress {
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin-top: 2px;
}

.profile-class__progress-row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 8px;
}

.profile-class__progress-label {
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: 0.06em;
    text-transform: lowercase;
    color: var(--color-fg-3);
}

.profile-class__progress-pct {
    font-family: var(--font-mono);
    font-weight: 500;
    font-size: 11px;
    color: var(--color-fg);
    font-variant-numeric: tabular-nums;
}

/* Hairline track + --grad-progress fill. Same blue→orange "journey"
   recipe as .progress__fill / .page-loader__fill, just wearing the
   profile pop-up's pill geometry (999px corners). Driven by
   `--progress-fill` (unitless 0..100) so width AND background-size
   come from one value, anchoring the gradient stops to the FULL
   track width — at 12% the bar is blue, at 90% the leading edge
   warms to amber-orange. min-width: 4px keeps a visible nub at
   sub-1% values so the bar never looks broken. */
.profile-class__progress-track {
    position: relative;
    width: 100%;
    height: 6px;
    background: rgba(255, 255, 255, 0.05);
    border: 1px solid var(--color-stroke);
    border-radius: 999px;
    overflow: hidden;
}
.profile-class__progress-fill {
    --progress-fill: 0;
    height: 100%;
    width: calc(var(--progress-fill) * 1%);
    min-width: 4px;
    background-image: var(--grad-progress);
    background-repeat: no-repeat;
    background-position: 0 0;
    background-size:
        calc(10000% / max(var(--progress-fill), 0.001)) 100%;
    box-shadow: 0 0 8px color-mix(
        in oklab,
        rgba(80, 167, 255, 0.30)
            calc((100 - var(--progress-fill)) * 1%),
        rgba(208, 98, 51, 0.40));
    border-radius: 999px;
    transition:
        width      var(--dur-state) var(--ease),
        box-shadow var(--dur-state) var(--ease);
}

.profile-class__need {
    margin: 6px 0 0;
    padding-top: 8px;
    border-top: 1px dashed rgba(64, 209, 253, 0.22);
    display: flex;
    flex-direction: column;
    gap: 3px;
}
.profile-class__need-label {
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: 0.06em;
    text-transform: lowercase;
    color: var(--color-cyan);
}
.profile-class__need-label::before {
    content: "// ";
    color: var(--color-cyan);
    opacity: 0.7;
}
.profile-class__need-items {
    font-family: var(--font-body);
    font-size: 12px;
    line-height: 1.4;
    color: var(--color-fg-2);
}

.profile-class__topped {
    margin: 0;
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.06em;
    text-transform: lowercase;
    color: var(--color-cyan);
    text-align: center;
    padding: 6px 8px;
    background: rgba(64, 209, 253, 0.06);
    border: 1px dashed rgba(64, 209, 253, 0.40);
    border-radius: var(--radius-sharp);
}

@media (max-width: 480px) {
    .profile-class__name { font-size: 20px; }
}

/* --- Stub 2: mileage departure board ----------------------------------- */
.profile-stub--miles {
    position: relative;
    flex-direction: row;
    gap: 0;
    padding: 14px 12px;
    background:
        linear-gradient(135deg,
            rgba(64, 209, 253, 0.05) 0%,
            rgba(36, 141, 255, 0.02) 100%),
        rgba(255, 255, 255, 0.02);
    border: 1px solid rgba(64, 209, 253, 0.20);
    border-radius: var(--radius-sharp);
}

.profile-miles__cell {
    flex: 1 1 0;
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
    padding: 0 6px;
}

.profile-miles__label {
    font-family: var(--font-mono);
    font-size: 10px;
    line-height: 1.35;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--color-fg-3);
}

.profile-miles__value {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 28px;
    line-height: 1;
    letter-spacing: -0.5px;
    color: var(--color-fg);
    /* Tabular numerals so 1,420 / 12,000 align in the same column slot
       and the value doesn't twitch as the balance grows. */
    font-variant-numeric: tabular-nums;
}

.profile-miles__cell--accent .profile-miles__value {
    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;
    text-shadow: 0 0 12px rgba(80, 167, 255, 0.20);
}

.profile-miles__meta {
    font-family: var(--font-mono);
    font-size: 10px;
    line-height: 1.3;
    letter-spacing: 0.04em;
    text-transform: lowercase;
    color: var(--color-fg-3);
}

.profile-miles__divider {
    flex: 0 0 1px;
    align-self: stretch;
    background: linear-gradient(to bottom,
        transparent 0%,
        var(--color-stroke) 30%,
        var(--color-stroke) 70%,
        transparent 100%);
    margin: 0 6px;
}

/* --- Stub 3: manifest (connections) ------------------------------------ */
.profile-conn {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.profile-conn__row {
    /* Hosts the stretched-link overlay (.profile-conn__action below) — the
       overlay is position:absolute inset:0 so the entire row becomes one
       hit target without redoing the existing 3-col grid layout. */
    position: relative;
    display: grid;
    grid-template-columns: 36px 1fr auto;
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    background: rgba(255, 255, 255, 0.02);
    border: var(--hairline);
    border-radius: var(--radius-sharp);
    transition: border-color var(--dur-hover) var(--ease);
}
/* Idle rows are interactive (they deep-link to the place where the user
   can fix the gap, see header.php's manifest block + main.js bindHashLanding).
   Pointer + brightened cyan border on hover signal "click me"; we
   deliberately don't fade in a chevron icon because the product owner
   picked the minimal treatment in the redesign brief. */
.profile-conn__row[data-state="idle"]        { cursor: pointer; }
.profile-conn__row[data-state="idle"]:hover  { border-color: rgba(64, 209, 253, 0.55); }
.profile-conn__row[data-state="connected"] {
    border-color: rgba(4, 218, 141, 0.32);
    background:
        linear-gradient(90deg,
            rgba(4, 218, 141, 0.04) 0%,
            transparent 60%),
        rgba(255, 255, 255, 0.02);
}

/* Stretched-link overlay. The anchor itself carries no visible content —
   the visible chip / value / dot are siblings and remain readable through
   it (the overlay is transparent). data-profile-show="idle" on the anchor
   pairs with the existing show/hide rule below, so the link collapses to
   display:none — and is therefore inert — the moment the row flips to
   "connected". border-radius matches the row so the focus ring lines up
   cleanly with the row's rounded corners. */
.profile-conn__action {
    position: absolute;
    inset: 0;
    z-index: 1;
    border-radius: var(--radius-sharp);
    text-decoration: none;
}
.profile-conn__action:focus-visible {
    outline: 2px solid var(--color-cyan);
    outline-offset: 2px;
}

.profile-conn__chip {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 22px;
    font-family: var(--font-mono);
    font-weight: 500;
    font-size: 10px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    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);
}
.profile-conn__row[data-state="connected"] .profile-conn__chip {
    color: var(--color-success);
    background: rgba(4, 218, 141, 0.06);
    border-color: rgba(4, 218, 141, 0.30);
}

.profile-conn__body {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.profile-conn__value {
    font-family: var(--font-mono);
    font-size: 12px;
    line-height: 1.2;
    letter-spacing: 0.02em;
    color: var(--color-fg);
    background: transparent;
    padding: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.profile-conn__value--idle {
    color: var(--color-fg-3);
    text-transform: lowercase;
}

.profile-conn__row[data-state="idle"]      [data-profile-show="connected"] { display: none; }
.profile-conn__row[data-state="connected"] [data-profile-show="idle"]      { display: none; }

.profile-conn__dot {
    flex: 0 0 auto;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.18);
    transition:
        background var(--dur-hover) var(--ease),
        box-shadow var(--dur-hover) var(--ease);
}
.profile-conn__row[data-state="connected"] .profile-conn__dot {
    background: var(--color-success);
    box-shadow: 0 0 6px rgba(4, 218, 141, 0.55);
}

.profile-stub__cta {
    margin-top: 6px;
}

/* --- Stub 4: crew invite (referral) ------------------------------------ */
.profile-ref {
    display: flex;
    align-items: stretch;
    gap: 6px;
}

.profile-ref__link {
    flex: 1 1 auto;
    min-width: 0; /* flex item must shrink or overflow-x never engages */
    display: block;
    padding: 0 12px;
    height: 36px;
    line-height: 36px;
    font-family: var(--font-mono);
    font-size: 12px;
    letter-spacing: 0.02em;
    color: var(--color-fg);
    background: rgba(64, 209, 253, 0.04);
    border: 1px solid rgba(64, 209, 253, 0.22);
    border-radius: var(--radius-sharp);
    overflow-x: auto;
    overflow-y: hidden;
    white-space: nowrap;
    direction: ltr;
    text-align: right;
    touch-action: pan-x pan-y;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
    scrollbar-width: thin;
}

/* Icon-only square button — sized to match .profile-ref__link's 36px
   height so it reads as a paired field+action pill rather than two
   separate controls. The button hosts two stacked SVGs (copy + check)
   and toggles which one is visible via [data-copied]; both glyphs sit
   in the same grid cell so the morph happens in place without the
   button width jittering on click. */
.profile-ref__copy {
    flex: 0 0 auto;
    position: relative;
    display: grid;
    place-items: center;
    width: 36px;
    height: 36px;
    padding: 0;
    color: var(--color-fg-2);
    background: rgba(255, 255, 255, 0.02);
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-sharp);
    cursor: pointer;
    transition:
        border-color var(--dur-hover) var(--ease),
        color        var(--dur-hover) var(--ease),
        background   var(--dur-hover) var(--ease);
}
.profile-ref__copy:hover {
    color: var(--color-cyan);
    border-color: rgba(64, 209, 253, 0.45);
    background: rgba(64, 209, 253, 0.06);
}
.profile-ref__copy:focus-visible {
    outline: none;
    border-color: var(--color-cyan);
    box-shadow: 0 0 0 2px rgba(64, 209, 253, 0.18);
}

.profile-ref__copy-icon {
    /* Stack both glyphs in the same cell so the swap happens without
       a width re-layout. Crossfade is short and snappy — the visible
       success state is what the user is reading, not the transition. */
    grid-area: 1 / 1;
    width: 14px;
    height: 14px;
    transition:
        opacity   var(--dur-state) var(--ease),
        transform var(--dur-state) var(--ease);
}
.profile-ref__copy-icon--check {
    opacity: 0;
    transform: scale(0.7);
}

.profile-ref__copy[data-copied="true"] {
    color: var(--color-success);
    border-color: rgba(4, 218, 141, 0.45);
    background: rgba(4, 218, 141, 0.06);
}
.profile-ref__copy[data-copied="true"] .profile-ref__copy-icon--copy {
    opacity: 0;
    transform: scale(0.7);
}
.profile-ref__copy[data-copied="true"] .profile-ref__copy-icon--check {
    opacity: 1;
    transform: scale(1);
}

/* Screen-reader-only utility — visually hides the copy/copied label
   while keeping it in the accessibility tree. The button itself is
   announced via aria-label, but the live label flip is what conveys
   success state to assistive tech. */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

.profile-ref__stats {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 8px;
}

.profile-ref__stats li {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 10px 12px;
    background: rgba(255, 255, 255, 0.02);
    border: var(--hairline);
    border-radius: var(--radius-sharp);
}

.profile-ref__stat-value {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 20px;
    line-height: 1;
    color: var(--color-fg);
    font-variant-numeric: tabular-nums;
}

.profile-ref__stat-label {
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--color-fg-3);
}

/* ==========================================================================
   Profile pop-up — Overview / My Flight Deals tabs (Edit 1)
   ==========================================================================
   The pop-up has two tabs: Overview (the existing stub stack of flight
   class / miles / manifest / crew invite) and My Flight Deals (per-user
   record of every successful Flight Deal claim). The passenger header
   stays outside the tabs since it identifies whoever is viewing both.

   Tab state lives on the panel root as data-profile-tab="overview|claims";
   .profile-tabpanel uses the native `hidden` attribute (= display: none)
   for the inactive tab so there's no extra CSS rule to maintain. */
.profile-tabs {
    display: flex;
    gap: 4px;
    padding: 4px;
    background: rgba(255, 255, 255, 0.025);
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-sharp);
}

.profile-tabs__btn {
    flex: 1 1 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    height: 30px;
    padding: 0 12px;
    font-family: var(--font-mono);
    font-weight: 500;
    font-size: 11px;
    letter-spacing: 0.04em;
    text-transform: lowercase;
    color: var(--color-fg-3);
    background: transparent;
    border: 0;
    border-radius: calc(var(--radius-sharp) - 2px);
    cursor: pointer;
    transition:
        color      var(--dur-hover) var(--ease),
        background var(--dur-hover) var(--ease);
}
.profile-tabs__btn:hover {
    color: var(--color-fg-2);
    background: rgba(255, 255, 255, 0.025);
}
/* Active state — solid cyan→blue gradient is the visual "selected"
   indicator on its own, so no extra underline / inset shadow is
   needed (the previous design layered both; the gradient alone reads
   stronger). Text flips to --color-bg (near-black) because pure-
   white would vanish against the light right end of the gradient
   (#9EDFFF). */
.profile-tabs__btn[aria-selected="true"] {
    color: var(--color-bg);
    background: var(--grad-cta);
}
.profile-tabs__btn:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px rgba(64, 209, 253, 0.40);
}

/* Claim count badge — only renders when the user has at least one claim
   (server-rendered conditionally). Sized to feel inline with the tab
   label, not a balloon. */
.profile-tabs__count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 10px;
    line-height: 1;
    letter-spacing: 0;
    color: var(--color-cyan);
    background: rgba(64, 209, 253, 0.08);
    border: 1px solid rgba(64, 209, 253, 0.30);
    border-radius: 9px;
    font-variant-numeric: tabular-nums;
}
/* Active-tab badge — the active tab now has the cyan gradient
   background, so the inactive-state cyan-on-cyan-tint badge would
   disappear into it. Flip to dark text on a darker translucent
   backdrop with a tinted border so the badge keeps reading against
   the bright gradient. */
.profile-tabs__btn[aria-selected="true"] .profile-tabs__count {
    color: var(--color-bg);
    background: rgba(17, 17, 19, 0.18);
    border-color: rgba(17, 17, 19, 0.40);
}

/* Tabpanels stack as flex items inside .profile-panel and inherit its
   gap, so each tabpanel is itself a column-flex container that owns the
   spacing between its child stubs. The :not([hidden]) qualifier is
   load-bearing: a plain `.profile-tabpanel { display: flex }` rule
   beats the user-agent `[hidden] { display: none }` on specificity
   (class vs attribute is a tie, but author-stylesheet > user-agent),
   which would keep the inactive tab visible. With this qualifier the
   inactive tabpanel falls back to the UA rule and disappears. */
.profile-tabpanel:not([hidden]) {
    display: flex;
    flex-direction: column;
    gap: 14px;
}

/* --- My Flight Deals — claim list -------------------------------------- */
.profile-claims {
    /* Inherit profile-stub padding/typography; nothing custom needed at
       the section level — the list inside owns the variable bits. */
}

.profile-claims__list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 10px;
}

/* Two-line vertical stack: deal name on line 1 (wraps to N lines on
   long names), miles + claim date on line 2. The earlier grid (icon /
   body / pill) was retired when the tab was stripped down to a quick
   receipt list; restoring it would require putting the icon and pill
   back in the markup. */
.profile-claim {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 12px 14px;
    background: rgba(255, 255, 255, 0.02);
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-sharp);
}

/* Cancelled claims fade — still readable but visually demoted, so the
   user's eye lands on active prizes first. */
.profile-claim[data-claim-status="cancelled"] {
    opacity: 0.62;
}

.profile-claim__name {
    margin: 0;
    font-family: var(--font-display);
    font-weight: 500;
    font-size: 14px;
    line-height: 1.3;
    color: var(--color-fg);
    /* `overflow-wrap: anywhere` rather than `break-word` so long unbroken
       strings (rare here but cheap insurance) wrap rather than blow out
       the panel width. Normal multi-word names still break at spaces
       first because the browser only forces mid-word breaks when needed. */
    overflow-wrap: anywhere;
}

.profile-claim__meta {
    margin: 0;
    font-family: var(--font-mono);
    font-size: 11px;
    line-height: 1.3;
    color: var(--color-fg-3);
    display: inline-flex;
    flex-wrap: wrap;
    gap: 0 6px;
}
.profile-claim__miles {
    color: var(--color-fg-2);
    font-variant-numeric: tabular-nums;
}
.profile-claim__prize {
    color: var(--color-fg-2);
    font-variant-numeric: tabular-nums;
}
.profile-claim__sep { color: var(--color-fg-4); }

/* --- Empty state ------------------------------------------------------- */
.profile-claims--empty {
    text-align: center;
}

.profile-claims__empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    padding: 18px 8px 4px;
}

.profile-claims__empty-glyph {
    width: 48px;
    height: 48px;
    color: var(--color-fg-4);
    opacity: 0.7;
    margin-bottom: 2px;
}

.profile-claims__empty-title {
    margin: 0;
    font-family: var(--font-display);
    font-weight: 500;
    font-size: 14px;
    line-height: 1.25;
    color: var(--color-fg-2);
}

.profile-claims__empty-lede {
    margin: 0;
    font-family: var(--font-body);
    font-size: 12px;
    line-height: 1.45;
    color: var(--color-fg-3);
    max-width: 26ch;
}

.profile-claims__empty-cta {
    margin-top: 6px;
    padding: 6px 14px;
    font-family: var(--font-mono);
    font-weight: 500;
    font-size: 11px;
    letter-spacing: 0.04em;
    text-transform: lowercase;
    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);
    text-decoration: none;
    transition:
        background   var(--dur-hover) var(--ease),
        border-color var(--dur-hover) var(--ease),
        color        var(--dur-hover) var(--ease);
}
.profile-claims__empty-cta:hover {
    background: rgba(64, 209, 253, 0.12);
    border-color: rgba(64, 209, 253, 0.55);
    color: var(--color-cyan-glow, var(--color-cyan));
}

/* Phones (≤480px) — the panel's desktop default (position: absolute,
   width: 380px, anchored to .profile-hub's right edge) overflows the
   viewport on small screens. We swap to a fixed drop-down anchored to
   the header's bottom edge, spanning between the same 16px gutters
   that the header itself uses (.site-header is width: 100% - 32px),
   so the panel and the header line up edge-for-edge. This matches
   the hamburger drawer's drop-down behaviour and keeps the trigger
   visually attached to its panel.

   IMPORTANT — only `top` is set, sizing comes from `max-height`:
   .site-header__inner has `backdrop-filter: blur(...)`, and WebKit
   treats backdrop-filter as a containing-block trigger for fixed
   descendants — so `position: fixed` here is positioned against the
   64px-tall inner header, not the viewport. That makes any `bottom`
   value resolve against the inner header (which is shorter than the
   panel needs to be), so combining `top` + `bottom` collapses the
   panel to zero height on iOS. The supported escape is to anchor
   only `top`, then size the box with `max-height` — viewport units
   (vh / dvh) are still resolved against the actual viewport, not
   the containing block.

   `svh` is the viewport *with* Safari chrome showing (address bar
   at the bottom). `dvh` still ran the panel under that bar, which
   hid the cyan bottom edge. JS (fitPanelToVisualViewport) then
   tightens max-height to visualViewport so the ring stays above
   the toolbar as it shows/hides. */
@media (max-width: 480px) {
    .profile-panel {
        position: fixed;
        top: calc(var(--header-offset) + var(--header-h) + 8px);
        right: 16px;
        left: 16px;
        bottom: auto;
        width: auto;
        height: auto;
        max-height: calc(100vh - var(--header-offset) - var(--header-h) - 80px);
        max-height: calc(100svh - var(--header-offset) - var(--header-h) - 16px - env(safe-area-inset-bottom, 0px));
        /* Re-uses the desktop drop-down animation (defined above) so
           the open feels identical across breakpoints. */
        transform-origin: top right;
    }
}

/* ==========================================================================
   TON Connect picker — third-party modal overlay tweaks
   ==========================================================================
   The TON Connect UI modal (@tonconnect/ui) renders into a div appended
   to <body> with z-index 1000 and a 40%-black overlay. Our sticky
   .site-header sits at z-index 100 below it, so the header bleeds
   through the not-quite-opaque overlay. WalletConnect's picker covers
   the header once the user has scrolled past it but lets the header
   show through at scrollY === 0; we replicate that here by only
   hiding the header when wallet.js stamps the attribute on <body>,
   which it does only when the page is already scrolled past the
   header at the moment the modal opens.
   -------------------------------------------------------------------------- */
body[data-tc-modal-open] .site-header {
    visibility: hidden;
}

/* Desktop QR dialog is `margin: auto` inside a flex overlay. On a short
   viewport Chrome/Firefox clip the overflowing top (and don't scroll to
   it), so the title sits under our sticky header. Pin the dialog below
   the header instead. Mobile (≤440px) is a bottom sheet — leave it. */
@media (min-width: 441px) {
    [data-tc-wallets-modal-container="true"] {
        align-items: flex-start;
        /* SDK injects `padding: 20px 0` at runtime (equal specificity,
           later in document order) — !important is the override. */
        padding-top: calc(var(--header-offset) + var(--header-h) + 8px) !important;
    }
    [data-tc-wallets-modal-container="true"] > * {
        margin-top: 0;
    }
}

/* Closed WalletConnect / web3modal hosts sometimes stay in the tree
   with pointer-events still on, which swallows page scroll on iOS
   until reload. Neutralize them unless the picker is actually open. */
w3m-modal:not([open]),
wcm-modal:not([open]) {
    pointer-events: none;
}

/* --------------------------------------------------------------------------
   Overlay chrome — hamburger, wallet hub, profile panel, or a [data-modal].
   JS stamps [data-overlay-open] on html + body (bindOverlayChrome).
   Lock html: that is the page scrollport. The open panel / modal keeps
   its own overflow-y: auto so it can still scroll.
   -------------------------------------------------------------------------- */
html[data-overlay-open] {
    overflow: hidden;
    overscroll-behavior: none;
}
/* Do not set overflow:hidden on <body> while a panel or modal is open.
   The page scrollport is <html>; locking body creates a clip
   edge that shears off the hamburger / wallet / profile panels
   hanging below the sticky header. touch-action stops iOS from
   panning the page under the dimmer. */
body[data-overlay-open] {
    overscroll-behavior: none;
    touch-action: none;
}

/* [data-modal] hosts are reparented onto <body> in bindModalTriggers
   so this overlay stacks above .site-header (z-index 100). Do not
   hide the header and do not lift <main>. */

/* ==========================================================================
   Modal — minimal reusable dialog primitive
   ============================================================================
   A modal is a host element with [data-modal="<id>"] containing a backdrop
   and a panel. Open/close is driven by JS (see bindModalTriggers in main.js)
   reacting to:
     · [data-modal-open="<id>"] — any clickable opens the matching modal
     · [data-modal-close]       — any clickable inside an open modal closes it
                                  (the .modal__backdrop carries this attribute,
                                  so click-outside-to-close is free)
     · Escape key               — closes any open modal

   Scope: the primitive intentionally does only what every modal needs (chrome,
   stacking, motion, dismiss surfaces). Page-specific copy + buttons live in
   the consumer's markup. The first consumer is the referral-rules modal in
   index.php; future modals can reuse the same shape by giving them their own
   [data-modal] id and a matching trigger.
   ========================================================================== */
.modal {
    position: fixed;
    inset: 0;
    /* Above the sticky header (z-index 100) and above the wallet hub /
       profile pop-up (z-index 40 / 50). Below the TON Connect modal
       (which manages its own z-index via the SDK). */
    z-index: 200;
    display: flex;
    /* flex-start + margin-block:auto on the panel: short dialogs stay
       centred; tall ones pin to the top instead of overflowing equally
       (align-items:center clips the title and close). */
    align-items: flex-start;
    justify-content: center;
    padding: var(--sp-5);
    overscroll-behavior: none;
}
.modal[hidden] { display: none; }

.modal__backdrop {
    position: absolute;
    inset: 0;
    background: rgba(2, 2, 2, 0.72);
    -webkit-backdrop-filter: blur(4px) saturate(140%);
            backdrop-filter: blur(4px) saturate(140%);
    cursor: pointer;
    /* Backdrop is its own click target for "click outside to close". The
       cursor:pointer is a small affordance hint; the real signal is the
       blur, which reads as "page is paused". */
}

.modal__panel {
    position: relative;
    z-index: 1; /* above .modal__backdrop (backdrop-filter creates a stacking context) */
    width: min(480px, 100%);
    /* 100% of the padded host (inset 0), not 100vh — mobile chrome
       makes 100vh taller than the visible overlay. */
    max-height: 100%;
    margin-block: auto;
    overflow-y: auto;
    overscroll-behavior: contain;
    touch-action: pan-y;
    background-color: var(--color-bg-2);
    background-image:
        radial-gradient(60% 80% at 50% 0%,
            rgba(64, 209, 253, 0.06),
            transparent 70%),
        var(--grad-card-glow);
    border: var(--hairline);
    border-radius: var(--radius-sharp);
    box-shadow:
        var(--shadow-elev-1),
        0 0 0 1px rgba(64, 209, 253, 0.06);
    animation: modal-in var(--dur-panel) var(--ease) forwards;
    transform-origin: top center;
}

@keyframes modal-in {
    from { opacity: 0; transform: translateY(-8px) scale(0.985); }
    to   { opacity: 1; transform: none; }
}

.modal__head {
    position: sticky;
    top: 0;
    z-index: 2;
    padding: 22px 56px 14px 24px; /* right padding leaves room for the close button */
    border-bottom: var(--hairline);
    background-color: var(--color-bg-2);
}

.modal__eyebrow {
    margin: 0 0 6px;
    font-family: var(--font-mono);
    font-weight: 400;
    font-size: 11px;
    line-height: 1;
    letter-spacing: 0.06em;
    text-transform: lowercase;
    color: var(--color-fg-3);
}
.modal__eyebrow::before {
    content: "// ";
    color: var(--color-cyan);
}

.modal__title {
    margin: 0;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 20px;
    line-height: 1.25;
    letter-spacing: -0.2px;
    color: var(--color-fg);
}

.modal__close {
    position: absolute;
    top: 16px;
    right: 16px;
    width: 32px;
    height: 32px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--color-fg-3);
    background: rgba(255, 255, 255, 0.03);
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-sharp);
    cursor: pointer;
    transition:
        color        var(--dur-hover) var(--ease),
        border-color var(--dur-hover) var(--ease),
        background   var(--dur-hover) var(--ease);
}
.modal__close:hover {
    color: var(--color-cyan);
    border-color: rgba(64, 209, 253, 0.45);
    background: rgba(64, 209, 253, 0.06);
}
.modal__close:focus-visible {
    outline: none;
    border-color: var(--color-cyan);
    box-shadow: 0 0 0 2px rgba(64, 209, 253, 0.18);
}
.modal__close svg {
    width: 14px;
    height: 14px;
}

.modal__body {
    padding: 18px 24px 24px;
    font-family: var(--font-body);
    font-size: 14px;
    line-height: 1.6;
    color: var(--color-fg-2);
}
.modal__body p { margin: 0 0 12px; }
.modal__body p:last-child { margin-bottom: 0; }
.modal__list {
    margin: 0 0 12px;
    padding-left: 1.2em;
}
.modal__list li {
    margin: 0 0 6px;
}
.modal__list li:last-child {
    margin-bottom: 0;
}
.modal__body > :last-child { margin-bottom: 0; }
.modal__body strong {
    color: var(--color-fg);
    font-weight: 600;
}
.modal__body a {
    color: var(--color-cyan);
    text-decoration: none;
    border-bottom: 1px dashed rgba(64, 209, 253, 0.40);
    padding-bottom: 1px;
    transition: border-color var(--dur-hover) var(--ease);
}
.modal__body a:hover {
    border-bottom-color: var(--color-cyan);
}
.modal__wallet-guide {
    margin-top: 4px;
}

@media (max-width: 480px) {
    .modal { padding: var(--sp-4); }
    .modal__panel { max-height: 100%; }
    .modal__title { font-size: 18px; }
}

/* ==========================================================================
   Modal form — single-input dialog body for the reusable task-verify modal
   ============================================================================
   Layered on top of the .modal__body chrome: same padding, same colour
   palette, but the body becomes a vertical stack (instructions / field /
   error / actions) instead of a free paragraph flow.

   First consumer is [data-modal="task-verify"] in code-entry mode
   (bindTaskVerify in main.js). The same shell hosts email and X-handle
   flows by swapping the input type/label and adding a step-2 body.
   ========================================================================== */
.modal-form {
    /* Override .modal__body's paragraph spacing — this body is a form
       stack, not a paragraph flow. */
    display: flex;
    flex-direction: column;
    gap: 16px;
}

.modal-form__instructions {
    margin: 0;
    color: var(--color-fg-2);
    font-size: 14px;
    line-height: 1.55;
}

/* Sent-notice preamble — shown above the input in email-mode step 2.
   Echoes the email the user submitted in step 1 + offers a resend.
   Cyan-tinted panel so it reads as confirmation/status rather than a
   plain note. */
.modal-form__sent-notice {
    margin: 0;
    padding: 10px 12px;
    color: var(--color-fg-2);
    background: rgba(64, 209, 253, 0.05);
    border: 1px solid rgba(64, 209, 253, 0.25);
    border-radius: var(--radius-sharp);
    font-size: 13px;
    line-height: 1.5;
}
.modal-form__sent-notice[hidden] { display: none; }
.modal-form__sent-notice strong {
    font-family: var(--font-mono);
    font-weight: 500;
    color: var(--color-cyan);
}
.modal-form__resend {
    appearance: none;
    -webkit-appearance: none;
    margin-left: 6px;
    padding: 0;
    color: var(--color-cyan);
    background: none;
    border: none;
    border-bottom: 1px dashed rgba(64, 209, 253, 0.40);
    font: inherit;
    font-size: 13px;
    cursor: pointer;
    transition: border-color var(--dur-hover) var(--ease);
}
.modal-form__resend:hover { border-bottom-color: var(--color-cyan); }
.modal-form__resend:focus-visible {
    outline: none;
    border-bottom-color: var(--color-cyan);
    box-shadow: 0 1px 0 var(--color-cyan);
}
.modal-form__resend[disabled] {
    color: var(--color-fg-4);
    border-bottom-color: var(--color-stroke);
    cursor: not-allowed;
}

.modal-form__field {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.modal-form__field-label {
    font-family: var(--font-mono);
    font-weight: 400;
    font-size: 11px;
    line-height: 1;
    letter-spacing: 0.06em;
    text-transform: lowercase;
    color: var(--color-fg-3);
}
.modal-form__field-label::before {
    content: "// ";
    color: var(--color-cyan);
}

.modal-form__input {
    width: 100%;
    height: 44px;
    padding: 0 14px;
    color: var(--color-fg);
    background: rgba(255, 255, 255, 0.02);
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-sharp);
    font-family: var(--font-mono);
    font-size: 14px;
    line-height: 1;
    letter-spacing: 0.02em;
    transition:
        border-color var(--dur-hover) var(--ease),
        background   var(--dur-hover) var(--ease),
        box-shadow   var(--dur-hover) var(--ease);
}
.modal-form__input::placeholder {
    color: var(--color-fg-4);
}
.modal-form__input:hover {
    border-color: rgba(64, 209, 253, 0.40);
}
.modal-form__input:focus {
    outline: none;
    border-color: var(--color-cyan);
    background: rgba(64, 209, 253, 0.03);
    box-shadow: 0 0 0 2px rgba(64, 209, 253, 0.18);
}
.modal-form__input:disabled {
    cursor: not-allowed;
    opacity: 0.55;
}
/* Failure tint on the input echoes the inline error message below.
   Driven by [aria-invalid="true"] set by bindTaskVerify when the
   verifier returns failed. Cleared on the next keystroke. */
.modal-form__input[aria-invalid="true"] {
    border-color: var(--color-danger);
    background: rgba(232, 76, 76, 0.04);
}
.modal-form__input[aria-invalid="true"]:focus {
    box-shadow: 0 0 0 2px rgba(232, 76, 76, 0.22);
}

.modal-form__error {
    margin: 0;
    padding: 10px 12px;
    color: var(--color-danger);
    background: rgba(232, 76, 76, 0.06);
    border: 1px solid rgba(232, 76, 76, 0.30);
    border-radius: var(--radius-sharp);
    font-family: var(--font-mono);
    font-size: 12px;
    line-height: 1.4;
    letter-spacing: 0.02em;
}
.modal-form__error[hidden] { display: none; }

.modal-form__actions {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    gap: 10px;
    margin-top: 4px;
}

/* --------------------------------------------------------------------------
   Activity spinner — small rotating ring inside the submit button. Revealed
   by bindTaskVerify (main.js) while a submission is in flight; the matching
   CTA copy ("Verifying…" / "Working…") sits next to it via the .btn's
   inline-flex + gap. Hidden by default via the [hidden] attribute so the
   button renders normally on first paint.

   Sizing: 14px ring matches the 13px button text cap-height. The 1.5px
   border keeps the ring crisp at the small size without overpowering the
   primary CTA's gradient fill.

   Reduced-motion: the keyframe still runs at one revolution per 700ms but
   is slow enough not to feel jittery; users with the OS preference set will
   see the static ring (animation pauses) thanks to the @media block at the
   bottom of this stanza.
   -------------------------------------------------------------------------- */
.modal-form__spinner {
    display: inline-block;
    width: 14px;
    height: 14px;
    border: 1.5px solid currentColor;
    border-right-color: transparent;
    border-radius: 50%;
    animation: modal-form-spinner-rotate 700ms linear infinite;
    /* Negative margin pulls the visual centre back so the ring + label
       feel like one chip, matching the rest of the button's :after/:before
       bracket geometry. */
    margin-right: -2px;
}
.modal-form__spinner[hidden] { display: none; }
@keyframes modal-form-spinner-rotate {
    to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
    .modal-form__spinner { animation: none; }
}
/* Compact buttons inside the modal so the row doesn't overflow the panel
   on narrow viewports. The existing .btn--sm is similar but visually
   heavier than we want here; an inline height override keeps things
   tight without forking a new btn-size class. */
.modal-form__actions .btn {
    --btn-h: 40px;
    padding: 0 18px;
    font-size: 13px;
}

@media (max-width: 480px) {
    .modal-form__actions {
        flex-direction: column-reverse;
        align-items: stretch;
        gap: 8px;
    }
    .modal-form__actions .btn { width: 100%; }
}

/* ==========================================================================
   Claim-confirm modal body — the "You're about to spend N miles" dialog
   ============================================================================
   Lives inside [data-modal="claim-confirm"] in flight-deals.php and is
   populated by flight-deals.js when a Claim button is pressed. Distinct
   from .modal-form because there's no input — just a copy stack with the
   miles cost emphasised so the user can see exactly what they're about to
   spend before they confirm.

   Layout: vertical flex stack matching .modal-form's gap so the actions
   row sits at the same offset whether the body is a form or this
   confirmation copy.
   ========================================================================== */
.claim-confirm {
    display: flex;
    flex-direction: column;
    gap: 14px;
}

.claim-confirm__lede {
    margin: 0;
    color: var(--color-fg-2);
    font-size: 14px;
    line-height: 1.55;
}
.claim-confirm__name {
    color: var(--color-fg);
    font-weight: 600;
    /* Allow long deal names to wrap inside the lede without breaking
       layout. White-space and overflow stay default — natural wrapping. */
}

/* Cost panel — the miles number is the largest piece of copy in the
   dialog so the user is never in doubt about what they're spending.
   The accent colour matches the rest of the campaign's miles-mention
   pattern (deal price footer, balance bar). */
.claim-confirm__cost {
    margin: 0;
    padding: 14px 16px;
    background: rgba(64, 209, 253, 0.05);
    border: 1px solid rgba(64, 209, 253, 0.25);
    border-radius: var(--radius-sharp);
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 8px;
    line-height: 1.4;
}
.claim-confirm__cost-value {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 28px;
    color: var(--color-cyan-glow, var(--color-cyan));
    line-height: 1;
    letter-spacing: -0.01em;
}
.claim-confirm__cost-unit {
    color: var(--color-fg-2);
    font-size: 13px;
}

.claim-confirm__note {
    margin: 0;
    color: var(--color-fg-3, rgba(255, 255, 255, 0.45));
    font-size: 12px;
    line-height: 1.5;
}

[data-claim-reveal-amount] {
    color: var(--color-cyan-glow, var(--color-cyan));
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 22px;
    letter-spacing: -0.01em;
}

/* Mid-flight state on the Confirm submit. The button is .btn--primary
   already; the data-busy flag dims it slightly so the spinner reads as
   the active element and the user understands the click landed. */
[data-claim-confirm-submit][data-busy="true"] {
    opacity: 0.85;
    cursor: progress;
}

@media (max-width: 480px) {
    .claim-confirm__cost {
        flex-direction: column;
        align-items: flex-start;
        gap: 2px;
    }
    .claim-confirm__cost-value { font-size: 24px; }
}

/* ==========================================================================
   Tooltip — minimal disclosure primitive for short contextual explainers
   ============================================================================
   A tooltip is a wrapper [.tooltip] containing a [.tooltip__trigger] button
   and a [.tooltip__bubble]. The bubble is shown when the wrapper is hovered
   OR when keyboard focus lands inside it (focus-within). Pure CSS — no JS
   required, which means the bubble works during the panel's open animation
   and the rest of the page hasn't booted yet.

   Why focus-within (not just :focus on the trigger)?
     Because the bubble can contain a link (e.g. "Read the Campaign Rules").
     Moving focus from the trigger to the link should KEEP the bubble open.
     focus-within stays true as long as any descendant is focused.

   Why pointer-events: auto only when shown?
     So the hidden bubble doesn't intercept clicks or hover on whatever sits
     under it (next stub in the profile panel, in the first consumer's case).

   First consumer: the "i" trigger next to the flight class title in the
   profile pop-up — see includes/header.php.
   ========================================================================== */
.tooltip {
    position: relative;
    display: inline-flex;
    align-items: center;
}

.tooltip__trigger {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    padding: 0;
    color: var(--color-fg-3);
    background: rgba(255, 255, 255, 0.03);
    border: 1px solid var(--color-stroke);
    border-radius: 50%;
    cursor: pointer;
    transition:
        color        var(--dur-hover) var(--ease),
        border-color var(--dur-hover) var(--ease),
        background   var(--dur-hover) var(--ease);
}
.tooltip__trigger:hover,
.tooltip:focus-within .tooltip__trigger {
    color: var(--color-cyan);
    border-color: rgba(64, 209, 253, 0.45);
    background: rgba(64, 209, 253, 0.06);
}
.tooltip__trigger:focus-visible {
    outline: none;
    border-color: var(--color-cyan);
    box-shadow: 0 0 0 2px rgba(64, 209, 253, 0.18);
}
.tooltip__trigger svg {
    width: 11px;
    height: 11px;
}

/* Bubble positions BELOW the trigger and ANCHORS to its right edge. The
   anchor choice matters because the first consumer (flight-class stub in
   the profile pop-up) puts the trigger near the panel's right edge — a
   left-anchored bubble would extend off the panel; right-anchored stays
   inside. The 18em width is a deliberate "1-2 short paragraphs" target;
   wider than this and the disclosure starts wanting to be a modal.

   Hover bridge: the bubble is flush under the trigger (top: 100%) but
   keeps an 8px visual gap via transparent padding-top; panel chrome is
   painted on ::after below that padding. The padding is still part of
   the bubble's hit box, so the cursor can travel from the "i" into the
   tip (and onto the Campaign Rules link) without leaving .tooltip:hover.
   Without it, the old calc(100% + 8px) gap was dead space and the tip
   collapsed mid-move. */
.tooltip__bubble {
    position: absolute;
    top: 100%;
    right: 0;
    width: 18em;
    max-width: calc(100vw - 32px);
    z-index: 5;
    /* 8px top = hover bridge; remaining padding matches the old panel inset. */
    padding: 20px 14px 12px;
    /* Visible panel chrome lives on ::after so the 8px bridge stays clear. */
    background: transparent;
    border: none;
    box-shadow: none;
    font-family: var(--font-body);
    font-size: 12px;
    line-height: 1.55;
    color: var(--color-fg-2);
    /* Hidden state */
    opacity: 0;
    pointer-events: none;
    transform: translateY(-4px);
    transition:
        opacity   var(--dur-state) var(--ease),
        transform var(--dur-state) var(--ease);
}

/* Visible panel — chrome that used to sit on .tooltip__bubble itself. */
.tooltip__bubble::after {
    content: "";
    position: absolute;
    inset: 8px 0 0;
    z-index: -1;
    background-color: var(--color-bg-3);
    background-image:
        radial-gradient(70% 80% at 90% 0%,
            rgba(64, 209, 253, 0.05),
            transparent 70%);
    border: var(--hairline);
    border-color: rgba(64, 209, 253, 0.22);
    border-radius: var(--radius-sharp);
    box-shadow: var(--shadow-elev-1);
    pointer-events: none;
}

/* Little arrow pointing back at the trigger so the bubble visually
   "belongs" to the i icon rather than floating alone. Sits on the
   visible panel's top edge (below the 8px bridge). */
.tooltip__bubble::before {
    content: "";
    position: absolute;
    top: 3px; /* 8px bridge - 5px half-diamond */
    right: 6px;
    width: 8px;
    height: 8px;
    background-color: var(--color-bg-3);
    border-top: 1px solid rgba(64, 209, 253, 0.22);
    border-left: 1px solid rgba(64, 209, 253, 0.22);
    transform: rotate(45deg);
}

.tooltip:hover .tooltip__bubble,
.tooltip:focus-within .tooltip__bubble {
    opacity: 1;
    pointer-events: auto;
    transform: none;
}

.tooltip__bubble a {
    color: var(--color-cyan);
    text-decoration: none;
    border-bottom: 1px dashed rgba(64, 209, 253, 0.40);
    padding-bottom: 1px;
    transition: border-color var(--dur-hover) var(--ease);
}
.tooltip__bubble a:hover {
    border-bottom-color: var(--color-cyan);
}

/* --------------------------------------------------------------------------
   Cookie notice
   Compact bottom-left toast: cookie glyph, copy + Privacy Policy link,
   dismiss X. Informational only — no consent categories. Markup lives
   in includes/footer.php; dismiss is persisted by main.js.
   -------------------------------------------------------------------------- */
.cookie-notice {
    position: fixed;
    z-index: 110; /* above .site-header (100), below .modal (200) */
    left: max(var(--sp-5), env(safe-area-inset-left, 0px));
    bottom: max(var(--sp-5), env(safe-area-inset-bottom, 0px));
    display: flex;
    align-items: flex-start;
    gap: var(--sp-4);
    width: min(400px, calc(100vw - 2 * var(--sp-5)));
    padding: 18px 16px 18px 18px;
    color: var(--color-fg);
    background: var(--color-bg-3);
    border: var(--hairline);
    border-radius: 20px;
    box-shadow: var(--shadow-elev-1);
}
.cookie-notice:not([hidden]) {
    animation: cookie-notice-in var(--dur-panel) var(--ease);
}
.cookie-notice[hidden] { display: none; }

@keyframes cookie-notice-in {
    from {
        opacity: 0;
        transform: translateY(10px);
    }
    to {
        opacity: 1;
        transform: none;
    }
}

@media (prefers-reduced-motion: reduce) {
    .cookie-notice:not([hidden]) { animation: none; }
}

.cookie-notice__icon {
    flex: 0 0 auto;
    width: 22px;
    height: 22px;
    margin-top: 1px;
    color: var(--color-fg);
}

.cookie-notice__text {
    flex: 1 1 auto;
    margin: 0;
    padding-right: var(--sp-2);
    font-family: var(--font-body);
    font-size: 14px;
    font-weight: 400;
    line-height: 1.5;
    color: var(--color-fg);
}
.cookie-notice__text a {
    color: inherit;
    text-decoration: underline;
    text-underline-offset: 2px;
    cursor: pointer;
    transition: color var(--dur-hover) var(--ease);
}
.cookie-notice__text a:hover {
    color: var(--color-cyan);
}
.cookie-notice__text a:focus-visible {
    outline: 2px solid var(--color-cyan);
    outline-offset: 2px;
}

.cookie-notice__close {
    flex: 0 0 auto;
    width: 28px;
    height: 28px;
    margin: -6px -4px 0 0;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--color-fg);
    background: transparent;
    border: 0;
    border-radius: var(--radius-sharp);
    cursor: pointer;
    transition: color var(--dur-hover) var(--ease);
}
.cookie-notice__close:hover { color: var(--color-cyan); }
.cookie-notice__close:focus-visible {
    outline: 2px solid var(--color-cyan);
    outline-offset: 2px;
}
.cookie-notice__close svg {
    width: 14px;
    height: 14px;
}
