/* ==========================================================================
   Starbucks SG 30th Anniversary — base stylesheet
   Colors/fonts sourced from .planning/DESIGN-TOKENS.md (extracted from the
   .ai source file) and FA/Export/R2 (layout + approved copy). Type scale
   follows .planning/TYPOGRAPHY.md §4's default fluid clamp() scale.
   ========================================================================== */

@font-face {
    font-family: "SoDoSans";
    src: url("../fonts/SoDoSans-Light.otf") format("opentype");
    font-weight: 300;
    font-style: normal;
    font-display: swap;
}
@font-face {
    font-family: "SoDoSans";
    src: url("../fonts/SoDoSans-LightItalic.otf") format("opentype");
    font-weight: 300;
    font-style: italic;
    font-display: swap;
}
@font-face {
    font-family: "SoDoSans";
    src: url("../fonts/SoDoSans-Regular.otf") format("opentype");
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}
@font-face {
    font-family: "SoDoSans";
    src: url("../fonts/SoDoSans-SemiBold.otf") format("opentype");
    font-weight: 600;
    font-style: normal;
    font-display: swap;
}
@font-face {
    font-family: "SoDoSans";
    src: url("../fonts/SoDoSans-Bold.otf") format("opentype");
    font-weight: 700;
    font-style: normal;
    font-display: swap;
}
@font-face {
    font-family: "StarbucksSerif";
    src: url("../fonts/StarbucksSerif-Regular.ttf") format("truetype");
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}
@font-face {
    font-family: "StarbucksSerif";
    src: url("../fonts/StarbucksSerif-Italic.ttf") format("truetype");
    font-weight: 400;
    font-style: italic;
    font-display: swap;
}

:root {
    /* Brand colors — exact hex values extracted from the .ai file (see DESIGN-TOKENS.md) */
    --color-green: #00A862;
    --color-green-dark: #006241;
    --color-green-frame: #006837; /* reward-card frame — a third, distinct green from the .ai source, see success.php */
    --color-cream: #F2ECE1;
    --color-grey-light: #ECECEE;
    --color-grey-border: #E1DFDD;
    --color-ink: #1A1A1A;
    --color-white: #FFFFFF;

    --font-serif: "StarbucksSerif", Georgia, serif;
    --font-sans: "SoDoSans", -apple-system, "Helvetica Neue", Arial, sans-serif;

    /* Fluid type scale — .planning/TYPOGRAPHY.md §4 defaults */
    --text-xs:   clamp(0.75rem, 0.7rem  + 0.25vw, 0.875rem);
    --text-sm:   clamp(0.875rem, 0.85rem + 0.13vw, 1rem);
    --text-base: clamp(1rem,     0.96rem + 0.22vw, 1.125rem);
    --text-lg:   clamp(1.125rem, 1.05rem + 0.38vw, 1.25rem);
    --text-xl:   clamp(1.25rem,  1.1rem  + 0.75vw, 1.5rem);
    --text-2xl:  clamp(1.5rem,   1.25rem + 1.25vw, 2rem);
    --text-3xl:  clamp(1.875rem, 1.5rem  + 1.88vw, 2.625rem);
    --text-4xl:  clamp(2.25rem,  1.75rem + 2.5vw,  3.5rem);

    /* Hand-tuned to the exact sizes extracted from the .ai file for the Landing Page —
       see .planning/DESIGN-TOKENS.md's Typography table. Formula: TYPOGRAPHY.md §3,
       min/max viewport 320–600px. "Caption/label" (9.1px) and "Micro" (8.2–8.4px) in that
       table fall below TYPOGRAPHY.md §4's 12px accessibility floor, so --text-landing-caption
       is clamped at 0.75rem (12px) instead of the literal extracted value — a deliberate
       deviation from the design file, not an oversight.
       Element mapping is best-fit, not position-verified (the extraction clusters font/size
       pairs per page, not per element) — flag if any of these look wrong once you compare
       against R2 directly. */
    --text-landing-hero:  clamp(2rem,    1.314rem + 3.43vw, 2.6rem);    /* ~38.5px target — .hero__heading */
    --text-landing-h1alt: clamp(1.5rem,  1.071rem + 2.14vw, 1.875rem); /* ~27px target — .wall-of-memories__heading-row h2, from 338px viewport up (see media query) */
    --text-landing-h2:    clamp(1.375rem, 0.946rem + 2.14vw, 1.75rem); /* ~25px target — section h2s */
    --text-landing-body-lg: clamp(1rem, 0.857rem + 0.71vw, 1.125rem); /* ~17.3px target — lead subheading/body */
    --text-landing-btn: clamp(0.8125rem, 0.78rem + 0.34vw, 0.9075rem); /* ~14.5px target — "Share Your Moments Now" buttons */

    --space-1: 0.5rem;
    --space-2: 1rem;
    --space-3: 1.5rem;
    --space-4: 2.5rem;
    --space-5: 4rem;

    --max-width: 28rem; /* mobile-first content column, matches the 375px design width */
    /* Session 26 (cont'd) — the home page's hero/cta-green/rewards-cream copy blocks each had
       their own bespoke max-width (15.979rem-22rem), tuned independently over several sessions
       for each block's own text — client flagged the resulting inconsistent column widths as
       "looks weird." Tested against every affected heading/paragraph's real text across
       320-480px: 20rem is the narrowest value that still gives every element a stable line
       count at every width (rewards-cream h2 needs >=20rem for a consistent 3-line wrap;
       cta-green h2 needs >=19rem to avoid a 3rd line at 480px). */
    --copy-max-width: 20rem;
}

