/* Moonfall Store - Premium Aesthetics (full-screen, two-section layout) */
.store-modal {
    display: none;
    position: fixed;
    inset: 0;
    background-color: rgba(0, 0, 0, 0.85);
    z-index: 30000;
    align-items: stretch;
    justify-content: center;
    opacity: 0;
    transition: opacity 0.3s ease;
}

.store-modal.visible {
    opacity: 1;
}

.store-container {
    width: 100%;
    height: 100%;
    background: linear-gradient(145deg, #1a1a2e, #16213e);
    display: flex;
    flex-direction: column;
    overflow: hidden;
    position: relative;
    /* How far the centred title block is inset from each side, so the
       absolutely positioned balance row (top-left) can never reach it. It is
       the row's widest state: Stardust pill ≤ 110px (glyph + "987M"),
       Moonlight meter ≤ 170px (its `max-width` below), the "+ Refill" pill
       ≤ 115px (Portuguese "Recarregar" is the widest), two 10px gaps less the
       4px the refill tucks under the meter, the 22px left offset — 423px at
       every cap at once (a real row measures ~330-400px). Raise this if any
       of those caps rise. */
    --store-chrome-inset: 430px;
}

/* Decorative Elements */
.store-container::before {
    content: '';
    position: absolute;
    top: -80px;
    right: -80px;
    width: 320px;
    height: 320px;
    background: radial-gradient(circle, rgba(255, 215, 0, 0.12), transparent 70%);
    pointer-events: none;
}

/* THE TITLE BLOCK IS INSET BY THE WIDTH THE BALANCE ROW CAN REACH.

   The balance row is absolutely positioned chrome in the top-left corner, and
   the header's side padding is the only thing that keeps the centred title
   and subtitle out from under it. That padding was 180px while the row ran to
   530px in French ("Poussière d'étoile" pill + meter + "Recharger le Clair de
   lune"): the subtitle painted straight across the refill button at 1400x640.

   The row is now bounded — the Stardust pill carries a glyph instead of a
   word, and the refill says a short verb, not a sentence — so its widest
   state is a known number: pill ≤ 110px, meter ≤ 170px, refill ≤ 115px, two
   gaps. The inset is that number, on both sides so the title stays centred
   on the screen. One-column and phone bands take the row out of the corner
   altogether and set their own inset. */
.store-header {
    flex: none;
    padding: 10px var(--store-chrome-inset, 430px) 8px;
    text-align: center;
    border-bottom: 1px solid rgba(255, 255, 255, 0.08);
    background: rgba(0, 0, 0, 0.22);
}

.store-title {
    font-family: 'Cinzel', serif;
    font-size: clamp(1.7rem, 2.4vw, 2.3rem);
    color: #ffd700;
    text-shadow: 0 2px 10px rgba(255, 215, 0, 0.4);
    margin: 0;
}

.store-subtitle {
    color: #aab;
    margin-top: 6px;
    font-size: clamp(0.9rem, 1.1vw, 1.05rem);
}

.store-close {
    position: absolute;
    top: 16px;
    right: 22px;
    background: none;
    border: none;
    color: #fff;
    font-size: 2rem;
    line-height: 1;
    width: 44px;
    height: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    margin: 0;
    padding: 0;
    transition: transform 0.2s, color 0.2s;
    z-index: 10;
}

.store-close:focus-visible {
    outline: 3px solid #fff2a8;
    outline-offset: 3px;
    border-radius: 50%;
}

.store-close:hover {
    color: #ff6b6b;
    transform: rotate(90deg);
}

.store-balance {
    position: absolute;
    top: 16px;
    left: 22px;
    z-index: 10;
    min-height: 44px;
    /* NO WORD, A GLYPH. The pill used to spell "Stardust" beside the number
       and ellipsise the word when it did not fit — which in French it never
       did ("Poussière …"), and the owner's verdict on that was "abbreviated,
       looks bad". The word is gone: the Stardust glyph (`--mf-stardust-glyph`,
       shared with the lobby chip and the pack prices) carries the meaning in
       every language, the accessible name still says the word, and the pill
       is the same width in French as in English. The cap only bounds a
       runaway number: "987M" plus the glyph is ~90px. */
    max-width: 150px;
    padding: 0 14px 0 12px;
    border: 1px solid rgba(189, 155, 239, 0.38);
    border-radius: 999px;
    background: rgba(12, 8, 24, 0.78);
    box-shadow: 0 5px 18px rgba(0, 0, 0, 0.28);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    color: #d9c2f4;
    font-size: 0.78rem;
    line-height: 1;
    white-space: nowrap;
    pointer-events: none;
}

/* The glyph is a pseudo-element so the pill's text stays the number alone:
   `readTriggerBalance` and the count animation both read the element's text. */
#store-stardust-balance::before {
    content: '';
    flex: none;
    width: 1.05rem;
    height: 1.05rem;
    background: var(--mf-stardust-glyph) center / contain no-repeat;
}

.store-balance-value {
    flex: 0 0 auto;
    max-width: 100%;
    overflow: hidden;
    color: #f6e27a;
    font-size: 0;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.store-balance-value::after {
    content: attr(data-compact-value);
    font-size: 0.94rem;
}

/* Two-section grid: Moon Pass | Stardust. Scrollbar only appears if a small
   viewport truly can't fit it (mobile), never on a normal desktop. */
.store-content {
    flex: 1;
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    /* Shared row tracks: header / cards / disclosure. Both columns are subgrids
       that inherit these rows, so the header row is the SAME height in each
       column and the card row starts at the same Y — alignment holds at every
       width, regardless of how the taglines wrap. */
    grid-template-rows: auto auto auto;
    column-gap: clamp(20px, 3vw, 44px);
    row-gap: 0;
    /* `safe` keeps the top reachable when content is taller than the viewport,
       instead of centering it off-screen and clipping the header/cards. */
    align-content: safe center;
    width: 100%;
    max-width: 1640px;
    margin: 0 auto;
    padding: clamp(10px, 1.6vh, 26px) clamp(20px, 4vw, 56px);
    overflow-y: auto;
}

/* MOONLIGHT IS SHOWN WHERE MOONLIGHT IS SOLD — the passes below are priced in
   it, and a phone has no hover, so this is where a touch player reads their
   own meter and what it buys.

   `.store-balance` is absolutely positioned at a FIXED top-left, so a second
   one landed exactly on top of the Stardust pill. Addressed by id because the
   shared class is declared later in this file and would otherwise win on
   source order at equal specificity. */
/* The pills sit side by side. `.store-balance` positions each one absolutely
   at a fixed corner, so the row takes over the positioning and the children
   become ordinary flex items — otherwise keeping them adjacent means guessing
   the Stardust pill's width, which changes with the number in it. */
.store-balances {
    position: absolute;
    top: 16px;
    left: 22px;
    z-index: 10;
    display: flex;
    /* STRETCH, not center. The two pills are siblings in one row and must be
       the SAME height, or their tops sit several pixels apart and the row
       reads as two unrelated things stuck together. */
    align-items: stretch;
    gap: 10px;
    max-width: calc(100% - 110px);
}

.store-balances > .store-balance {
    position: static;
    top: auto;
    left: auto;
    right: auto;
    /* One shared floor, so neither pill sets the row's height on its own. */
    min-height: 36px;
}

#store-moonlight-balance {
    /* Ten segments and a percentage: 158px. The cap is what the header inset
       above is computed from, so a wider meter must raise both. */
    max-width: min(30vw, 170px);
    justify-content: flex-start;
    gap: 0.55rem;
    flex-wrap: nowrap;
    white-space: nowrap;
    overflow: hidden;
}

#store-moonlight-balance .store-balance-meter {
    display: inline-flex;
    align-items: center;
    gap: 0.42rem;
    padding: 0;
    border: 0;
    background: none;
    box-shadow: none;
    height: auto;
    min-height: 0;
}

/* A quiet secondary action, not a second Subscribe: refill is a top-up for
   someone who already pays, and it must not compete with the passes below.

   It is a SIBLING of the two status pills, not a child of one. Inside
   `#store-moonlight-balance` it inherited that pill's `pointer-events: none`
   (dead to every click) and was clipped by its `overflow: hidden` + max-width,
   which is exactly how it shipped: a cut-off button that did nothing. */
/* A continuation of the line above it, not a feature of its own: no tick, no
   divider, and set back so the eye reads the two as one statement. */
.plan-features > .plan-feature-note {
    list-style: none;
    border-top: 0;
    padding-top: 0;
    margin-top: -0.35em;
    padding-left: 1.55em;
    color: rgba(217, 194, 244, 0.62);
    font-size: 0.92em;
}

.plan-features > .plan-feature-note::before { content: none; }

/* The divider belongs to the line ABOVE, so zeroing the note's own border-top
   left the hairline exactly where it severed the sentence it continues. */
.plan-features > li:has(+ .plan-feature-note) { border-bottom: 0; }
.plan-features > .plan-feature-note { border-bottom: 0; }