*, *::before, *::after { box-sizing: border-box; }

html {
    font-size: clamp(1em, 1em, 1.25em);
    -webkit-text-size-adjust: 100%;
}

body {
    margin: 0;
    font-family: var(--font-sans);
    font-size: var(--text-base);
    line-height: 1.6;
    color: var(--color-ink);
    background: var(--color-white);
}

h1, h2, h3 { font-family: var(--font-serif); font-weight: 400; margin: 0 0 var(--space-2); }
h1 { font-size: var(--text-4xl); line-height: 1.15; }
h2 { font-size: var(--text-2xl); line-height: 1.25; }
h3 { font-size: var(--text-xl); line-height: 1.3; }
p  { margin: 0 0 var(--space-2); }
ul { margin: 0; padding: 0; list-style: none; }
img { max-width: 100%; height: auto; display: block; }
a { color: inherit; }
/* Visually hidden but still readable by screen readers / present in the DOM for SEO —
   for real headings/labels whose visible text is baked into a flattened image instead. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

.btn {
    display: inline-block;
    width: 100%;
    max-width: 22rem;
    text-align: center;
    padding: var(--space-2) var(--space-3);
    border-radius: 999px;
    font-family: var(--font-sans);
    font-weight: 700;
    font-size: var(--text-landing-btn);
    line-height: 1.2;
    text-decoration: none;
    border: 2px solid transparent;
}
.btn--primary { background: var(--color-green); color: var(--color-grey-light); }
.btn--secondary { background: var(--color-white); color: var(--color-green); }

/* ── Header ───────────────────────────────────────────────────────────── */
/* Height/logo size from the .ai/Figma source (199px / 180px at the file's 3x export
   scale — the frame is exactly 1125px = 375px x 3, confirmed with the client), divided
   by 3. No sub-pages exist to browse, so the hamburger menu is replaced with a direct
   "Share Your Moments Now" CTA, vertically centered with the logo via flex. */
.site-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    height: 4.146rem;
    padding: 0 var(--space-3);
}
.site-header__logo img { width: 3.75rem; height: 3.75rem; }
.site-header__cta {
    display: inline-block;
    flex-shrink: 0;
    background: var(--color-green);
    color: var(--color-grey-light);
    font-family: var(--font-sans);
    font-weight: 700;
    font-size: var(--text-xs);
    white-space: nowrap;
    text-decoration: none;
    padding: 0.75em 1.5em;
    border-radius: 999px;
}

/* ── Hero ─────────────────────────────────────────────────────────────── */
/* Session 6: padding tightened against a real pixel measurement of the R2
   export — the built hero section ran noticeably taller than the design. */
/* Session 10 — rebuilt against the .ai source's own exported pages
   (FA/Export/Starbucks_30th_Microsite-UI_Starbucks_30th_Microsite_*.png), now authoritative
   over the earlier R2 revision per explicit client instruction. Full-width color sections,
   not card-in-card — a different visual language from the R2-era build this replaces. */
.hero {
    background: var(--color-grey-light);
    padding: 1.375rem 0 .75rem 0; /* session 26 (cont'd) — client override, tighter than the 70px used elsewhere */
    text-align: center;
}
.hero__badge { display: block; margin: 0 auto var(--space-1); width: auto; height: 6.25rem; }
.hero__heading { color: var(--color-green); font-size: calc(var(--text-landing-hero) * 1.35); line-height: 1; max-width: var(--copy-max-width); margin: 0 auto 0; }
/* No max-width cap (client request) — the animated flipbook PNG has some built-in whitespace
   margin baked into every frame, so displaying it at full container width keeps that margin
   proportionally small instead of shrinking the illustration further inside it. Figma's own
   layer bounding boxes show the heading text and the book image essentially touching (no real
   gap) — the book's own baked-in whitespace already reads as the gap, so the heading needs no
   margin-bottom of its own. */
.hero__book { display: block; width: 100%; margin: 0 auto; height: auto; }
.hero__subheading {
    font-family: var(--font-serif);
    font-weight: 400;
    color: var(--color-green);
    /* Session 26 (cont'd) — client asked for this to wrap onto 2 lines; ×1.4 was tested against
       every common mobile width (320-480px) and is the only multiplier that never collapses back
       to 1 line across that whole range while keeping a reasonably balanced break. */
    font-size: calc(var(--text-landing-h2) * 1.4);
    line-height: 1;
    max-width: var(--copy-max-width);
    margin: 0 auto var(--space-1);
}
.hero__body { max-width: var(--copy-max-width); margin-inline: auto; font-size: var(--text-landing-body-lg); line-height: 1.35; color: var(--color-green-dark); }

/* ── Full-width green CTA ────────────────────────────────────────────── */
.cta-green {
    /* The wave art (home-favourite-starbucks-moment-bg.png) is fully transparent in thin
       slivers right at the very top/bottom edges — that's the wave's cutout, meant to show
       whatever's behind it. A single flat background-color fallback can only match ONE
       neighbor, so the other edge's cutout blends into the wrong color (confirmed: alpha
       sampled at y=0 and y=100% is fully transparent, opaque green by ~2% in). Layering a
       second "background" that's a hard-split gradient — grey-light for the top half, cream
       for the bottom half — behind the art gives each edge its own correct color with no
       extra markup. (First-listed background layer paints on top.)
       Split at 50%: the transparent slivers only exist within a few % of each edge, so
       anywhere in the wide opaque-green middle works as the split point. */
    background-image:
        url("../images/home-favourite-starbucks-moment-bg.png"),
        linear-gradient(to bottom, var(--color-grey-light) 50%, var(--color-cream) 50%);
    background-size: 100% 100%, 100% 100%;
    background-repeat: no-repeat, no-repeat;
    color: var(--color-white);
    padding: 4.375rem 0; /* 70px, per client request */
    text-align: center;
    position: relative;
}
.cta-green h2 { color: var(--color-white); font-size: var(--text-landing-hero); line-height: 1; max-width: var(--copy-max-width); margin: 0 auto var(--space-2); }
.cta-green p { max-width: var(--copy-max-width); line-height: 1.2; margin: 0 auto var(--space-3); }
/* The Figma source gives this button an exact inset (16.89%/18.67%/44.89%/52.16% of the
   section box) — tried as absolute positioning first, but the paragraph above it wraps to
   a different number of lines at different viewport widths (fluid type), so a fixed % of
   section height overlapped the text at some widths (confirmed via a real render, not
   assumed). Kept in normal flow instead — same visual position in practice, but never
   collides with the text above it regardless of how it wraps. */
.cta-green__button-wrap {
    position: relative;
    display: inline-block;
}
.cta-green .btn { width: auto; min-width: 12rem; padding-inline: var(--space-4); }
/* The extracted star assets are dark green (#006241) — right for the grey reward-card
   context they came from, but invisible against this section's similar dark-green
   background (confirmed via a real rendered screenshot, not assumed). The reference shows
   these stars in white here, so recolor via filter rather than extracting a second
   white-specific asset set. */
.cta-green__star { position: absolute; z-index: 2; filter: brightness(0) invert(1); }
.cta-green__star--topleft { left: -1.3rem; top: -1.6rem; width: 1.3rem; }
.cta-green__star--left { left: -2.5rem; top: 75%; transform: translateY(-50%); width: 1.9rem; }
.cta-green__star--right { right: -2rem; top: -1.2rem; width: 1.8rem; }
.cta-green__seal { position: absolute; right: var(--space-3); bottom: -2.75rem; width: 5.5rem; height: auto; z-index: 3; }

/* ── Full-width cream rewards section ────────────────────────────────── */
.rewards-cream {
    background: var(--color-cream);
    padding: 4.375rem 0; /* 70px, per client request */
    text-align: center;
    position: relative;
}
.rewards-cream h2 {
    color: var(--color-green);
    font-size: var(--text-landing-h2);
    font-weight: 400;
    max-width: var(--copy-max-width);
    margin: 0 auto;
    line-height: 1.2;
}
/* Bear + rewards-box illustration, was live markup (bear <img> + a CSS-styled box with a
   real <ul>) — replaced with the client's own single flattened PNG (bear + box + copy all
   baked in) per their request; simpler and pixel-exact to their source in one image. */
.rewards-cream__card { display: block; width: 100%; max-width: 24rem; height: auto; margin: 0 auto var(--space-3); }
.rewards-cream__btn {
    background: var(--color-green-dark);
    color: var(--color-grey-light);
    width: auto;
    min-width: 12rem;
    padding-inline: var(--space-4);
}
/* left/top from the Figma inset percentages; width derived the same way (100-71.07-2.49)
   rather than also setting right/bottom — for a replaced element (img), left+right with
   no explicit width doesn't stretch it, it falls back to the file's full native pixel
   size, which blew this up to a huge circle covering most of the section (confirmed via
   a real render, not assumed). height:auto preserves the image's own aspect ratio. */