/* "+ REFILL" ON THE METER, NOT A SENTENCE BESIDE IT — AND NOT A BARE "+".

   The refill was a text button — "Refill Moonlight", and in French "Recharger
   le Clair de lune", 172px of it — sitting third in a row that already held
   two pills, on the one line of the store that has no room to give. A round
   "+" alone was tried next and the owner's verdict was "even more strange,
   still unclear". So the control keeps the mark every game store uses for
   "more of this" AND says the verb: "+ Refill", "+ Recharger", "+ 補充" — the
   short translation, never the sentence, so it is ~60-110px in every
   language. It sits at the end of the meter it fills, the pills' height. Its
   name is still the full sentence (aria-label and tooltip), and the purchase
   is spelled out before any money moves by the confirm dialog it opens. */
.store-balances > .store-balance-refill {
    flex: none;
    /* Explicit, not stretched: Chromium does not stretch a <button> flex item
       to the line (measured 28px against the pills' 36 with `align-self:
       stretch` applied), so the control states the pills' floor. */
    height: 36px;
    padding: 0 12px 0 10px;
    /* Tucked against the meter: the two read as one control, "Moonlight, +".
       The vertical zeros matter: the global `button` rule carries a 4px
       margin, which made this 36px control a 44px row item and pushed both
       pills to 44 with it. */
    margin: 0 0 0 -4px;
    border-radius: 999px;
    border: 1px solid rgba(214, 176, 92, 0.45);
    background: rgba(214, 176, 92, 0.12);
    color: #e0c68a;
    font: inherit;
    font-size: 0.72rem;
    font-weight: 600;
    line-height: 1;
    white-space: nowrap;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    /* The pills opt out of pointer events; a control must opt back in. */
    pointer-events: auto;
    transition: background 160ms ease, border-color 160ms ease;
}

/* The mark is drawn, not typed: a "+" character sits on the font's baseline
   and lands a pixel or two off centre beside a word, and its weight changes
   with the fallback font. Two hairlines are the same in every font. */
.store-balances > .store-balance-refill::before {
    content: '';
    flex: none;
    width: 10px;
    height: 10px;
    background:
        linear-gradient(currentColor, currentColor) center / 10px 2px no-repeat,
        linear-gradient(currentColor, currentColor) center / 2px 10px no-repeat;
}

.store-balances > .store-balance-refill:hover {
    background: rgba(214, 176, 92, 0.22);
    border-color: rgba(214, 176, 92, 0.7);
}

.store-balances > .store-balance-refill:focus-visible {
    outline: 2px solid rgba(224, 198, 138, 0.9);
    outline-offset: 2px;
}

.store-balances > .store-balance-refill[hidden] { display: none; }

/* A short page box: the meter keeps its ten segments but gives up the extra
   breathing room. (This block once repeated the whole row and styled a refill
   button INSIDE the meter pill — a child the markup stopped having when the
   refill became the row's own item. Only the live declarations remain.) */
@media (orientation: landscape) and (max-height: 560px) {
    #store-moonlight-balance {
        min-height: 30px;
        gap: 0.35rem;
    }
}

.store-loading {
    grid-column: 1 / -1;
    text-align: center;
    color: #aaa;
}

/* Sections — each spans the header + card rows of the parent grid and inherits
   them via `subgrid`, so the two columns are locked to the same row geometry. */
.store-section {
    display: grid;
    grid-template-rows: subgrid;
    grid-row: 1 / 3;
    min-width: 0;
}

.store-section--stardust {
    padding-left: clamp(16px, 2.5vw, 40px);
    border-left: 1px solid rgba(255, 215, 0, 0.12);
}

/* The head occupies the shared header row. Both heads therefore have identical
   height (the row's height) no matter how many lines a tagline wraps to, so the
   titles align at the top and the cards below align at the bottom. */
.store-section__head {
    text-align: center;
    padding-bottom: 10px;
}

.store-section__title {
    font-family: 'Cinzel', serif;
    font-size: clamp(1.3rem, 1.8vw, 1.7rem);
    color: #ffd700;
    margin: 0;
    text-shadow: 0 2px 10px rgba(255, 215, 0, 0.3);
}

.store-section__sub {
    color: #bdb8c8;
    margin: 8px auto 0;
    max-width: 460px;
    font-size: clamp(0.88rem, 1vw, 1rem);
    line-height: 1.45;
}

/* THE PERIOD CONTROL — one segmented pill, replacing four buttons.
   It lives in the section HEAD, never in a wrapper around `.store-plans`:
   that element is placed by `grid-row` inside a subgrid shared with the
   Stardust column, so anything that stops it being a direct child silently
   unplaces the cards. */
.store-period {
    display: inline-flex;
    margin: 12px auto 0;
    padding: 3px;
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.42);
    border: 1px solid rgba(255, 215, 0, 0.16);
    gap: 3px;
}

.store-period__option {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 1px;
    min-height: var(--mf-touch, 40px);
    padding: 4px 16px;
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: #bdb8c8;
    font-family: inherit;
    font-size: clamp(0.8rem, 0.95vw, 0.92rem);
    font-weight: 600;
    line-height: 1.15;
    cursor: pointer;
    transition: background 160ms ease, color 160ms ease;
}

.store-period__option:hover {
    color: #f3eef8;
}

.store-period__option.is-selected {
    background: linear-gradient(135deg, rgba(255, 215, 0, 0.22), rgba(255, 215, 0, 0.1));
    color: #ffe98a;
    box-shadow: inset 0 0 0 1px rgba(255, 215, 0, 0.3);
}

.store-period__option:focus-visible {
    outline: 2px solid #ffd700;
    outline-offset: 2px;
}

/* The saving rides the tab it belongs to, so the two prices are comparable
   BEFORE one is chosen — which is the whole reason this control exists. */
.store-period__note {
    font-size: 0.68rem;
    font-weight: 700;
    letter-spacing: 0.02em;
    color: #7bd88f;
    text-transform: none;
}

/* Shown only where the cards give up their own copy of it, and inline inside
   the disclosure sentence so it costs no line of its own. */
.store-disclosure .store-tax-note {
    display: none;
    margin-right: 4px;
}

/* The renewal disclosure is always shown — it is the one fact a subscriber
   must be told before they pay. Inline, so it joins the existing sentence
   flow instead of adding a paragraph. */
.store-disclosure .store-renew-note {
    display: inline;
    margin-right: 4px;
}

.store-plans,
.store-stardust {
    display: flex;
    gap: 18px;
    justify-content: center;
    align-items: stretch;
    flex-wrap: wrap;
    /* Sits in the shared card row; both columns' cards therefore start at the
       same Y and stretch to the same row height. */
    align-self: stretch;
}

.store-stardust {
    gap: 14px;
}

/* Plan Cards */
.plan-card {
    background: rgba(255, 255, 255, 0.05);
    border-radius: 16px;
    padding: 14px 18px;
    flex: 1 1 240px;
    min-width: 0;
    max-width: 360px;
    display: flex;
    flex-direction: column;
    align-items: center;
    border: 1px solid rgba(255, 255, 255, 0.1);
    transition: transform 0.3s ease, box-shadow 0.3s ease, border-color 0.3s;
    position: relative;
    cursor: default;
}

.plan-card:hover {
    transform: translateY(-8px);
    border-color: rgba(255, 215, 0, 0.5);
    box-shadow: 0 15px 30px rgba(0, 0, 0, 0.4), 0 0 15px rgba(255, 215, 0, 0.2);
}

.plan-card.highlight {
    background: linear-gradient(135deg, rgba(255, 215, 0, 0.1), rgba(0, 0, 0, 0.4));
    border: 1px solid rgba(255, 215, 0, 0.6);
    box-shadow: 0 10px 40px rgba(255, 215, 0, 0.15);
}

.plan-card--active {
    flex-basis: 100%;
    max-width: 460px;
}

.plan-card--status {
    flex-basis: 100%;
    max-width: 460px;
    min-height: clamp(230px, 38svh, 390px);
    justify-content: center;
}

.plan-card--status:hover {
    transform: none;
    border-color: rgba(255, 215, 0, 0.6);
}

.plan-card--status-error {
    border-color: rgba(255, 151, 151, 0.5);
    background: linear-gradient(135deg, rgba(90, 32, 48, 0.24), rgba(0, 0, 0, 0.4));
}

.store-subscription-status {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
}

.store-status-spinner {
    width: 18px;
    height: 18px;
    flex: 0 0 18px;
    border: 2px solid rgba(255, 223, 92, 0.25);
    border-top-color: #ffdf5c;
    border-radius: 50%;
    animation: store-status-spin 900ms linear infinite;
}

.store-status-copy {
    margin: 0;
    color: #e8e2d4;
    font-size: 1rem;
    line-height: 1.5;
    text-align: center;
}

.store-status-retry {
    max-width: 220px;
    margin-top: 20px;
}

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


/* Compact cards for the Stardust column — always three across and equal width
   (flex-basis 0 + nowrap) so no card ever drops to a second row and their
   amounts/prices stay on one shared baseline at every two-column width. */