.rewards-cream__leaf { position: absolute; right: 1.5rem; bottom: 1.1rem; width: 5.5rem; height: auto; z-index: 3; }

/* ── Wall of Memories — 2x2 grid, matching the .ai export (not a horizontal
   scroll carousel — that was an earlier R2-era interpretation) ─────────── */
.wall-of-memories { background: var(--color-white); padding: 4.375rem var(--space-1); text-align: center; position: relative; } /* 70px top/bottom per client request; slight left/right so the card grid doesn't touch the viewport edge — client asked for tighter var(--space-1) on both this and the inter-card gap below (session 22, reverting the brief var(--space-2) match) */
.wall-of-memories__heading-row { display: flex; align-items: center; justify-content: center; gap: var(--space-2); margin-bottom: var(--space-3); }
.wall-of-memories__heading-row h2 { font-size: var(--text-landing-h2); line-height: 1.2; color: var(--color-green); margin: 0; text-align: left; max-width: var(--copy-max-width); }
/* The larger --text-landing-h1alt size wraps "with you" onto its own 3rd line below 337px
   (measured directly) — the badge sitting alongside it in the flex row leaves too little room at
   the very narrowest common widths (e.g. 320px/iPhone SE). Client asked to keep the original size
   there and only size up once there's room for a clean 2-line wrap. */
@media (min-width: 338px) {
    .wall-of-memories__heading-row h2 { font-size: var(--text-landing-h1alt); }
}
.wall-of-memories__heading-row h2 em { font-style: italic; }
.wall-of-memories__badge { width: 4.25rem; height: auto; flex-shrink: 0; }
.wall-of-memories__seal { position: absolute; right: 3rem; top: -0.75rem; width: 5.5rem; height: auto; z-index: 3; }
/* Masonry layout — cards have naturally varying height (session 11 correction: the earlier
   2x2 grid was an interim build for testing the AJAX plumbing, not the final layout).
   `columns: 2` below is the no-JS fallback (also what the layout looked like before session
   22): a real cross-browser way to get a masonry effect with zero script, but it re-balances
   its ENTIRE column layout on every DOM change — appending new cards during infinite scroll
   could visibly reflow cards already on screen, sometimes even jumping them into the other
   column (client feedback: infinite-scroll loading "looks weird"; confirmed live before
   fixing). `initMemoriesMasonry()` in main.js progressively enhances this into real absolute
   positioning (`--js` below) computed from actual card heights, where only newly-appended
   cards are ever positioned — existing ones are never moved again. */
/* Gap matches the section's own left/right padding above — both var(--space-1), per client
   preference (session 22: briefly tried matching both at var(--space-2), client asked for the
   tighter var(--space-1) on both instead). */
.wall-of-memories__grid { columns: 2; column-gap: var(--space-1); }
.wall-of-memories__grid--js { columns: initial; position: relative; }
.wall-of-memories__grid--js .memory-card { position: absolute; margin-bottom: 0; }
/* <a> now (session 11 — cards link to the submitter's /share/<code> page), not <article> —
   reset link defaults so it still reads as a plain card. break-inside/margin-bottom make
   the CSS-column masonry above work (each card stays whole, spaced within its column). */
.memory-card {
    background: var(--color-green);
    border-radius: 12px;
    overflow: hidden;
    text-align: left;
    display: flex;
    flex-direction: column;
    min-height: 11rem;
    text-decoration: none;
    color: inherit;
    break-inside: avoid;
    margin-bottom: var(--space-1);
    transition: opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1), transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}
/* Fade-in-up on scroll (session 15, made more pronounced + staggered per batch in session 22) —
   JS (initMemoryCardReveal) adds this the instant a card enters the DOM, then removes it once
   the card scrolls into view (and its photo, if any, has actually finished loading), so cards
   ease in instead of popping in abruptly. The base .memory-card above stays fully opaque with
   no class needed — if JS never runs, every card is simply visible immediately, no broken/
   invisible state possible. transition-delay is set inline per card (main.js) so a batch of
   cards revealed together cascades in one after another instead of all at once. */
.memory-card--pending { opacity: 0; transform: translateY(28px); }
@media (prefers-reduced-motion: reduce) {
    .memory-card { transition: none; }
    .memory-card--pending { opacity: 1; transform: none; }
}
.memory-card:not(.memory-card--photo) { padding: var(--space-2); justify-content: flex-start; }
.memory-card--photo img { width: 100%; aspect-ratio: 1 / 1; object-fit: cover; display: block; }
.memory-card--photo .memory-card__footer { padding: var(--space-1) var(--space-2) var(--space-2); }
.memory-card__name { color: var(--color-white); font-weight: 700; font-size: var(--text-sm); margin: 0 0 0.2rem; }
.memory-card__snippet { color: var(--color-white); font-size: var(--text-xs); margin: 0; line-height: 1.4; }
.memory-card__more { text-decoration: underline; font-weight: 700; white-space: nowrap; }

/* True infinite scroll (session 11 correction — the client's original spec, not a manual
   "load more" click, which was only ever an interim step for testing the AJAX plumbing).
   #memories-sentinel is an invisible trigger an IntersectionObserver watches (assets/js/
   main.js) — no visible "scroll for more" copy needed once loading is fully automatic,
   per the client's own 17 Jul note that such text becomes redundant with auto-loading. */
.wall-of-memories__sentinel { height: 1px; }
.wall-of-memories__loading {
    text-align: center;
    color: var(--color-green-dark);
    opacity: 0.7;
    font-weight: 700;
    font-size: var(--text-sm);
    margin-top: var(--space-3);
}
.wall-of-memories__end { text-align: center; color: var(--color-ink); opacity: 0.6; font-size: var(--text-sm); margin-top: var(--space-3); }

/* ── Footer ───────────────────────────────────────────────────────────── */
.site-footer p { margin: 0; } /* the global p{margin-bottom} rule left extra space under the footer, unbalancing its padding */
.site-footer {
    text-align: center;
    padding: var(--space-3);
    font-size: var(--text-xs);
    color: var(--color-ink);
}

/* ── Submission Form ─────────────────────────────────────────────────── */
/* Session 6: pixel-compared against R2's Submission Form export — the whole intro
   area (mascot + bubble + note copy) sits on a light-grey field there, not a plain
   white background with only the speech-bubble shape shaded. */
/* Session 10 — rebuilt against the .ai source's Submission Form export, now authoritative
   over R2/Copy.txt for this page's layout and the two-checkbox consent structure. */
.submit { max-width: var(--max-width); margin: 0 auto; background: var(--color-grey-light); } /* #ECECEE across the whole page, per client request */
.submit__hero {
    background: var(--color-grey-light);
    padding: var(--space-3);
    text-align: center;
}
/* Bear + heading + callout was live markup (image + a CSS-styled speech-bubble h1 + a
   separate callout panel) — replaced with the client's own single flattened PNG (all 3
   baked in) per their request. The real <h1> text lives on in a visually-hidden element
   right above the image so the page keeps a real heading for SEO/screen readers. */
.submit__hero-image { display: block; width: 100%; max-width: 24rem; height: auto; margin: 0 auto; }
/* Session 26 (cont'd) — the first pass (flat single-shape, flush color split) was still wrong:
   Figma's overlapping layer bounds (header rect + body rect overlap by ~100px) don't mean the
   two sections just butt together with no seam — they mean the LIGHTER body rectangle has its
   OWN rounded top corners and physically rises up over the dark banner, revealing the banner's
   own color in the small gaps around the body's curve (a layered "peeking card" effect, not a
   flat color split). Missed this because the earlier fix gave `.submit-form__card-body` no
   radius of its own, relying entirely on the outer card's clip — that produces a flat edge, not
   a peeking curve. Fixed: `.submit-form__card` is now transparent with top-only radius (its
   only job is clipping the banner's own square top corners to match); `-banner` gets extra
   bottom padding (`--space-5`) so its actual text is never at risk of being covered, then a
   negative `margin-bottom` pulls `-body` up over that blank padding area; `-body` has its own
   top radius so the reveal is a real rounded-corner peek, not a straight cut. */
.submit-form__card {
    background: transparent;
    border-radius: 20px 20px 0 0;
    overflow: hidden;
    margin: var(--space-2) var(--space-2) 0 var(--space-2);
    color: var(--color-white);
}
.submit-form__card-banner {
    background: var(--color-green-dark);
    text-align: center;
    padding: var(--space-3) var(--space-3) var(--space-5) var(--space-3);
    margin-bottom: -35px;
}
.submit-form__card-body {
    padding: var(--space-3);
    border-radius: 20px 20px 0 0;
    background: var(--color-green);
}
.submit__banner-line { font-weight: 700; line-height: 1.2; margin: 0 0 var(--space-2); }
.submit__banner-line:last-child { margin-bottom: 0; }
.submit__banner-line--secondary { font-weight: 400; }
.submit-form__section-heading { font-family: var(--font-serif); text-align: center; margin-bottom: 0.25rem; }
.submit-form__section-hint { text-align: center; font-size: var(--text-sm); margin-bottom: var(--space-2); }