.store-stardust {
    flex-wrap: nowrap;
}

.store-stardust .plan-card {
    flex: 1 1 0;
    max-width: 220px;
    /* Top padding trimmed by the same few pixels the larger emblem below adds,
       so the symbol grows INSIDE the card instead of growing the card. */
    padding: 10px 12px 14px;
}

/* PRODUCT ARTWORK LIVES IN A WELL THE LAYOUT SIZES, NEVER THE IMAGE.

   Every emblem is an `<img class="plan-icon">` inside a `.plan-art` box. The
   box is what the card lays out — its size comes from these rules alone —
   and the image is absolutely positioned inside it, so an image that has not
   arrived yet occupies exactly the space the arrived one will. That is
   docs/asset-loading.md law 2 ("placeholders reserve media space, media swaps
   never move existing UI"), and it was being broken on every landscape phone:
   the bare `<img>` had no box until its bytes landed, then took 30-52px of
   intrinsic height, and every buy button, the pass cards and the legal
   footer moved down by that amount a moment after the store opened. Measured
   in Chrome and Firefox at 932x300 (+30.6px), 852x344 (+51.6px), 667x275
   (+34.9px), 915x336 (+50.4px). The owner: "it morphs ... looks like bugs,
   immaterial, unfinished".

   The well is also what lets the Stardust art be BIG on desktop (below):
   a flexible box takes the card's spare height, which a fixed-size image
   never could, and it costs the fold nothing because the box only grows into
   space the pass cards already forced the row to have. */
.plan-art {
    position: relative;
    flex: none;
    /* Slightly larger than the old clamp(54px, 8.5svh, 92px): the emblem is the
       thing being bought and it read as a small decoration above the text.
       Sized in svh (not vw) so it shrinks on short viewports. */
    width: clamp(60px, 9.6svh, 104px);
    height: clamp(60px, 9.6svh, 104px);
    margin: 0 auto 5px;
    pointer-events: none;
    -webkit-user-select: none;
    user-select: none;
}

.plan-icon {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    /* No crop, whatever the well's proportions. */
    object-fit: contain;
    margin: 0;
    /* Firefox paints an image's alt text INSIDE its box while the bytes are
       still arriving ("Sac de poussière" in white, then the picture over it):
       a text-to-picture swap on every cold load, and the exact "not hidden is
       not the same as not blank" flash docs/asset-loading.md forbids. The alt
       keeps its accessible role; it just has no ink. Chromium and WebKit
       paint nothing there either way. */
    color: transparent;
    filter: drop-shadow(0 6px 14px rgba(0, 0, 0, 0.45)) drop-shadow(0 0 10px rgba(255, 215, 0, 0.22));
    transition: transform 0.3s ease, filter 0.3s ease;
    pointer-events: none;
    -webkit-user-select: none;
    user-select: none;
}

.plan-card:hover .plan-icon {
    transform: translateY(-3px) scale(1.05);
    filter: drop-shadow(0 10px 18px rgba(0, 0, 0, 0.5)) drop-shadow(0 0 16px rgba(255, 215, 0, 0.4));
}

/* THE STARDUST ART FILLS THE CARD'S SPARE HEIGHT. A pack card carries a
   title, an amount, a price and a button; a pass card beside it carries a
   benefit list. The row is as tall as the pass cards, so the pack cards had
   a hundred-odd pixels of nothing between the price and the button while the
   product picture above sat at 38px — "the whole point of these assets is so
   they're readable, they need to be a bit bigger at least on desktop where
   there is space". The well is the flexible item in the card, so the spare
   height goes to the picture, bounded on the tallest windows. The floor keeps
   it legible where the row is short, and in the one-column band (no pass card
   to stretch against) it is the size. */
.store-stardust .plan-art {
    flex: 1 1 auto;
    width: 100%;
    height: auto;
    min-height: clamp(96px, 14svh, 160px);
    /* Capped a little under what a 1080p window can hand it, so on the
       tallest windows the last of the slack reaches the buy button's
       `margin-top: auto` instead — the card then keeps a breath of air above
       its button, as the pass cards beside it do, rather than stacking price,
       USD line and button edge to edge under a picture. */
    max-height: 200px;
    margin: 0 auto 6px;
}

/* The same breath of air on the windows where the art takes every spare
   pixel (1400x640 hands it ~140px): the USD line keeps a fixed gap to the
   button, paid for by the art, never by the fold. */
.store-stardust .plan-price-subtitle {
    margin-bottom: 16px;
}

/* The single "you're subscribed" card centers one slightly larger emblem. */
.plan-card--active .plan-art {
    width: clamp(72px, 11svh, 116px);
    height: clamp(72px, 11svh, 116px);
}

@media (prefers-reduced-motion: reduce) {
    .plan-icon,
    .plan-card:hover .plan-icon {
        transition: none;
        transform: none;
    }

    .store-status-spinner {
        animation: none;
    }
}