.field { display: block; margin-bottom: var(--space-2); }
.field__label { display: block; font-weight: 700; margin-bottom: var(--space-1); }
.field__sr-label {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
.field input[type="text"],
.field input[type="email"],
.field textarea {
    width: 100%;
    border: none;
    border-radius: 8px;
    padding: var(--space-2);
    font-family: var(--font-sans);
    font-size: var(--text-base);
    color: var(--color-ink);
}
.field--upload {
    background: var(--color-white);
    color: var(--color-ink);
    border-radius: 8px;
    padding: var(--space-3);
    text-align: center;
    display: block;
    position: relative;
    /* Session 24: the preview fills this box via absolute positioning (top+bottom insets),
       which doesn't itself contribute any height to the parent — and hiding the icon/prompt/
       disclaimer while a preview shows (see initUploadPreview()) removes the only flow content
       that WAS giving this box its height, so without a min-height the whole box would
       collapse to near-zero the moment a photo is selected. This also keeps the box a
       consistent size in both states, rather than jumping when a photo is picked/cleared. */
    min-height: 15rem;
    border: 2px dashed transparent;
    transition: border-color 0.15s ease, background-color 0.15s ease;
}
/* Drag-and-drop affordance (session 14) — dropping a file here does the same thing as
   tapping and picking one, so this is the only visual signal needed; no copy change. */
.field--upload--dragover {
    border-color: var(--color-green);
    background: var(--color-grey-light);
}
/* The native file input's browser chrome ("Choose File / No file chosen") must never be
   visible — the label + this custom UI are the whole control. Visually hidden, not
   display:none, so it stays keyboard/screen-reader accessible. Session 6 pixel-comparison
   against R2 caught this rendering as a real, visible bug, not just a style nicety. */
.field--upload input[type="file"] {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}
.field--upload__icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2.75rem;
    height: 2.75rem;
    margin: 0 auto;
    background: var(--color-green);
    border-radius: 10px;
    color: var(--color-white);
    font-size: 1.5rem;
    line-height: 1;
}
.field--upload__prompt { display: block; font-weight: 400; margin-top: var(--space-2); color: var(--color-ink); }
.field--upload__disclaimer { display: block; margin-top: 0.35rem; font-size: var(--text-xs); color: #666; }
/* Session 24: these 3 get JS-toggled [hidden] while a photo preview is showing. Each has its
   own explicit `display` above with the same (0,1,0) specificity as the browser's own
   [hidden]{display:none} default, and author CSS wins ties — the exact bug just found and
   fixed for .field--story__hint. Guarding all 3 here up front rather than finding out the
   same way again. */
.field--upload__icon[hidden],
.field--upload__prompt[hidden],
.field--upload__disclaimer[hidden] { display: none; }
/* Selected-photo preview (session 16: a small bottom-right thumbnail; session 24: client
   asked for it much larger — centered horizontally, filling the section's full height,
   matching their reference screenshot — since a photo is now the dominant thing in this box
   once one's selected, not a small confirmation chip). top+bottom define the height (the
   box's own height minus its padding); aspect-ratio then derives a matching width from that,
   centered via the transform, so it always stays a true square regardless of box height. */
.field--upload__preview-wrap {
    position: absolute;
    top: var(--space-3);
    bottom: var(--space-3);
    left: 50%;
    transform: translateX(-50%);
    width: auto;
    aspect-ratio: 1 / 1;
    /* display:block (not the default inline for a <span>) + overflow:hidden make the fixed
       size above unconditional — if the image ever fails to load for any reason, there is
       nothing here for a fallback alt-text render to expand into. */
    display: block;
    overflow: hidden;
}
.field--upload__preview-wrap[hidden] { display: none; }
.field--upload__preview {
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: 10px;
    border: 2px solid var(--color-green);
    display: block;
    background: var(--color-grey-light);
    /* Belt-and-suspenders alongside the wrap's overflow:hidden above — font-size:0 keeps a
       broken image's alt text from ever becoming visible, large body-sized text spilling
       out of this box (exactly what happened when a CSP gap silently blocked the preview's
       blob: URL — the real fix for that is in .htaccess's img-src directive). */
    font-size: 0;
    color: transparent;
}
.field--upload__preview-clear {
    position: absolute;
    top: -0.5rem;
    right: -0.5rem;
    width: 1.5rem;
    height: 1.5rem;
    border-radius: 50%;
    background: var(--color-white);
    border: 1.5px solid var(--color-green);
    color: var(--color-green-dark);
    font-size: 1rem;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    padding: 0;
}
/* Story field — label + hint + textarea all sit inside one white box in the design,
   not a green-background label with a separate white textarea. */
.field--story {
    background: var(--color-white);
    color: var(--color-ink);
    border-radius: 8px;
    padding: var(--space-2);
    display: block;
}
.field--story__label { display: block; font-size: var(--text-sm); margin-bottom: var(--space-1); }
/* Session 24: client clarified this needs to behave like a real placeholder — sitting inside
   the textarea itself and disappearing the instant typing starts — not static copy stacked
   above it (which is all the session-20/23 version actually did). A native <textarea
   placeholder> can't hold this — it has line breaks, a bulleted list, and italic examples,
   none of which plain placeholder text supports — so this overlays the hint absolutely inside
   a wrapper sized to match the textarea exactly, with pointer-events:none so a tap anywhere in
   the hint focuses the real textarea underneath it, exactly like a placeholder does. */
.field--story__textarea-wrap { position: relative; display: block; }
.field--story__hint {
    /* No padding of its own — inset:0 must line up exactly with the textarea's own box below
       it (also padding:0, relying on .field--story's padding for inset), so hint text starts
       at the identical position real typed text would, the same way a real placeholder does. */
    position: absolute;
    inset: 0;
    font-size: var(--text-xs);
    color: #666;
    pointer-events: none;
    overflow: hidden;
}
.field--story__hint[hidden] { display: none; }
.field--story__hint em { font-style: italic; }
.field--story textarea { padding: 0; border-radius: 0; display: block; }

.submit-form__consent { background: var(--color-grey-border); padding: var(--space-3); text-align: center; } /* #E1DFDD, per client request */
.submit-form__consent .btn { margin-top: var(--space-2); }
.field--checkbox { display: flex; align-items: flex-start; gap: var(--space-1); font-size: var(--text-sm); text-align: left; margin-bottom: var(--space-2); }
.field--checkbox input { margin-top: 0.3rem; flex-shrink: 0; }
.field--checkbox a { color: var(--color-green-dark); text-decoration: underline; }

.form-success { color: var(--color-green-dark); background: #DCF0E4; font-size: var(--text-sm); font-weight: 700; padding: var(--space-2); border-radius: 8px; text-align: center; }
.form-error { color: #B00020; font-size: var(--text-xs); display: block; margin-top: var(--space-1); }
.form-error[hidden] { display: none; }
.form-error--banner {
    background: #FFEAEA;
    color: #B00020;
    padding: var(--space-2);
    margin: var(--space-3) var(--space-3) 0;
    border-radius: 8px;
    text-align: center;
}

.cf-turnstile { margin: var(--space-3) 0; display: flex; justify-content: center; }

/* ── Success Page ─────────────────────────────────────────────────────── */
.success {
    max-width: var(--max-width);
    margin: 0 auto;
    padding: var(--space-3);
    text-align: center;
    position: relative;
}
.success__heading { color: var(--color-green); }
.success__saved { margin-top: var(--space-1); }
.success__saved-heart { display: inline-block; width: 1.1em; height: 1.1em; vertical-align: -0.15em; }
/* Reward/voucher card frame — dimensions, radius, color, and star positions are
   traced directly from the client's reference screenshot (a selection off the
   .ai source's Success Page artboard, page index 2, drawing #44 + the 3 star/
   sparkle clusters around it), not eyeballed. The photo fills the whole card;
   the footer strip (barcode + terms) is an opaque overlay in the same frame
   green, so it visually overlaps the bottom of the photo — this is "the
   frame's green portion overlaps the uploaded image" per the client's brief. */
/* Split into an outer wrapper (unclipped, holds the stars) and an inner card
   (overflow:hidden, needed to round the photo's corners) — the stars sit
   outside the card's own edges, so they'd be invisibly clipped if they were
   positioned relative to the same element that clips the photo. */
.reward-card-wrap {
    position: relative;
    width: 13.3rem;
    max-width: 100%;
    margin: var(--space-4) auto var(--space-3);
}
.reward-card {
    position: relative;
    aspect-ratio: 638.6 / 798.2;
    border-radius: 16px;
    overflow: hidden;
    background: var(--color-green-frame);
}
.reward-card__photo {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}
.reward-card__footer {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 2;
    background: var(--color-green-frame);
    padding: var(--space-2) var(--space-3);
    text-align: center;
}
/* The .ai source displays this barcode at 165.82x42.23px (1x) — a 3.93:1 stretch, much
   flatter than the source file's own 592x337 native aspect ratio. Without an explicit
   aspect-ratio here the browser falls back to the native ratio, rendering ~2.2x too tall —
   confirmed by re-measuring the source directly (client flagged this as visibly too tall). */
.reward-card__barcode {
    display: block;
    width: 100%;
    aspect-ratio: 497.46 / 126.70;
    object-fit: contain;
    background: var(--color-white);
    padding: var(--space-1);
    border-radius: 8px;
}
.reward-card__terms { color: var(--color-white); font-size: var(--text-xs); margin: var(--space-1) 0 0; }
.reward-card__star { position: absolute; z-index: 3; display: block; }
.reward-card__star--top-left { left: -30.79px; top: 11.9px; width: 18.03px; }
.reward-card__star--top-right { left: calc(100% + 9.42px); top: 0.42px; width: 28.99px; }
.reward-card__star--bottom-left { left: -38.13px; bottom: 3.85px; width: 31.46px; }
.reward-card-image { display: block; width: 100%; max-width: 20rem; margin: var(--space-4) auto var(--space-2); height: auto; }
.reward-card-image__save { width: auto; min-width: 12rem; padding-inline: var(--space-3); margin-bottom: var(--space-3); }
.success__voucher-image { display: block; width: 100%; max-width: 20rem; margin: 0 auto var(--space-2); height: auto; border-radius: 12px; }
.success__voucher-code { font-size: var(--text-sm); color: var(--color-ink); margin-bottom: var(--space-2); }
.success__valid-till-label { font-family: var(--font-serif); color: var(--color-green-dark); font-size: var(--text-xl); margin-bottom: var(--space-1); }
.success__valid-till-box {
    background: var(--color-green);
    color: var(--color-white);
    font-family: var(--font-serif);
    font-size: var(--text-3xl);
    border-radius: 10px;
    padding: var(--space-2);
    margin-bottom: var(--space-3);
}
.share-card { margin: var(--space-4) 0; }
.share-card__image { border-radius: 12px; margin-bottom: var(--space-2); }
.share-card .btn { margin-bottom: var(--space-1); }
.share-card .btn[hidden] { display: none; }

/* ── Public share page (/share/<code>) ────────────────────────────────── */
.share {
    max-width: var(--max-width);
    margin: 0 auto;
    padding: var(--space-3);
    text-align: center;
}
/* Width set explicitly on the <picture> wrapper itself, not just the <img> —
   picture is a non-replaced element, so it fills 100% of its container by
   default and margin:auto centers nothing without a fixed width to center. */
.share__logo { display: block; width: 56px; margin: 0 auto var(--space-3); }
.share__image { width: 100%; border-radius: 20px; margin-bottom: var(--space-3); }
.share__story {
    font-family: var(--font-serif);
    font-size: var(--text-lg);
    color: var(--color-ink);
    margin: 0 0 var(--space-1);
}
.share__author { font-weight: 700; color: var(--color-green-dark); margin-bottom: var(--space-3); }

/* ── Legal pages (Terms & Conditions, Privacy) ───────────────────────────── */
.legal-page { max-width: var(--max-width); margin: 0 auto; padding: var(--space-3); }
.legal-page h2 { margin-top: var(--space-4); font-size: var(--text-xl); }
.legal-page h3 { margin-top: var(--space-3); font-size: var(--text-lg); }
.legal-page p { margin: 0 0 var(--space-2); line-height: 1.5; }
.legal-page ul,
.legal-page ol { margin: 0 0 var(--space-2); padding-left: 1.25em; }
.legal-page ul { list-style: disc; }
.legal-page ol { list-style: decimal; }
.legal-page li { margin-bottom: 0.5em; line-height: 1.5; }
.legal-page li > ul { margin-top: 0.5em; }
.legal-page a { color: var(--color-green-dark); text-decoration: underline; }

/* ── Mobile-portrait-only gate ────────────────────────────────────────── */
/* This microsite is meant to work like a native mobile app, not scale up to
   tablet/desktop (client instruction, session 7) — no design was ever produced
   for those sizes anyway (confirmed session 6: the source .ai file only defines
   a 375px mobile artboard). Hidden by default; shown — and real content hidden —
   the instant the viewport stops being narrow-and-portrait. Comma = OR in a media
   query: either condition alone is enough to trigger the gate. Pure CSS on
   purpose: reacts to resize/rotation immediately with no JS listener required,
   and still works if JS never runs at all. */
.mobile-gate {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 999;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    padding: var(--space-4);
    background: var(--color-green);
    color: var(--color-white);
    text-align: center;
}
.mobile-gate h2 { color: var(--color-white); margin: 0; }
.mobile-gate p { max-width: 22rem; margin: 0; }

@media (min-width: 48rem), (orientation: landscape) {
    .mobile-gate { display: flex; }
    /* Scoped with the ~ sibling combinator, not bare tag/class selectors — admin
       pages (and share.php) use plain <main>/.site-footer too but never render
       .mobile-gate (see includes/header.php's $skipMobileGate), so an unscoped
       rule here would hide the admin panel's content at desktop width even
       though the gate itself never shows there. Found via Playwright testing
       the admin dashboard at 1440x900, session 7. */
    .mobile-gate ~ .site-header, .mobile-gate ~ main, .mobile-gate ~ .site-footer { display: none; }
}