.plan-badge {
    position: absolute;
    top: -12px;
    background: linear-gradient(90deg, #ffd700, #ffaa00);
    color: #000;
    font-weight: bold;
    padding: 5px 15px;
    border-radius: 20px;
    font-size: 0.85rem;
    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
}

.plan-title {
    font-family: 'Cinzel', serif;
    font-size: 1.5rem;
    margin-bottom: 6px;
    color: #fff;
    text-align: center;
}

/* Reserve a fixed two-line box for every Stardust title and center the text in
   it. "Bag of Stardust" is one line while "Chest/Vault of Stardust" can wrap to
   two at some widths; without this the price line below would sit at different
   heights across the three packs. A fixed box keeps all three prices aligned. */
.store-stardust .plan-title {
    font-size: 1.15rem;
    line-height: 1.2;
    min-height: 2.4em;
    display: flex;
    align-items: center;
    justify-content: center;
    /* The titles carry soft hyphens ("Star·dust", "Sternen·staub") for the
       landscape squeeze band below, where a pack card is 58-83px wide. Line
       breaking is greedy, so anywhere the whole word fits on its own line a
       live soft hyphen still gets used the moment "Chest of Star-" is shorter
       than the box — "Chest of Star- / dust" at 1400x640, where "Chest of /
       Stardust" is right. `none` makes the character inert; the band turns it
       back on. */
    hyphens: none;
}

.plan-price {
    font-size: 2.1rem;
    font-weight: 700;
    color: #fff;
    margin-bottom: 4px;
    display: flex;
    align-items: flex-start;
}

.plan-price span {
    font-size: 0.95rem;
    margin-top: 8px;
    color: #ccc;
    font-weight: normal;
    margin-left: 5px;
}

/* THE PACK AMOUNT IS "1K" AND THE GLYPH, not "1K Stardust". The unit word
   repeated the card title ("Bag of STARDUST"), and in French, German and
   Portuguese it was long enough to wrap the amount line at the very widths
   the store is checked at. The glyph is the one the balance pill and the
   lobby chip use, so the amount reads as the same currency the player already
   holds; a clipped `.mf-sr-only` span speaks the exact amount and the word.

   The visible figure sits in its own span (so it can be hidden from
   assistive tech in favour of the exact spoken amount), and that span must
   NOT inherit the "/mo" suffix styling `.plan-price span` above gives every
   span: it is the amount, at the amount's size and colour. */
.plan-price > .plan-price-amount {
    font-size: inherit;
    font-weight: inherit;
    /* `line-height` too: the landscape band gives `.plan-price span` a 1.35
       line box for the CJK "/月" suffix, which on this span grew the amount
       row by 7px at 932x300 and put the buy buttons 3px under the fold. */
    line-height: inherit;
    color: inherit;
    margin: 0;
}

.plan-price .stardust-glyph {
    /* `.plan-price span` above sets the "/mo" suffix's 0.95rem; the glyph
       follows the AMOUNT's size instead, so it scales with the figure. */
    font-size: inherit;
    align-self: center;
    width: 0.62em;
    height: 0.62em;
    margin: 0 0 0 0.2em;
}

.plan-price-subtitle {
    color: #888;
    margin-bottom: 10px;
    font-size: 0.9rem;
}

/* Tax disclosure. It must stay visible on every page box — it is the only
   place a buyer learns the price is exclusive of VAT — so it is sized to cost
   as little vertical space as possible rather than being hidden when small. */
.plan-tax-note {
    color: #7a7a7a;
    font-size: 0.72rem;
    line-height: 1.25;
    margin: 0 0 6px;
}

.plan-saving {
    color: #4cd137;
    font-weight: bold;
    font-size: 0.9rem;
    margin-bottom: 10px;
    min-height: 18px;
}

.plan-features {
    list-style: none;
    padding: 0;
    margin: 8px 0;
    width: 100%;
    text-align: left;
}

.plan-features li {
    padding: 4px 0;
    color: #ddd;
    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
    /* A GRID, NOT A FLEX ROW. As flex, a benefit too long to sit beside its
       tick became its own full-width line and left the tick stranded alone
       above it — visible on any narrow card. Two columns give a hanging
       indent instead: the tick keeps its own column and the text wraps
       under itself. */
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: start;
    column-gap: 10px;
    font-size: 0.96rem;
}

.plan-features li::before {
    content: '✓';
    color: #ffd700;
    font-weight: bold;
}

.feature-badge {
    display: inline-block;
    margin-left: 6px;
    font-size: 0.6rem;
    font-weight: 700;
    letter-spacing: 0.5px;
    color: #0c0c14;
    background: #ffd700;
    padding: 2px 6px;
    border-radius: 999px;
    vertical-align: middle;
    white-space: nowrap;
}

/* THE GIFT'S END DATE. A complimentary pass (a redeemed voucher) says when it
   ends on its own card, quietly, above the feature list. */
.plan-complimentary {
    margin: 0 0 8px;
    color: #cdb6ff;
    font-size: 0.82rem;
    font-weight: 700;
    line-height: 1.35;
}

.plan-note {
    font-size: 0.8rem;
    color: #888;
    margin: 10px 0 0;
    text-align: center;
}

.plan-btn {
    margin-top: auto;
    width: 100%;
    padding: 13px;
    font-size: 1.05rem;
    font-weight: bold;
    border: none;
    border-radius: 8px;
    cursor: pointer;
    background: #444;
    color: #fff;
    transition: all 0.2s;
    /* Keep the label on one line so every card's button is the same height and
       the buttons stay on a shared baseline (a wrapped "Buy for $100" would
       otherwise grow taller than its neighbours). */
    white-space: nowrap;
}

.plan-btn.primary {
    background: linear-gradient(45deg, #ffd700, #ffa502);
    color: #000;
    box-shadow: 0 4px 15px rgba(255, 215, 0, 0.3);
}

.plan-btn:hover {
    transform: scale(1.02);
    filter: brightness(1.1);
}

.plan-btn:focus-visible {
    outline: 3px solid #fff2a8;
    outline-offset: 3px;
}

.plan-btn:disabled {
    opacity: 0.7;
    cursor: wait;
}

/* Subtle full-width legal fine print spanning both columns (EU consumer-rights
   disclosure). Reads as a footer, not a boxed afterthought. The footer is the
   single grid item so its paragraphs stack normally — placing each paragraph as
   its own grid item collapsed them into the same cell and overlapped the text. */
.store-disclosure-footer {
    grid-column: 1 / -1;
    grid-row: 3;
    max-width: 920px;
    margin: 12px auto 0;
    padding-top: 6px;
    border-top: 1px solid rgba(255, 255, 255, 0.08);
}

.store-disclosure {
    margin: 0;
    color: #8d8a7c;
    font-size: 0.76rem;
    line-height: 1.45;
    text-align: center;
}

.store-disclosure-waiver {
    margin-top: 6px;
}

.store-disclosure a {
    color: #c9b878;
    text-decoration: underline;
}

.store-disclosure-links {
    display: inline-flex;
    align-items: center;
    gap: 0 4px;
    white-space: nowrap;
}

/* A SHORT DESKTOP WINDOW — 1400x640 is this repo's default QA viewport, and
   several laptops sit near it. Latin languages cleared the fold by 31px there;
   Japanese did not, because CJK inflates the badge, the title and the button
   line box, and its card ran 423px against English's 403. The buy button then
   sat 21px below the fold in Firefox at the very size everything else is
   checked at. The emblem yields first — it is decoration on a card whose price
   and benefits are the product. */
@media (min-width: 1251px) and (max-height: 760px) {
    .plan-art {
        width: clamp(36px, 6svh, 76px);
        height: clamp(36px, 6svh, 76px);
        margin-bottom: 4px;
    }
    /* The pack art keeps growing into the pass cards' height here — that is
       the spare room the emblem was starving in — but its FLOOR drops with
       the rest of the band, so a pack card can never be the one that sets the
       row and pushes the buy buttons under the fold. Measured at 1400x640:
       the floor is 58px and the pass cards hand the well ~120px anyway. */
    .store-stardust .plan-art {
        width: 100%;
        height: auto;
        min-height: clamp(44px, 9svh, 96px);
    }
    .plan-card {
        padding: 10px 16px;
    }
    .plan-features li {
        padding: 3px 0;
    }
    .plan-features {
        margin: 5px 0;
    }
    /* The store's own title block is ~90px of pure decoration at the top of a
       640px window. It yields before any disclosure does. */
    .store-header {
        padding-top: 10px;
        padding-bottom: 6px;
    }
    .store-title {
        font-size: clamp(1.4rem, 2.4vw, 1.9rem);
        line-height: 1.1;
    }
    .store-subtitle {
        font-size: 0.82rem;
        margin-top: 2px;
    }
    /* The legal footer has to clear the fold too — it now carries the
       renewal disclosure, and Spanish ran 7-10px under it at 1400x640 (and
       Firefox lays this block out ~14px taller than Chromium) while English
       cleared by 31. A disclosure the reader has to scroll for is not a
       disclosure. */
    .store-disclosure-footer {
        margin-top: 6px;
    }
    .store-disclosure {
        font-size: 0.7rem;
        line-height: 1.3;
    }
    /* Five cards were each printing the same tax sentence. One copy, in the
       footer beside the other pre-purchase disclosures, says the same thing
       and gives every card back a row — which is what put the Japanese
       renewal notice back above the fold in Firefox. */
    #store-content .plan-tax-note {
        display: none;
    }
    .store-disclosure .store-tax-note {
        display: inline;
    }
}

/* Stack into a single column once two columns get cramped. Below this the
   stacked layout (full-width sections) is cleaner — and the Stardust packs have
   room for their buttons — than squeezing two columns together. */
@media (max-width: 1250px) {
    .store-content {
        grid-template-columns: 1fr;
        grid-template-rows: none;
        align-content: start;
        row-gap: 22px;
    }

    /* Single column: drop the subgrid and let each section stack normally.
       Cross-column alignment is irrelevant when there is only one column. */
    .store-section {
        display: flex;
        flex-direction: column;
        grid-row: auto;
    }

    .store-disclosure-footer {
        grid-row: auto;
    }

    .store-section--stardust {
        border-left: none;
        border-top: 1px solid rgba(255, 215, 0, 0.12);
        padding-left: 0;
        padding-top: 22px;
    }

    /* THE BALANCE ROW COMES OUT OF THE CORNER HERE TOO. Below 1250px the
       store is one column, and a 340px inset on each side of an 800px window
       left the title 120px to live in: "Moon Store" broke across two lines
       and the subtitle ran to five (measured 800x600, header 179px — 30% of
       the viewport; 700x600 fr, 51%). The phone bands already solved this by
       putting the row in flow under the title; the same answer holds for
       every one-column width, and the header inset only has to clear the
       close button. The phone band below restates this with its safe-area
       paddings. `.store-close` is absolute and unaffected. */
    .store-container {
        --store-chrome-inset: 82px;
    }
    .store-balances {
        position: static;
        order: 1;
        max-width: none;
        justify-content: center;
        /* Room above and below, so the row reads as the header's second
           line and not as a widget dropped onto the first section heading.
           The phone band below tightens this to what a 300px box can pay. */
        margin: 12px 0 4px;
        padding: 0 12px;
    }
    .store-container > * { order: 2; }
    .store-container > .store-header { order: 0; }
    .store-container > .store-balances { order: 1; }
}

/* ============================================================================
 * MOON STORE — mobile polish (phone gates only; desktop is byte-identical).
 * The store already ships a correct sheet: full-bleed .store-container, pinned
 * .store-header (flex:none), single #store-content scroll region, and the
 * @media (max-width:1250px) block above already collapses both phone viewports
 * to one column. These rules only (1) make the sheet dynamic-viewport /
 * safe-area robust and (2) declutter the 845x400 landscape density. Appended
 * last so they win within store.css. NOTE: mobile.css loads AFTER this file and
 * owns .store-stardust layout in the same phone gate, so the stardust-pack
 * rules below are lifted above it via the #store-content id.
 * ==========================================================================*/

/* ---- (A) both phone orientations: viewport / safe-area / legibility -------- */
@media (orientation: portrait) and (max-width: 768px),
       (orientation: landscape) and (max-height: 560px) {
    /* Track the dynamic viewport so a collapsing URL bar can't hide the pinned
       header or the bottom of the scroll region (100% resolves against the
       LARGE viewport of the position:fixed overlay). Explicit height also beats
       the overlay's align-items:stretch. */
    .store-container {
        height: 100dvh;
    }

    .store-header {
        padding-left: max(
            82px,
            calc(var(--mf-safe-left, env(safe-area-inset-left, 0px)) + 82px)
        );
        padding-right: max(
            82px,
            calc(var(--mf-safe-right, env(safe-area-inset-right, 0px)) + 82px)
        );
    }

    /* Keep both pinned header controls clear of notches / rounded corners. */
    .store-close {
        top: calc(var(--mf-safe-top, env(safe-area-inset-top, 0px)) + 10px);
        right: calc(var(--mf-safe-right, env(safe-area-inset-right, 0px)) + 12px);
    }

    /* 68px was 20px of padding around a 46px content box, and every balance of
       1000 or more formats to seven characters (`1.23K ☆`) that needs ~55px.
       The value box clipped the overflow, so the ☆ was sliced down its middle
       on every account that had actually earned anything — while a
       pill-level scrollWidth check stayed green, because the inner box clips
       first. The formatter already bounds this string to a handful of
       characters, so the pill can simply fit its own content. */
    /* THE PILLS COME OUT OF THE CORNER ON A PHONE.

       They are absolutely positioned chrome — `top: 16px` against the
       container, with no relation to anything it might land on. Two separate
       things made that unsurvivable here. The pill's own `top`/`left` used to
       be set in this band and applied to NOTHING, because
       `.store-balances > .store-balance` is `position: static` and the row is
       what positions; so the row kept the bare desktop offset, notch and all.
       And the header on a short landscape box is deliberately squeezed to
       24px to buy card height, so a 36px pill row can never fit inside it
       however the offset is tuned — it lands on whatever the scroll region
       starts with, which in the owner's photograph was "Passes".

       In flow it cannot overlap anything, at any page box, whatever the header
       does next. It costs one row of a region that scrolls, which is the one
       place on this screen where height is not scarce. */
    .store-balances {
        position: static;
        order: 1;
        max-width: none;
        justify-content: center;
        margin: 6px 0 0;
        padding-left: max(12px, calc(var(--mf-safe-left, env(safe-area-inset-left, 0px)) + 12px));
        padding-right: max(12px, calc(var(--mf-safe-right, env(safe-area-inset-right, 0px)) + 12px));
    }

    /* The header keeps the top, the pills follow it, everything else comes
       after. `.store-close` is absolute and unaffected by any of this. */
    .store-container > * { order: 2; }
    .store-container > .store-header { order: 0; }
    .store-container > .store-balances { order: 1; }

    .store-balance {
        min-height: 36px;
        max-width: min(42vw, 132px);
        padding: 0 10px;
        /* Glyph, then number: a hair of air between them. (This was `0` when
           the pill still carried a hidden word beside the figure.) */
        gap: 5px;
    }

    /* A 44px THUMB TARGET ON A 36px PILL. The refill keeps the pills' 36px
       so the row costs a phone no extra height, and grows its hit area by
       4px on every side underneath — 44px tall, the touch floor the rest of
       the phone chrome keeps. Nothing else sits within those 4px: the meter
       is a status with `pointer-events: none`, and the row is centred with
       air on its right. */
    .store-balances > .store-balance-refill {
        position: relative;
    }
    .store-balances > .store-balance-refill::after {
        content: '';
        position: absolute;
        /* -5, not -4: an absolute child is placed against the PADDING box,
           which sits inside the 1px border, so -5px is what reaches 4px past
           the visible edge on every side (measured: -4px left the right and
           bottom probes 1px short). */
        inset: -5px;
        border-radius: 999px;
    }

    .store-balance-value::after {
        font-size: 0.88rem;
    }

    /* Clear the home indicator / gesture bar at the bottom of the scroll. */
    .store-content {
        padding-left: max(
            clamp(20px, 4vw, 56px),
            calc(env(safe-area-inset-left, 0px) + 12px)
        );
        padding-right: max(
            clamp(20px, 4vw, 56px),
            calc(env(safe-area-inset-right, 0px) + 12px)
        );
        padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 16px);
    }

    /* Legible fine print + a slightly larger tap box for the Terms / Privacy
       links (they are the disclosure's tap targets). */
    .store-disclosure {
        font-size: 0.82rem;
    }
    .store-disclosure a {
        display: inline-flex;
        align-items: center;
        min-height: 44px;
        padding: 8px 4px;
        vertical-align: middle;
    }
    .plan-note {
        font-size: 0.82rem;
    }
    /* Decorative uppercase chip — bumped well clear of the near-illegible
       0.6rem; kept compact so the nowrap pill never overflows its feature row. */
    .feature-badge {
        font-size: 0.72rem;
    }
}

/* ---- (B) portrait phones: three equal-width stardust packs ----------------- */
@media (orientation: portrait) and (max-width: 768px) {
    /* mobile.css wraps the packs here; without this the two top cards compute to
       ~168px while the lone third caps at 220px (asymmetric 2+1). Force every
       pack to exactly half-width so the orphan matches its siblings and centers
       (justify-content:center is already on .store-stardust). The #store-content
       id lifts this above mobile.css's .store-stardust .plan-card authority. */
    #store-content .store-stardust .plan-card {
        flex: 0 1 calc(50% - 7px);
        max-width: none;
    }

    /* THE SAME ESCAPE THE LANDSCAPE BAND ALREADY PREVENTS. `.plan-btn` is
       `white-space: nowrap` with `width: 100%`, inside a card that now carries
       `min-width: 0` and a half-width basis — so the card can be sized below
       the button's own label and the label renders outside the card's border.
       Measured (DOM forced open) at 393x659 fr: 21.5px past the card edge;
       360x640 fr: 38px; 320x568 fr: 58px; 320x568 en: 8px.

       NOT REACHABLE TODAY, and said plainly rather than left implied: the
       orientation guard blocks every portrait viewport at or under 1024px and
       `Store.open()` refuses outright there — `store.spec.js` asserts the modal
       stays hidden with zero profile reads. This band styles a surface no
       player can currently see. It is fixed anyway because the whole band is,
       and because the day portrait becomes reachable is not the day to
       rediscover this. */
    .plan-btn {
        white-space: normal;
    }
}

/* ---- (C) landscape phones (short viewport): reclaim the chrome that ate the
   top ~35% of a 400px screen, and halve per-card height ---------------------- */
@media (orientation: landscape) and (max-height: 560px) {
    /* The section TITLES carry the meaning on a short screen; the taglines and
       the store subtitle are redundant and cost precious height. */
    .store-header {
        min-height: 48px;
        padding-top: 0;
        padding-bottom: 0;
        display: flex;
        align-items: center;
        justify-content: center;
    }
    .store-close {
        top: calc(var(--mf-safe-top, env(safe-area-inset-top, 0px)) + 2px);
    }
    /* Band (A) takes the row out of the corner and into flow; there is no
       offset left to tune here. */
    .store-title {
        font-size: clamp(1.35rem, 4vw, 1.7rem);
        line-height: 1.05;
    }
    .store-subtitle {
        display: none;
    }
    /* THE PERIOD CONTROL SITS BESIDE THE TITLE HERE, NOT UNDER IT.
       Stacked, it cost ~52px of a 300px page box and pushed both Subscribe
       buttons off the bottom of the screen — the exact invariant the card
       grid below states it protects. On one line it costs nothing: the row
       was already as tall as the control. */
    .store-section__head {
        grid-row: 2;
        padding-bottom: 0;
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 10px;
        min-width: 0;
    }
    .store-section__title {
        min-width: 0;
    }
    .store-section__title {
        font-size: clamp(0.95rem, 2.8vw, 1.2rem);
        line-height: 1.1;
    }
    .store-section__sub {
        display: none;
    }
    /* A landscape phone spends its height on the cards, so the note moves
       beside the label instead of under it — but the touch target stays the
       full 44px the buy buttons use. The art row below is `minmax(0, 1fr)`
       with `min-height: 0`, so it is what yields the space, exactly as the
       card layout already intends: content yields art, never actions. */
    .store-period {
        margin: 0;
        padding: 2px;
        flex: 0 0 auto;
    }
    .store-period__option {
        flex-direction: row;
        gap: 5px;
        min-height: var(--mf-touch, 44px);
        padding: 2px 10px;
        font-size: clamp(0.7rem, 2.2vw, 0.85rem);
    }
    .store-period__note {
        font-size: 0.62rem;
    }
    .store-content {
        /* max-width:1250 stacks the two product families, which puts Stardust
           entirely below the fold on every landscape phone. A short landscape
           viewport has horizontal room, so restore two equal, bounded panes and
           keep the legal disclosure as the shared row underneath. */
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
        /* THE CARD ROW IS FLEXIBLE, NOT `auto`, AND THAT IS LOAD-BEARING.
           As `auto` it grew to whatever the cards wanted, so the card never
           had a bounded height and its own `minmax(0, 1fr)` art row never had
           anything to yield — the art kept its 41px while the Subscribe button
           was pushed off the bottom of the screen. On a 667x275 page box the
           button sat at y=309 on `main` and y=358 after the period control was
           added; measured in all three engines.

           The card row is now the ONLY flexible track. Sharing `1fr` with the
           two centering spacers gave it a third of the leftover — 34px on a
           667x275 box, with the whole card spilling out of it — so the spacers
           collapse to nothing and the cards take the remaining height instead.
           They lose their vertical centring and stretch to fill, which is what
           `align-content: stretch` on this grid already intends, and the card's
           own art row is what absorbs the difference. `store.css` already
           promised this: "content yields art, never actions". This is what
           makes the promise true. */
        /* `min-content` FLOOR, not zero.

           The section row was `minmax(0, 1fr)`, so when a card genuinely needed
           more height than the box had, the track was clamped and the cards
           painted straight over the legal footer pinned beneath them — 43.8px
           of overlap at 667x275 and 63.4px at 932x232, measured. A disclosure
           covered by a Buy button is not a layout that "still fits".

           With a `min-content` floor the track grows instead, the footer is
           pushed down, and `#store-content` scrolls — which is exactly what the
           owner sanctioned for this part of the screen: "that part, to be
           honest, it's even okay to require scrolling for". Where the content
           already fits, `1fr` still wins and nothing moves. */
        grid-template-rows: 0 auto minmax(min-content, 1fr) 0 auto;
        column-gap: 12px;
        padding-top: 2px;
        padding-bottom: env(safe-area-inset-bottom, 0px);
        row-gap: 0;
        align-content: stretch;
    }
    .store-section {
        display: grid;
        grid-template-rows: subgrid;
        grid-row: 1 / 5;
        align-self: stretch;
        min-width: 0;
        min-height: 0;
    }
    .store-plans,
    .store-stardust {
        grid-row: 3;
        min-height: 0;
        align-items: stretch;
    }
    /* Without `min-height: 0` a grid item refuses to shrink below its content,
       which would silently undo the bounded row above. */
    .store-plans .plan-card,
    .store-stardust .plan-card {
        min-height: 0;
    }
    /* THE STARDUST COLUMN REFUSES TO SHRINK BELOW ITS TEXT.

       Three `min-height: 0` declarations in this band — on the section, on the
       row, and on the card — exist so the store can be squeezed into a short
       box, and together they let the cards be sized smaller than their own
       content and painted over the legal footer beneath. The Passes column
       keeps that behaviour: its cards have an artwork row that genuinely can
       absorb the difference (and at 932x300 it absorbs 7px that would
       otherwise push every buy button 3px under the fold — measured in all
       three engines when this rule was tried on the pass column too). The
       Stardust column no longer does, because what it was absorbing was the
       text.

       Where the content fits, this changes nothing at all. Where it does not,
       the column grows, the footer is pushed below it, and `#store-content`
       scrolls — the outcome the owner sanctioned: "that part, to be honest,
       it's even okay to require scrolling for". */
    .store-section--stardust,
    #store-content .store-stardust,
    #store-content .store-stardust .plan-card {
        min-height: auto;
    }

    /* A STARVED PASS EMBLEM HIDES INSTEAD OF SHRINKING TO A DOT.

       Chrome and Firefox size the section track from the pass cards' content
       and scroll; WebKit honours the shrink above and hands the emblem's
       flexible row whatever is left, which on the Manga card (one more
       benefit line) at 852x344 and 915x336 was 12-27px — a gold dot beside a
       full-size Storyteller emblem, on the owner's own engine. The 305px rule
       below already says what a starved emblem should do ("a 10px dot reads
       as broken"); this says it from the well's ACTUAL height rather than the
       page box's, so it fires exactly when the row is starved and never
       otherwise. `visibility`, not `display`: the well keeps its box, so the
       card's rows do not move when the picture lands. */
    .store-plans .plan-card:not(.plan-card--active):not(.plan-card--status) .plan-art {
        container-type: size;
    }
    @container (max-height: 27px) {
        .plan-icon {
            visibility: hidden;
        }
    }

    .store-section--stardust {
        border-top: none;
        border-left: 1px solid rgba(255, 215, 0, 0.12);
        padding-top: 0;
        padding-left: 12px;
    }

    /* Preserve every offer, benefit, price, and action while spending the
       short viewport on information rather than decorative whitespace. */
    .plan-card {
        padding: 4px 8px;
        border-radius: 10px;
    }
    /* THE PRODUCT ART SITS BELOW THE TEXT, IN THE LEFTOVER SPACE — round
       three of this layout, now matching the owner's words exactly: "place
       pictures below text, in empty space". The side-mounted column from
       round two widened the text lanes and pushed buy buttons off the
       owner's screen. Every card is now a single column: badge strip, text
       rows, then the ART in the one FLEXIBLE row (the empty space), then the
       button pinned last. The art scales to whatever height is left —
       shrinking all the way out on the tightest boxes — so the buy button
       can NEVER leave the screen: content yields art, never actions.
       Row 1 stays the reserved badge strip shared by every card so badged
       and badgeless siblings keep one geometry. Status/active cards keep
       their flex column. */
    .store-plans .plan-card:not(.plan-card--active):not(.plan-card--status) {
        display: grid;
        grid-template-columns: minmax(0, 1fr);
        grid-template-rows: minmax(17px, auto) auto auto auto auto auto minmax(0, 1fr) auto;
        row-gap: 0;
        align-content: stretch;
        align-items: center;
    }
    /* A STARDUST CARD NO LONGER SIZES ITS OWN ROWS — see the row grid further
       down this band. It kept `display: grid` and the single column; the eight
       tracks come from its parent so all three cards share them. */
    #store-content .store-stardust .plan-card {
        display: grid;
        grid-template-columns: minmax(0, 1fr);
        align-content: stretch;
        align-items: center;
    }

    .store-plans .plan-card:not(.plan-card--active):not(.plan-card--status) .plan-badge,
    #store-content .store-stardust .plan-card .plan-badge {
        position: static;
        grid-row: 1;
        justify-self: center;
        align-self: start;
    }
    .store-plans .plan-card:not(.plan-card--active):not(.plan-card--status) .plan-title,
    #store-content .store-stardust .plan-card .plan-title {
        grid-row: 2;
        /* TOP OF THE TRACK, NOT ITS MIDDLE. The track is as tall as the longest
           title needs, and on the narrowest card column "Chest of Stardust"
           takes three lines where its two siblings take two. Centred, the two
           short ones were pushed 6.8px down and the three titles started at
           three different heights — the owner's "we should have titles have the
           same y coordinate". Aligned to the start, they share one baseline
           whatever any one of them does with its line count. */
        align-self: start;
    }
    .store-plans .plan-card:not(.plan-card--active):not(.plan-card--status) .plan-price,
    #store-content .store-stardust .plan-card .plan-price {
        grid-row: 3;
        justify-content: center;
    }
    .store-plans .plan-card:not(.plan-card--active):not(.plan-card--status) .plan-tax-note {
        grid-row: 4;
    }
    .store-plans .plan-card:not(.plan-card--active):not(.plan-card--status) .plan-saving {
        grid-row: 5;
    }
    .store-plans .plan-card:not(.plan-card--active):not(.plan-card--status) .plan-features {
        grid-row: 6;
        align-self: start;
        width: 100%;
    }
    #store-content .store-stardust .plan-card .plan-price-subtitle {
        grid-row: 4;
    }
    #store-content .store-stardust .plan-card .plan-tax-note {
        grid-row: 5;
        align-self: start;
    }
    /* THE ART WELL IS THE SAME SIZE BEFORE AND AFTER THE IMAGE LANDS.

       This used to be the bare image: `height: 100%` of the flexible row,
       capped at `clamp(0px, 15svh, 96px)` and `max-width: 60%`. An image
       with no bytes yet has no intrinsic size, so it contributed nothing to
       the row's min-content; the arrived image contributed its capped
       height — and because the section row is `minmax(min-content, 1fr)`,
       every card grew by that amount the moment the art loaded. The buy
       buttons moved 30-52px under the player's thumb.

       The well is sized by the SAME cap, as a definite box: 60% of the
       card, or 15svh, or 96px, whichever is smallest, square. That is
       exactly the size the loaded image measured at every page box (45px at
       932x300, 51.6 at 852x344, 34.9 at 667x275, 50.4 at 915x336) — the
       owner asked for the phone art not to shrink — and it is the same
       number before the first byte, so nothing moves. The cap still yields:
       `max-height: 100%` lets a row that is genuinely shorter clip the box,
       and `object-fit: contain` keeps the picture whole inside it. */
    .store-plans .plan-card:not(.plan-card--active):not(.plan-card--status) .plan-art,
    #store-content .store-stardust .plan-card .plan-art {
        grid-row: 7;
        justify-self: center;
        align-self: center;
        flex: none;
        width: min(60%, 15svh, 96px);
        height: auto;
        aspect-ratio: 1 / 1;
        max-height: 100%;
        min-height: 0;
        margin: 0;
    }
    .store-plans .plan-card:not(.plan-card--active):not(.plan-card--status) .plan-btn,
    #store-content .store-stardust .plan-card .plan-card .plan-btn,
    #store-content .store-stardust .plan-card > .plan-btn {
        grid-row: 8;
        align-self: end;
        width: 100%;
    }
    /* A PASS CARD NOW HAS TWO ACTIONS, monthly and yearly, wrapped in
       `.plan-actions`. This grid assigns every child an explicit row, so an
       UNASSIGNED wrapper auto-placed into row 1 and threw both buttons to the
       top of the card while the rest overflowed past its bottom edge. The
       wrapper takes the row the single button used to own, and the buttons
       inside it stop being grid items — so the `grid-row: 8` above no longer
       applies to them and must not be allowed to. */
    .store-plans .plan-card:not(.plan-card--active):not(.plan-card--status) .plan-actions {
        grid-row: 8;
        align-self: end;
        width: 100%;
        display: flex;
        flex-direction: row;
        flex-wrap: nowrap;
        gap: 4px;
        margin-top: 0;
    }
    .store-plans .plan-card:not(.plan-card--active):not(.plan-card--status) .plan-actions .plan-btn {
        /* Share the row evenly and be allowed to shrink: two 44px targets side
           by side is what a landscape phone has width for, and stacking them
           would cost a row this viewport does not have. */
        flex: 1 1 0;
        min-width: 0;
        width: auto;
        align-self: stretch;
        padding-inline: 3px;
        font-size: clamp(0.66rem, 1.5vw, 0.82rem);
    }
    /* Tame the single "you're subscribed" emblem on a short viewport too. */
    .plan-card--active .plan-art {
        width: clamp(56px, 9svh, 92px);
        height: clamp(56px, 9svh, 92px);
    }
    .plan-card--active > p:not(.plan-note) {
        margin: 10px 0 12px !important;
    }
    .plan-title {
        font-size: clamp(0.86rem, 2.3vw, 1.05rem);
        line-height: 1.12;
        min-height: 1.12em;
        display: flex;
        align-items: center;
        justify-content: center;
        margin-bottom: 2px;
    }
    .plan-price {
        font-size: clamp(1.3rem, 4vw, 1.75rem);
        line-height: 1;
        margin-bottom: 2px;
    }
    .plan-price span {
        font-size: clamp(0.68rem, 1.5vw, 0.78rem);
        /* NOT 1. `line-height: 1` clamps the line BOX, but a CJK face reports a
           content area taller than 1em, so Japanese "/月" overflowed its box by
           3px at 768x400 while Latin fit. The suffix is one short run, so the
           extra couple of pixels cost nothing and every locale measures the
           same. Same font-dependent-`normal` trap as .feedback-link-label. */
        line-height: 1.35;
        margin-top: 4px;
        margin-left: 3px;
    }
    .plan-saving {
        /* One shared full-width line (it cannot wrap there), so Monthly's
           blank spacer and Yearly's saving line always measure the same. */
        min-height: 14px;
        margin-bottom: 2px;
        font-size: clamp(0.68rem, 1.5vw, 0.78rem);
        line-height: 1.05;
    }
    .plan-features {
        margin: 1px 0;
    }
    .plan-features li {
        flex-wrap: wrap;
        column-gap: 3px;
        padding: 1px 0;
        font-size: clamp(0.72rem, 1.3vw, 0.78rem);
        line-height: 1.03;
    }
    .plan-features li::before {
        margin-right: 2px;
    }
    .feature-badge {
        margin-left: 2px;
        padding: 1px 4px;
        font-size: clamp(0.68rem, 1.3vw, 0.72rem);
        line-height: 1.05;
    }
    .plan-btn {
        padding: 5px 4px;
        min-height: 44px;
        font-size: clamp(0.75rem, 1.8vw, 0.9rem);
        line-height: 1.05;
        white-space: normal;
    }
    .plan-badge {
        max-width: 100%;
        padding: 3px 8px;
        font-size: 0.68rem;
        line-height: 1;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        box-sizing: border-box;
    }
    /* The active card keeps the base absolute badge; pin it inside the
       trimmed landscape padding. */
    .plan-card--active .plan-badge {
        top: 4px;
        right: 2px;
    }

    /* Fixed product counts are better expressed as grids here: two Moon Pass
       choices and three Stardust packs stay aligned and never wrap into a hidden
       second row. A one-card subscription/status state spans the Pass pane. */
    .store-plans {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 8px;
    }
    .store-plans .plan-card {
        width: 100%;
        max-width: none;
        min-width: 0;
    }
    .store-plans .plan-card:only-child,
    .store-plans .plan-card--active,
    .store-plans .plan-card--status {
        grid-column: 1 / -1;
    }

    /* The loading / error status card reserved a fixed ~230px for a spinner or
       one line of text — collapse it to its content on a short screen. */
    .plan-card--status {
        min-height: 0;
        padding: 18px;
    }

    /* Keep the three packs on one compact nowrap row (portrait uses 2+1). The
       #store-content id lifts these above the mobile.css wrap rules that load
       after this file. */
    /* THE THREE STARDUST CARDS SHARE ONE SET OF TRACKS.

       Every card sized its own rows, and row 1 is the badge strip. Only the
       middle card carries POPULAR, so only its row 1 grew to the badge's
       height — and every row beneath it, title, amount, USD and artwork alike,
       sat 11px lower than the same row in its two siblings, at every landscape
       page box. On the tightest boxes the strip was shorter than the badge,
       which then overflowed its own row and printed on top of the title.

       The owner, with a photograph: "It's not aligned at all. We should have
       titles have the same y coordinate. The sums ... should also have the same
       height. The USD should have the same height."

       `subgrid` is that sentence expressed as layout. The tracks live here, on
       the row; each card adopts them; a track is as tall as the tallest card
       needs. So the badge reserves its strip in all three at once, and a title
       that wraps to two lines in one card cannot push that card out of step
       with the others. It is not a set of tuned offsets that the next currency
       symbol or translation would quietly break.

       THIS RULE MUST KEEP OWNING THE TRACKS. Two per-card rules used to set
       `grid-template-rows` on `.plan-card` as well, and being later in the file
       they took the subgrid straight back off — the computed rows read
       `0 35.1 25 16.5 ...` per card against the row's `17 0 0 ... 92.3`, which
       looks exactly like "the fix had no effect". They are split now: the
       per-card template belongs to `.store-plans` alone. */
    #store-content .store-stardust {
        display: grid;
        grid-template-columns: repeat(3, minmax(0, 1fr));
        /* `min-content` on the three TEXT rows, not `auto`.

           The row is a grid item with a definite height, so `auto` tracks are
           free to shrink below their content when the total does not fit — and
           at 667x275 they did: the title needed 54.8px and was handed 30.8,
           so it overflowed its own track and printed across the amount beneath
           it. `min-content` refuses that squeeze. The artwork row below is
           `minmax(0, 1fr)` and collapses to nothing first, so text only wins
           this argument after the art has already given up everything it has;
           past that the section overflows into the store's own scroll region,
           which the owner sanctioned for exactly this: "that part, to be
           honest, it's even okay to require scrolling for". */
        grid-template-rows:
            minmax(17px, min-content) min-content min-content min-content auto auto minmax(0, 1fr) auto;
        /* The separation the owner asked for: "it's not enough to just
           eliminate overlapping. Ideally we should also have some minimal
           padding ... don't do anything crazy in terms of padding". Three
           pixels reads as a gap without costing a row on a 232px page box. */
        row-gap: 3px;
        column-gap: 8px;
        align-items: stretch;
    }
    #store-content .store-stardust .plan-card {
        grid-row: 1 / -1;
        grid-template-rows: subgrid;
        /* NO `gap` HERE. A subgrid's own gap REPLACES the one it inherits, so
           declaring `row-gap: 0` deleted the 3px the row had just set and the
           artwork went straight back to touching the button. */
    }
    #store-content .store-stardust .plan-card {
        width: 100%;
        min-width: 0;
        max-width: none;
        padding: 6px 5px;
    }
    #store-content .store-stardust .plan-btn {
        padding-inline: 4px;
        font-size: clamp(0.75rem, 1.8vw, 0.95rem);
        line-height: 1.1;
    }
    #store-content .store-stardust .plan-price {
        flex-direction: column;
        align-items: center;
        justify-content: center;
        max-width: 100%;
        white-space: nowrap;
        /* Sized for the beside-the-art column: at the old 1.75rem cap "2.5K"
           overflowed the narrowest pack card by a pixel. */
        font-size: clamp(1.05rem, 2.6vw, 1.5rem);
    }
    #store-content .store-stardust .plan-price .stardust-glyph {
        /* The amount stands alone on these compact cards, as it always has
           here: the unit word was hidden because CJK locales could not fit it
           beside the art (ja overflowed the card by a pixel at 768x400), and
           the glyph that replaced it does not fit either — "2.5K" at this
           band's price size is 53-58px in a 63-73px content box, and the
           glyph needs another 18. The card title ("Bag of Stardust") and the
           section title carry the unit; the spoken text still says it. */
        display: none;
    }
    #store-content .store-stardust .plan-price-subtitle {
        margin-bottom: 4px;
        font-size: clamp(0.68rem, 1.5vw, 0.78rem);
        line-height: 1;
    }
    #store-content .plan-tax-note {
        margin-bottom: 1px;
        font-size: clamp(0.58rem, 1.2vw, 0.66rem);
        line-height: 1.1;
    }
    /* Single subscription/status states fill their whole product pane. */
    #store-content .plan-card--active,
    #store-content .plan-card--status {
        max-width: 680px;
    }
    /* Drop the reserved two-line title box so packs collapse to real content. */
    .store-stardust .plan-title {
        display: grid;
        place-items: center;
        width: 100%;
        min-width: 0;
        min-height: 2.3em;
        font-size: clamp(0.78rem, 1.8vw, 0.9rem);
        line-height: 1.1;
        /* A 58-83px card cannot hold "Stardust", "Sternenstaub" or
           "poussière" on one line, and `anywhere` alone cut them wherever
           the column ran out: "Stardus / t", "Sternenst / aub". The soft
           hyphens in ui.json are live here and only here, so the break lands
           on a syllable and shows its hyphen; `anywhere` stays as the last
           resort for a word with no hyphen in it. `store.spec.js` measures
           every Latin title per character at each landscape page box. */
        hyphens: manual;
        overflow-wrap: anywhere;
    }
    .store-disclosure-footer {
        grid-row: 5;
        align-self: end;
        width: 100%;
        margin-top: 0;
        padding-top: 0;
    }
    .store-disclosure {
        display: flex;
        align-items: center;
        justify-content: center;
        flex-wrap: wrap;
        column-gap: 4px;
        line-height: 1.15;
    }

    /* Under ~340px of page box the head's 44px target is the difference
       between a visible Subscribe button and one below the fold. The buy
       buttons keep their full 44px; this secondary control yields first. */
    @media (max-height: 340px) {
        .store-period__option {
            min-height: 34px;
            padding-block: 0;
        }
        .store-period {
            padding: 1px;
        }
    }
    @media (max-height: 305px) {
        .store-period__option {
            min-height: 30px;
        }
        /* The pass emblem sits in the card's one flexible row, so on the
           shortest boxes it is handed whatever is left — which was about ten
           pixels, and rendered as a stray dot between the benefits and the
           button. A card with no art reads as deliberate; a 10px dot reads as
           broken. The Stardust cards keep theirs: their content is short
           enough that the row still has real height. */
        .store-plans .plan-card:not(.plan-card--active):not(.plan-card--status) .plan-art {
            display: none;
        }
    }

    /* THE SHORTEST PAGE BOXES: reclaim height without removing information.
       Measured at 844x280 / 844x288 / 932x300 / 667x275, where the card wanted
       19-82px more than its row could give and the Subscribe button was pushed
       off the bottom of the screen in all three engines. Nothing here hides a
       benefit, a price or an action: the tax notice MOVES to the shared footer
       (it is identical on all five cards, so five copies were the waste), the
       empty badge strip and the empty saving line stop reserving space they do
       not use, the store's own decorative title yields, and the type tightens.

       THESE TWO BLOCKS MUST STAY LAST in the landscape section. An earlier
       `.plan-features li` further down the file has the same specificity, so
       placed above it these rules lost on source order and the squeeze did
       nothing — the measurements did not move and it read as "the fix had no
       effect" rather than "the fix never applied". */
    @media (max-height: 400px) {
        .store-plans .plan-card:not(.plan-card--active):not(.plan-card--status) {
            grid-template-rows: minmax(0, auto) auto auto auto auto auto minmax(0, 1fr) auto;
        }
        /* The Stardust tracks live on the row, so the squeeze is applied there
           rather than per card — applying it here would silently take the
           subgrid away again, which is exactly how the first attempt failed. */
        #store-content .store-stardust {
            grid-template-rows:
                min-content min-content min-content min-content auto auto minmax(0, 1fr) auto;
        }
        .store-plans .plan-card:not(.plan-card--active):not(.plan-card--status) .plan-badge,
        #store-content .store-stardust .plan-card .plan-badge {
            font-size: 0.56rem;
            padding: 1px 6px;
        }
        /* One notice for the whole store instead of one per card. */
        #store-content .plan-tax-note {
            display: none;
        }
        .store-disclosure .store-tax-note {
            display: inline;
        }
        .plan-price {
            font-size: clamp(1.1rem, 3.4vw, 1.5rem);
            line-height: 1.05;
        }
        .plan-features li {
            line-height: 1.2;
            padding-block: 1px;
            font-size: clamp(0.66rem, 1.9vw, 0.8rem);
        }
        .store-header {
            min-height: 32px;
        }
        .store-title {
            font-size: clamp(1rem, 3vw, 1.25rem);
        }
        .plan-title {
            font-size: clamp(0.8rem, 2.4vw, 0.98rem);
            line-height: 1.1;
        }
        .plan-saving {
            font-size: 0.64rem;
            line-height: 1.15;
            margin-bottom: 0;
            min-height: 0;
        }
        /* The separators between benefits are decoration, and on this box they
           and their surrounding margins are the last ~12px between a Subscribe
           button inside its card and one hanging below it. */
        .plan-features {
            margin: 2px 0;
        }
        .plan-features li {
            border-bottom: none;
        }
        /* An empty saving line reserved a whole row for a non-breaking space. */
        .plan-saving.is-empty {
            display: none;
        }
        /* THE LEGAL FOOTER ATE 70px OF A 275px PAGE BOX — a quarter of the
           screen, and the reason the Subscribe buttons could not fit however
           much the cards were tightened. It is a flex row whose two spans wrap
           as units, so a 155-character sentence broke across FIVE lines in
           655px of width. As flowing text it takes two. Not one word of it is
           removed; it simply stops being laid out as if it were a toolbar. */
        .store-disclosure {
            display: block;
            font-size: 0.58rem;
            line-height: 1.15;
        }
        .store-disclosure-copy {
            display: inline;
        }
        /* The two legal links are touch targets and stay tappable, but a 44px
           line box inside a 275px page box is the single most expensive
           element on the screen. 34px is still a comfortable target for a
           secondary text link, and the alternative was the Subscribe button
           below the fold. */
        .store-disclosure a {
            min-height: 24px;
            padding: 2px 3px;
        }
        .store-header {
            min-height: 24px;
        }
        .store-section__head {
            gap: 8px;
        }
        /* THE TWO COLUMNS ARE NOT EQUAL. A pass card carries three wrapping
           benefit lines; a Stardust card carries an amount and a price. Split
           evenly, every bullet broke across three lines and the cards wanted
           30-50px more than the row could give. The width goes where the text
           is, which costs the Stardust column nothing it was using. */
        .store-content {
            grid-template-columns: minmax(0, 1.26fr) minmax(0, 0.74fr);
        }
    }

    /* The narrowest supported page boxes put two cards in a ~330px column, so
       every bullet wraps twice. This is the last squeeze before anything would
       have to be hidden, which is why it is scoped to those boxes rather than
       dragging the roomier phones down with it. */
    @media (max-height: 290px), (max-width: 700px) {
        .plan-features li {
            font-size: clamp(0.6rem, 1.8vw, 0.72rem);
            line-height: 1.15;
            padding-block: 0;
        }
        .plan-price {
            font-size: clamp(1rem, 3vw, 1.3rem);
        }
        .plan-btn {
            font-size: clamp(0.7rem, 1.7vw, 0.82rem);
        }
        /* The last 5px between a Subscribe button inside its card and one
           hanging 5px below it, on the single narrowest supported page box. */
        .plan-title {
            font-size: clamp(0.74rem, 2.2vw, 0.9rem);
            line-height: 1.05;
        }
        .store-plans .plan-card:not(.plan-card--active):not(.plan-card--status) .plan-badge {
            font-size: 0.5rem;
            padding: 0 5px;
        }
        .plan-saving {
            font-size: 0.56rem;
            line-height: 1.1;
        }
        /* CJK line boxes are taller than Latin ones at the same font size, so
           the Japanese card wanted a few more pixels than the English one it
           was tuned against. `overflow: hidden` on the card is the backstop:
           the layout is proven to fit in all eight languages, and if a future
           translation is longer still it clips rather than painting a button
           outside its own frame. */
        .plan-features li {
            line-height: 1.05;
            padding-block: 0;
        }
        .store-plans .plan-card:not(.plan-card--active):not(.plan-card--status) {
            overflow: hidden;
        }
        .store-content {
            padding-top: 0;
        }
        .plan-card {
            padding: 2px 6px;
        }
        .store-plans {
            gap: 6px;
        }
        /* Column gap only: the shorthand would reset `row-gap` and take the
           separation between the Stardust rows with it. */
        .store-stardust {
            column-gap: 6px;
        }
        .store-content {
            column-gap: 8px;
            padding-inline: 6px;
        }
        /* The tick's gutter costs a character of width on every wrapped line,
           and there are nine of them on this box. */
        .plan-features li {
            column-gap: 5px;
        }
    }
}

/* Two buttons per plan card now — monthly and yearly for the same tier. They
   sit on one row and wrap to two on a narrow card, so a tier never loses its
   annual option to a viewport. */
.plan-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    width: 100%;
    margin-top: auto;
}

.plan-actions .plan-btn {
    flex: 1 1 8rem;
    min-width: 0;
}
