/* MentaLoyalty — global custom styles.
 * Tailwind handles 99% of styling; this stylesheet covers the bits Tailwind
 * cannot express ergonomically: glassmorphism, the 3D card perspective,
 * the ambient background blobs, and the custom scrollbar.
 */

:root {
    --brand-primary: #4EBCB8;
    --brand-secondary: #F59092;
    --brand-font-body: "Geologica Brand", "Geologica", system-ui, sans-serif;
    --brand-font-heading: "Geologica Brand", "Geologica", system-ui, sans-serif;
    /* Pre-derived tints so children don't need color-mix calls everywhere. */
    --brand-soft-bg: color-mix(in srgb, var(--brand-primary) 14%, transparent);
    --brand-soft-border: color-mix(in srgb, var(--brand-primary) 32%, transparent);
    --brand-strong-bg: color-mix(in srgb, var(--brand-primary) 28%, transparent);
    --brand-readable: color-mix(in srgb, var(--brand-primary) 55%, #14171a 45%);
    --brand-glow: color-mix(in srgb, var(--brand-primary) 45%, transparent);

    /* Shared premium gradient: a plain 2-stop diagonal reads flat/muddy at
     * the midpoint (teal→coral crosses a dull brown-grey). Layering a soft
     * white sheen over it gives every button/pill/avatar that uses it a
     * glossy, catch-the-light quality instead — same brand hues, richer feel.
     *
     * This is the surface white text/icons sit on (buttons, avatar fills,
     * the login CTA) — var(--brand-primary)/(--brand-secondary) on their own
     * are pastel enough that white text fails contrast at BOTH ends of the
     * gradient (~2.3:1). Darkened 42% toward black here, which lands ~5:1 —
     * still unmistakably teal→coral, just deep enough to hold white text.
     * --brand-primary/--brand-secondary themselves stay untouched since they
     * back light surfaces (page canvas, card, badges) with dark text on top. */
    --brand-gradient:
        linear-gradient(135deg, rgba(255, 255, 255, 0.16) 0%, rgba(255, 255, 255, 0) 42%),
        linear-gradient(135deg, color-mix(in srgb, var(--brand-primary) 58%, black 42%) 0%, color-mix(in srgb, var(--brand-secondary) 58%, black 42%) 100%);
    --brand-gradient-h:
        linear-gradient(180deg, rgba(255, 255, 255, 0.38) 0%, rgba(255, 255, 255, 0) 65%),
        linear-gradient(90deg, var(--brand-primary) 0%, var(--brand-secondary) 100%);

    /* EXPERIMENTAL (branch: experiment/light-brand-theme) — teal (#4EBCB8) is
     * Menta's actual dominant brand color, per the client — not an invented
     * cream tone. The page canvas itself is teal; white glass cards float on
     * top (same relationship as the real site's teal footer band with a
     * white wordmark). Kept as tokens so the swap stays isolated to this
     * block + a handful of consuming rules below. */
    --surface-bg: var(--brand-primary);
    --surface-card: #ffffff;
    --text-primary: #14171a;
    --text-muted: #6b7280;
    --border-soft: rgba(20, 23, 26, 0.10);
    --border-strong: rgba(20, 23, 26, 0.16);
}

.blob-field {
    /* The clipping box for the decorative blobs. See the long note in
       templates/base.html for why this exists at all.

       Fixed rather than absolute so it is its own containing block AND its own
       clipping context: whatever blob-drift does to its children, nothing
       inside can extend the document's scroll width. This is the thing that
       stopped every member page being draggable ~110px sideways.

       ⚠ `overflow-x: clip` on <html> was the first attempt and did not work —
       root-element overflow is propagated to the viewport under its own rules
       and the blobs kept widening the scrollable area with the rule live and
       computing on production. Do not "simplify" this back to a root rule. */
    position: fixed;
    inset: 0;
    overflow: hidden;
    pointer-events: none;
    z-index: 0;
}

body {
    background-color: var(--surface-bg);
    /* Depth pass: the flat teal canvas read as muddy/lifeless next to the
       card and splash (both already use a radial light/dark gradient).
       Only ever LIGHTENS relative to the flat baseline — the app's dark
       text sits directly on this background in many places, so darkening
       any region would reopen the contrast bugs just fixed. */
    background-image:
        radial-gradient(ellipse 90% 60% at 18% -8%, rgba(255, 255, 255, 0.32) 0%, transparent 55%),
        radial-gradient(ellipse 70% 55% at 105% 105%, color-mix(in srgb, var(--brand-secondary) 40%, transparent) 0%, transparent 62%),
        linear-gradient(160deg, color-mix(in srgb, var(--brand-primary) 88%, white 12%) 0%, var(--brand-primary) 55%, color-mix(in srgb, var(--brand-primary) 92%, white 8%) 100%);
    background-attachment: fixed;
    color: var(--text-primary);
    font-family: var(--brand-font-body, "Geologica Brand", system-ui, sans-serif);
}

@font-face {
    font-family: "Geologica Brand";
    src: url("../brands/fonts/Geologica-Variable.2926daa6f6c9.ttf") format("truetype");
    font-weight: 100 900;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: "Artegra Brand";
    src: url("../brands/fonts/ArtegraSansCondensed-SemiBold.ade1a3c02b2d.otf") format("opentype");
    font-weight: 600;
    font-style: normal;
    font-display: swap;
}

h1, h2, h3, .brand-heading {
    font-family: var(--brand-font-heading, var(--brand-font-body));
}

::selection {
    background-color: var(--brand-primary);
    color: white;
}

/* ------------------------------------------------------------------ */
/* Brand-aware helpers — used in place of Tailwind static brand-XXX   */
/* utilities so the whole UI re-tints when the active brand changes.  */
/* ------------------------------------------------------------------ */

.text-brand        { color: var(--brand-readable); }
.text-brand-deep   { color: var(--brand-primary); }
.bg-brand-soft     { background-color: var(--brand-soft-bg); }
.bg-brand-strong   { background-color: var(--brand-strong-bg); }
.border-brand-soft { border-color: var(--brand-soft-border); }
.ring-brand        { box-shadow: 0 0 0 1px var(--brand-soft-border); }

.btn-brand {
    background: var(--brand-gradient);
    color: white;
    box-shadow: 0 12px 28px -12px var(--brand-glow);
}

.btn-brand:hover {
    filter: brightness(1.08);
}

.btn-brand-ghost {
    background-color: var(--brand-soft-bg);
    color: var(--brand-readable);
    border: 1px solid var(--brand-soft-border);
}

.btn-brand-ghost:hover {
    background-color: var(--brand-strong-bg);
}

.input-brand:focus {
    outline: none;
    border-color: var(--brand-primary);
    box-shadow: 0 0 0 1px var(--brand-primary);
}

/* Smooth re-tint of brand-aware surfaces between switches. The actual
 * brand-color CSS variables are toggled instantly by the server-rendered
 * <style> block, but any element that pulls from them gets a soft fade. */
.btn-brand,
.btn-brand-ghost,
.text-brand,
.text-brand-deep,
.bg-brand-soft,
.bg-brand-strong,
.border-brand-soft,
.blob,
.loyalty-card-bg {
    transition:
        background-color 600ms ease,
        color 600ms ease,
        border-color 600ms ease,
        box-shadow 600ms ease,
        background 600ms ease;
}

/* ------------------------------------------------------------------ */
/* Glassmorphism                                                       */
/* ------------------------------------------------------------------ */

.glass {
    /* EXPERIMENTAL light theme: frosted white instead of frosted navy. */
    background: rgba(255, 255, 255, 0.82);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border: 1px solid var(--border-soft);
}

.glass-card {
    /* EXPERIMENTAL light theme: frosted white instead of frosted dark. */
    background: linear-gradient(135deg, rgba(255, 255, 255, 0.92) 0%, rgba(255, 255, 255, 0.72) 100%);
    backdrop-filter: blur(10px);
    border: 1px solid var(--border-soft);
    box-shadow: 0 18px 40px -20px rgba(20, 23, 26, 0.18);
}

/* Store portal — KPI card labels & section headings.
 * Were solid white ("not muted grey") from when this portal's canvas was
 * dark — on the current light glass-card surface that reads white-on-
 * near-white (~1:1), not "not muted", invisible. */
.card-stat-label {
    font-size: 0.75rem;
    line-height: 1rem;
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-primary, #14171a);
    opacity: 0.65;
}

.card-stat-label-sm {
    font-size: 0.625rem;
    line-height: 0.875rem;
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-primary, #14171a);
    opacity: 0.65;
}

.section-kicker {
    font-size: 0.875rem;
    line-height: 1.25rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    /* Every current usage (store/manage/dashboard.html) sits directly on
       the teal body background, not inside a card — var(--brand-readable)
       was tuned for near-white card surfaces and reads as dark-teal-on-teal
       here (~2.4:1). Plain dark neutral text is what the rest of this page
       already uses directly on that same background (the h1 above it, the
       text-gray-700 subtitle) and gives ~9:1 against the teal. */
    color: var(--text-primary, #14171a);
    opacity: 0.75;
    margin-bottom: 0.75rem;
}

/* ------------------------------------------------------------------ */
/* 3D card                                                             */
/* ------------------------------------------------------------------ */

.perspective-1000 {
    perspective: 1000px;
    touch-action: none;
}

.preserve-3d {
    -webkit-transform-style: preserve-3d;
    transform-style: preserve-3d;
}

.translate-z-10 {
    transform: translateZ(50px);
}

.card-corner-btn {
    transform: translateZ(60px);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    pointer-events: auto;
    cursor: pointer;
    touch-action: manipulation;
    -webkit-tap-highlight-color: transparent;
    transition: background-color 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
}

.card-corner-btn:active {
    transform: translateZ(60px) scale(0.94);
}

.card-corner-btn:focus-visible {
    outline: 2px solid rgba(255, 255, 255, 0.85);
    outline-offset: 2px;
}

.translate-z-bg {
    transform: translateZ(-20px);
}

.card-flip {
    transition: transform 0.7s cubic-bezier(0.4, 0, 0.2, 1);
    -webkit-transform-style: preserve-3d;
    transform-style: preserve-3d;
}

.card-flip.is-flipped {
    transform: rotateY(180deg);
}

.card-face-back {
    /* translateZ(0) forces its own GPU compositing layer — without it,
       Safari/iOS fails to honour backface-visibility:hidden and the
       front face bleeds through mirrored on top of this one. */
    transform: rotateY(180deg) translateZ(0);
    backface-visibility: hidden;
    -webkit-backface-visibility: hidden;
    /* Belt-and-suspenders for iOS/WebKit builds where backface-visibility
       is *still* not honoured on this face even with the layer promotion
       above (confirmed on-device: the QR face stays permanently visible,
       mirrored, behind the front face). Opacity is toggled explicitly by
       .is-flipped below, independently of whatever the 3D transform does,
       so the wrong face is never simply left showing. transition-delay
       (not duration) snaps it at roughly the midpoint of the .card-flip
       rotation so it still reads as a clean flip on browsers where the
       backface-visibility above already works correctly on its own. */
    opacity: 0;
    transition: opacity 0s linear 0.35s;
}

.card-face-front {
    /* Same Safari compositing-layer requirement as .card-face-back. */
    transform: translateZ(0);
    backface-visibility: hidden;
    -webkit-backface-visibility: hidden;
    opacity: 1;
    transition: opacity 0s linear 0.35s;
}

.card-flip.is-flipped .card-face-front {
    opacity: 0;
}

.card-flip.is-flipped .card-face-back {
    opacity: 1;
}

/* ------------------------------------------------------------------ */
/* Brand-themed loyalty card                                           */
/* ------------------------------------------------------------------ */

.loyalty-card-bg {
    background:
        radial-gradient(circle at 20% 20%, color-mix(in srgb, var(--brand-primary) 92%, white 8%) 0%, transparent 55%),
        radial-gradient(circle at 80% 75%, color-mix(in srgb, var(--brand-secondary) 80%, black 20%) 0%, transparent 60%),
        linear-gradient(135deg, var(--brand-primary) 0%, var(--brand-secondary) 100%);
    box-shadow: 0 25px 50px -12px color-mix(in srgb, var(--brand-primary) 45%, transparent);
}

/* The Menta lockup is a two-tone asset — teal glyphs plus a black "MENTA
   COFFEE" wordmark — and on the card the black half sits directly over the
   mint radial in the top-left corner. Sampled from the rendered card at 3x,
   the ink is #000000 on #2F6D6A: 3.52:1, under the 4.5:1 floor and visibly
   unreadable at the 6px cap height it renders at.

   Measured from PIXELS, not from getComputedStyle: the card's background is a
   gradient, so walking the DOM for a background-color skips it entirely and
   reports the page's mint body instead — which is a different colour and gives
   a different, wrong answer.

   The fix is the surface, not a reversed asset. There is exactly one Menta
   logo file and three places in this codebase already reached that same
   conclusion independently — the splash plate (.menta-splash-logo), the login
   pill (.auth-brand-pill) and the email header chip, whose comment spells it
   out: "There is only one logo file, so the fix is the surface." Black on this
   chip measures 19.32:1 off the rendered pixels — not the 21:1 of pure
   white, which a 0.94-alpha plate over a dark card cannot reach. */
.loyalty-card-logo {
    padding: 0.4rem 0.6rem;
    /* Cancelled by an equal negative margin on BOTH axes, so the plate paints
       around the mark without occupying a single pixel of layout. This is not
       a flourish — the card face is aspect-[1.586/1] with overflow-hidden, so
       the 12.8px this padding would otherwise add to the first row does not
       make the card taller, it pushes the cardholder's name and tier pill off
       the bottom edge: sliced at 360px, gone at 320px. The template says so
       itself, six lines above the row it happens to
       (partials/_loyalty_card.html, "anything that wraps here does not push
       the card taller, it falls off the bottom edge"), and I styled the file
       without reading its own warning.
       Cancelling the horizontal padding too keeps the mark optically aligned
       with the labels below it; only the plate bleeds outward, into the card's
       own p-6 gutter, which has 24px to give. */
    margin: -0.4rem -0.6rem;
    border-radius: 0.85rem;
    background: rgba(255, 255, 255, 0.94);
    box-shadow: 0 6px 14px -6px rgba(0, 0, 0, 0.45);
}

/* brand_mark falls back to the brand NAME in text when a brand has no logo
   asset, and that span inherits the card's white. White on the plate is
   invisible, so the fallback needs the same treatment the ink gets. */
.loyalty-card-logo span {
    color: #14171a;
}

.loyalty-card-logo img {
    /* The drop-shadow lifted the mark off the dark card. On the chip it only
       muddies the white, and the chip's own shadow does the lifting now. */
    filter: none;
}

.loyalty-card-bg[data-brand="menta"] {
    background:
        radial-gradient(circle at 18% 22%, rgba(78, 188, 184, 0.65) 0%, transparent 58%),
        radial-gradient(circle at 82% 78%, rgba(245, 144, 146, 0.42) 0%, transparent 64%),
        linear-gradient(145deg, #050505 0%, #0a0a0a 42%, #111827 100%);
    border: 1px solid rgba(78, 188, 184, 0.28);
    /* The far lift-shadow is deliberately near-black, not brand-teal: the
       page canvas itself is teal, so a teal-tinted shadow blends straight
       into it and the card loses its sense of lift. A neutral dark shadow
       reads as real depth against any backdrop. A close, tighter shadow is
       layered underneath it for crisper edge definition. */
    box-shadow:
        0 1px 0 rgba(255, 255, 255, 0.08) inset,
        0 8px 16px -8px rgba(0, 0, 0, 0.5),
        0 30px 54px -14px rgba(2, 12, 12, 0.55);
}

.loyalty-card-bg[data-brand="menta"]::after {
    content: "";
    position: absolute;
    left: 8%;
    right: 8%;
    bottom: 0;
    height: 2px;
    background: linear-gradient(
        90deg,
        transparent,
        rgba(78, 188, 184, 0.85) 50%,
        transparent
    );
    pointer-events: none;
}

/* ------------------------------------------------------------------ */
/* Auth pages — dual-brand ambient backdrop                            */
/* ------------------------------------------------------------------ */

.auth-scene {
    position: relative;
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1.5rem;
    overflow: hidden;
    isolation: isolate;
    min-height: 100%;
    width: 100%;
}

/* Auth pages: side gradients only — no bottom band, full viewport height. */
body:has(.auth-scene) .blob {
    display: none;
}

body:has(.auth-scene) main,
body:has(.auth-scene) .page-fade {
    min-height: 100%;
}

.auth-scene::before {
    /* EXPERIMENTAL light theme: warm paper backdrop instead of near-black. */
    content: "";
    position: absolute;
    inset: 0;
    z-index: 0;
    background-color: var(--surface-bg);
    background-image:
        radial-gradient(ellipse 88% 120% at 0% 50%, var(--auth-glow-menta, rgba(78, 188, 184, 0.22)) 0%, transparent 58%),
        radial-gradient(ellipse 60% 90% at 100% 45%, rgba(245, 144, 146, 0.20) 0%, transparent 55%);
}

.auth-scene::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 0;
    opacity: 0.16;
    background:
        radial-gradient(ellipse 48% 92% at 0% 50%, var(--auth-primary-menta, #4EBCB8) 0%, transparent 54%);
    filter: blur(88px);
    pointer-events: none;
}

.auth-scene > * {
    position: relative;
    z-index: 1;
}

.auth-scene[data-auth-brand="menta"]::before {
    background-color: var(--surface-bg);
    background-image:
        radial-gradient(ellipse 92% 120% at 0% 50%, var(--auth-glow-menta) 0%, transparent 62%),
        radial-gradient(ellipse 55% 85% at 100% 50%, rgba(245, 144, 146, 0.16) 0%, transparent 58%);
}

.auth-card {
    border: 1px solid var(--border-soft);
    box-shadow:
        0 24px 48px -16px rgba(20, 23, 26, 0.16),
        0 0 0 1px rgba(255, 255, 255, 0.6) inset;
}

.auth-brand-strip {
    padding: 0.35rem 0.5rem;
}

.auth-brand-pill {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0.55rem 0.85rem;
    border-radius: 9999px;
    border: 1px solid var(--border-soft);
    background: rgba(255, 255, 255, 0.7);
}

.auth-brand-pill--menta {
    box-shadow: 0 0 24px -6px rgba(78, 188, 184, 0.35);
    border-color: rgba(78, 188, 184, 0.25);
}

.auth-brand-divider {
    width: 1px;
    height: 2rem;
    background: linear-gradient(
        180deg,
        transparent,
        rgba(255, 255, 255, 0.22),
        transparent
    );
}

.store-role-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.2rem 0.55rem;
    border-radius: 9999px;
    font-size: 0.625rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

.store-role-badge--manager {
    color: #92400e;
    background: rgba(245, 158, 11, 0.14);
    border: 1px solid rgba(251, 191, 36, 0.35);
}

.store-role-badge--cashier {
    color: #334155;
    background: rgba(148, 163, 184, 0.12);
    border: 1px solid rgba(148, 163, 184, 0.25);
}

/* Tier-coloured chip used in the levels page. */
.tier-chip {
    background: color-mix(in srgb, var(--brand-primary) 18%, transparent);
    border: 1px solid color-mix(in srgb, var(--brand-primary) 38%, transparent);
    color: color-mix(in srgb, var(--brand-primary) 30%, #f8fafc 70%);
}

/* ------------------------------------------------------------------ */
/* Ambient animated blobs                                              */
/* ------------------------------------------------------------------ */

.blob {
    position: absolute;
    filter: blur(80px);
    z-index: 0;
    /* EXPERIMENTAL light theme: 0.4 was tuned as a glow against a dark
       backdrop; the same opacity on cream reads as a solid tinted stain. */
    opacity: 0.14;
    animation: blob-drift 18s infinite alternate ease-in-out;
}

.blob--primary   { background-color: var(--brand-primary); }
.blob--secondary { background-color: var(--brand-secondary); }

@keyframes blob-drift {
    0%   { transform: translate(0, 0)        scale(1); }
    50%  { transform: translate(100px, 50px)  scale(1.15); }
    100% { transform: translate(-60px, 90px)  scale(0.85); }
}

/* ------------------------------------------------------------------ */
/* Scrollbar                                                           */
/* ------------------------------------------------------------------ */

/* Leftover dark-theme values (#0f172a track) never updated when the
   customer app went light — rendered as a jarring dark bar down the page,
   most visible in WebKit/Safari, which shows a styled scrollbar track more
   readily than Chromium's screenshot compositing does. This file is shared
   with the (still intentionally dark) store portal, so neutral mid-grey
   values here rather than light-theme-specific ones — reads fine on both. */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: rgba(128, 128, 128, 0.12); }
::-webkit-scrollbar-thumb { background: rgba(128, 128, 128, 0.4); border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: rgba(128, 128, 128, 0.55); }

/* ------------------------------------------------------------------ */
/* Form polish                                                         */
/* ------------------------------------------------------------------ */

input[type="checkbox"] {
    accent-color: var(--brand-primary);
    /* The browser default is 13x13px, which is what the marketing-consent
       boxes on /profile/ were rendering at — a target well under half the
       width of a fingertip, on the one form where mis-taps change what the
       member consented to receive. The label beside each box is already a
       <label> wrapping the input, so the text is tappable too; this makes the
       box itself hittable on its own. */
    width: 1.15rem;
    height: 1.15rem;
    flex-shrink: 0;
    cursor: pointer;
}

.errorlist {
    color: #dc2626;
    font-size: 0.8rem;
    margin-top: 0.25rem;
}

.messages-toast {
    animation: fade-in-up 0.4s ease-out;
}

@keyframes fade-in-up {
    from { opacity: 0; transform: translateY(8px); }
    to   { opacity: 1; transform: translateY(0); }
}

/* Dismiss ("X") control on every toast — wired by static/js/messages.js.
 *
 * Every rule below is scoped through .messages-toast on purpose. tailwind.css
 * is linked AFTER app.css (see the ORDER MATTERS comment at the top of
 * templates/base.html), so a single-class rule in this file loses every
 * equal-specificity tie to a Tailwind utility. The toast markup is
 * utility-driven — `flex items-center gap-3 px-4 py-3` — and the day somebody
 * drops a `p-2` or a `w-8` onto this button, the extra class is what keeps the
 * tap target from silently collapsing back to glyph size. */

.messages-toast .messages-toast-text {
    /* min-width is the load-bearing half. A flex item defaults to
       min-width:auto and refuses to shrink below its longest unbreakable run,
       so a message carrying a member code or an email (POS screens do) shoves
       the X clean off a 360px phone instead of wrapping. flex-grow is the other
       half: it keeps the X pinned to the far right even when the message is two
       words long, so the control does not wander depending on text length. */
    flex: 1 1 auto;
    min-width: 0;
}

.messages-toast > i {
    /* The status icon is a flex item too, and it is the narrowest one — without
       this it is the first thing the browser squeezes once the text wraps, and
       the warning triangle comes out visibly ovalled. */
    flex-shrink: 0;
}

.messages-toast .messages-toast-dismiss {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    /* 2.5rem square = a true 40x40 target. Sizing the BUTTON and not the glyph
       is the entire point: the X itself is ~14px, and a 14px target is a miss
       on a phone held one-handed. */
    width: 2.5rem;
    height: 2.5rem;
    padding: 0;
    /* ...but a 40px flex item in a row whose other content is one 20px line of
       text-sm makes the toast 64px tall instead of 44px, which is a visibly
       chunkier bar for what is meant to be a quiet strip. The negative block
       margins shrink the button's MARGIN box back to 20px so the row keeps its
       height, while the button's own box still paints (and still takes taps)
       10px above and below — inside the toast's 12px py-3 padding, so nothing
       spills past the border.
       The -0.75rem on the right eats three quarters of the 1rem px-4 padding
       and lands the glyph's centre 24px in from the edge, which is exactly
       where the status icon on the left sits (16px padding + half a 16px
       glyph). Without it the X floats 36px in and reads as misaligned. */
    margin: -0.625rem -0.75rem -0.625rem 0;
    background: none;
    border: 0;
    border-radius: 0.5rem;
    cursor: pointer;
    line-height: 1;
    /* The toast's semantic colour lives on the parent (text-red-700 and
       friends; the store portal uses its own -800 shades), so inherit it. A
       hard-coded grey looked fine on the info toast and washed-out and wrong on
       the error one. Dimmed to keep the X secondary to the message, but not
       below 0.7 — it is an interactive control and has to stay legible against
       the glass panel, not just detectable. */
    color: inherit;
    opacity: 0.7;
    transition: opacity 150ms ease;
}

.messages-toast .messages-toast-dismiss:hover {
    opacity: 1;
}

.messages-toast .messages-toast-dismiss:focus-visible {
    /* Same treatment as .password-toggle below — the outline has to survive on
       both the cream customer app and the store portal, and the brand secondary
       is the one colour guaranteed to contrast with the glass panel. */
    outline: 2px solid var(--brand-secondary);
    outline-offset: 2px;
    opacity: 1;
}

/* The exit. Deliberately an ANIMATION rather than a transition: fade-in-up
   above is still running for the first 400ms of the toast's life, and while an
   animation owns opacity/transform a transition on those same properties never
   takes effect. A toast dismissed inside that window would sit there
   half-faded and — much worse — never emit the event messages.js removes it
   on, leaving an invisible row propping the space-y-2 gap open. Setting a new
   animation-name cancels the entrance outright and always reaches its end. */
.messages-toast.is-dismissing {
    animation: fade-out-up 0.24s ease-in forwards;
    pointer-events: none;
}

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

@media (prefers-reduced-motion: reduce) {
    /* Near-zero rather than `animation: none`: messages.js waits for
       animationend before it takes the toast out of the DOM, and `none` means
       that event never fires. 0.01ms fires it on the next frame. */
    .messages-toast,
    .messages-toast.is-dismissing { animation-duration: 0.01ms; }
}

/* ------------------------------------------------------------------ */
/* Scan-to-pay mode — the 3D card lifts out of its grid slot, scales  */
/* up, flips to QR, a red laser-style beam slides over the QR and a   */
/* faux 1D barcode shimmers below. Driven by static/js/card.js.        */
/* ------------------------------------------------------------------ */

#scan-backdrop {
    position: fixed;
    inset: 0;
    z-index: 90;
    opacity: 0;
    pointer-events: none;
    background:
        radial-gradient(circle at 50% 45%,
            color-mix(in srgb, var(--brand-primary) 35%, transparent) 0%,
            rgba(0, 0, 0, 0.85) 65%);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    transition: opacity 380ms ease;
}

#scan-backdrop.is-active {
    opacity: 1;
    pointer-events: auto;
}

.scan-backdrop-hint {
    position: absolute;
    left: 50%;
    bottom: 6%;
    transform: translateX(-50%);
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.55rem 1rem;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.08);
    border: 1px solid rgba(255, 255, 255, 0.12);
    color: rgba(255, 255, 255, 0.78);
    font-size: 0.75rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    opacity: 0;
    transition: opacity 280ms ease 320ms;
}

#scan-backdrop.is-active .scan-backdrop-hint { opacity: 1; }

/* Hide scan-only chrome by default — visible only while .is-scan-mode is on. */
.scan-headline,
.scan-beam,
.scan-barcode {
    opacity: 0;
    pointer-events: none;
    transition: opacity 280ms ease;
}

#card-container.is-scan-mode .scan-headline,
#card-container.is-scan-mode .scan-beam,
#card-container.is-scan-mode .scan-barcode {
    opacity: 1;
}

.scan-headline {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    transition-delay: 140ms;
}

.scan-headline-dot {
    width: 8px; height: 8px;
    border-radius: 999px;
    background: #f87171;
    box-shadow: 0 0 12px rgba(248, 113, 113, 0.8);
    animation: scan-pulse 1.1s ease-in-out infinite;
}

@keyframes scan-pulse {
    0%, 100% { transform: scale(1);   opacity: 1;   }
    50%      { transform: scale(1.4); opacity: 0.5; }
}

/* Laser scan beam — slides up and down over the QR while scanning. */
.scan-beam {
    position: absolute;
    left: -6%;
    right: -6%;
    top: 0;
    height: 3px;
    z-index: 20;
    background: linear-gradient(90deg,
        rgba(239, 68, 68, 0) 0%,
        rgba(239, 68, 68, 0.95) 50%,
        rgba(239, 68, 68, 0) 100%);
    box-shadow: 0 0 18px 4px rgba(239, 68, 68, 0.55);
    transform: translateY(0);
}

#card-container.is-scan-mode .scan-beam {
    animation: scan-beam 1.8s cubic-bezier(0.65, 0, 0.35, 1) infinite alternate;
}

@keyframes scan-beam {
    0%   { transform: translateY(0);     opacity: 0.4; }
    8%   { opacity: 1; }
    92%  { opacity: 1; }
    100% { transform: translateY(11rem); opacity: 0.4; }
}

/* In scan mode the card is portrait, so we can grow the QR + barcode for
 * easier cashier scanning. */
#card-container.is-scan-mode .scan-qr {
    width:  11rem;
    height: 11rem;
}

/* Faux 1D barcode rendered with repeating-linear-gradients. JS overrides
 * the gradient stops per member_code so each card has its own pattern. */
.scan-barcode {
    width: 11rem;
    height: 28px;
    background:
        repeating-linear-gradient(
            90deg,
            #0f172a 0 2px,
            transparent 2px 4px,
            #0f172a 4px 5px,
            transparent 5px 8px,
            #0f172a 8px 11px,
            transparent 11px 13px);
    border-radius: 4px;
    transition-delay: 200ms;
}

/* The loyalty card itself: when scan-mode is on, the container is pulled
 * into a fixed-position centred frame by JS (via inline left/top/width).
 * The CSS here just adds the polish: deeper shadow, glow, and disabling
 * the natural mouse-tilt while scanning. */
#card-container.is-scan-mode {
    cursor: default;
    filter: drop-shadow(0 30px 60px var(--brand-glow));
}

#card-container.is-scan-mode #loyalty-card {
    transition: transform 0.7s cubic-bezier(0.4, 0, 0.2, 1);
}

/* Reusable transition for the page underlay so each navigation feels smooth */
.page-fade {
    animation: page-fade-in 0.35s ease-out;
}

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

/* ------------------------------------------------------------------ */
/* Magical brand-switch overlay                                        */
/* ------------------------------------------------------------------ */

#brand-switch-overlay {
    position: fixed;
    inset: 0;
    z-index: 100;
    pointer-events: none;
    opacity: 0;
    background:
        radial-gradient(circle at var(--switch-x, 50%) var(--switch-y, 50%),
            var(--switch-primary, #4F46E5) 0%,
            var(--switch-secondary, #7E22CE) 55%,
            #0f172a 100%);
    clip-path: circle(0% at var(--switch-x, 50%) var(--switch-y, 50%));
    transition: clip-path 700ms cubic-bezier(0.83, 0, 0.17, 1), opacity 220ms ease;
}

#brand-switch-overlay.is-expanding {
    opacity: 1;
    pointer-events: auto;
    clip-path: circle(160% at var(--switch-x, 50%) var(--switch-y, 50%));
}

#brand-switch-overlay.is-collapsing {
    opacity: 1;
    pointer-events: none;
    /* The overlay is rendered fully expanded at first paint when arriving
     * from a brand switch, then this class fades it back to invisible. */
    clip-path: circle(0% at var(--switch-x, 50%) var(--switch-y, 50%));
}

.brand-switch-card {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%) scale(0.6);
    text-align: center;
    color: white;
    opacity: 0;
    transition: opacity 320ms ease 180ms, transform 480ms cubic-bezier(0.34, 1.56, 0.64, 1) 180ms;
    pointer-events: none;
}

#brand-switch-overlay.is-expanding .brand-switch-card {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1);
}

.brand-switch-emoji {
    font-size: 5rem;
    line-height: 1;
    margin-bottom: 0.75rem;
    filter: drop-shadow(0 0 24px rgba(255, 255, 255, 0.45));
    animation: brand-switch-pulse 1.2s ease-in-out infinite alternate;
}

.brand-switch-name {
    font-size: 1.5rem;
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
}

.brand-switch-tagline {
    margin-top: 0.5rem;
    font-size: 0.9rem;
    color: rgba(255, 255, 255, 0.8);
    max-width: 28ch;
}

@keyframes brand-switch-pulse {
    from { transform: scale(1);    filter: drop-shadow(0 0 18px rgba(255,255,255,0.35)); }
    to   { transform: scale(1.08); filter: drop-shadow(0 0 32px rgba(255,255,255,0.7));  }
}

/* Inline pre-paint state — set by the inline <script> in <head>
 * when arriving from a brand switch. Renders the overlay fully covered
 * so there is no flash of the new brand colors before the iris collapses. */
#brand-switch-overlay.is-arriving {
    opacity: 1;
    clip-path: circle(160% at var(--switch-x, 50%) var(--switch-y, 50%));
}

/* ---------------------------------------------------------------------------
 * Co-branding (Buondi Craft × Menta): dashboard banner + card credit.
 * ------------------------------------------------------------------------- */
.cobrand-banner {
    width: 100%;
}
.cobrand-banner-media,
.cobrand-banner-placeholder {
    display: block;
    position: relative;
    width: 100%;
    aspect-ratio: 4 / 1;
    border-radius: 1rem;
    overflow: hidden;
}
.cobrand-banner-img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
/* Tagline rendered by us as an overlay — decoupled from whatever banner
   image file the partner sends, so it always shows regardless of format. */
.cobrand-banner-tagline {
    position: absolute;
    left: 1rem;
    bottom: 0.9rem;
    padding: 0.3rem 0.75rem;
    border-radius: 999px;
    font-size: 0.72rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: #fff;
    background: rgba(15, 23, 42, 0.55);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    pointer-events: none;
}
.cobrand-banner-tagline-placeholder {
    font-size: 0.75rem;
    font-style: italic;
    color: rgba(20, 23, 26, 0.75);
}
.cobrand-banner-placeholder {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.35rem;
    text-align: center;
    border: 2px dashed color-mix(in srgb, var(--brand-primary) 45%, transparent);
    background:
        radial-gradient(120% 140% at 50% 0%,
            color-mix(in srgb, var(--brand-primary) 10%, transparent),
            transparent 70%),
        rgba(20, 23, 26, 0.02);
}
.cobrand-banner-icon {
    font-size: 1.5rem;
    color: rgba(20, 23, 26, 0.78);
    margin-bottom: 0.15rem;
}
.cobrand-banner-title {
    font-size: 0.8rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: rgba(20, 23, 26, 0.82);
}
.cobrand-banner-dims {
    font-family: "Space Mono", monospace;
    font-size: 0.7rem;
    color: rgba(20, 23, 26, 0.75);
}

/* "powered by …" credit baked under the QR on the loyalty-card back face. */
.cobrand-card-credit {
    font-size: 8px;
    line-height: 1;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    font-weight: 500;
    color: rgba(15, 23, 42, 0.42);
    padding-top: 1px;
}

/* "Powered by" + Buondi Craft logo — co-brand watermark on the loyalty-card
   front. The QR/scan back face carries the same pairing since 2026-09-07
   (Bensasson: the caption there should be followed by the logo alone); it
   used the plain-text .cobrand-card-credit from 2026-08-31 until then, and
   that rule still serves as the no-dark-asset fallback. */
.cobrand-card-credit-inline {
    position: absolute;
    right: 1.5rem;
    bottom: 4.3rem;
    z-index: 10;
    display: flex;
    align-items: center;
    gap: 0.35rem;
    pointer-events: none;
}
.cobrand-card-credit-text {
    font-size: 8px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    font-weight: 500;
    color: rgba(255, 255, 255, 0.72);
    white-space: nowrap;
}
.cobrand-card-logo {
    height: 20px;
    width: auto;
    opacity: 0.82;
}

/* ---------------------------------------------------------------------------
   Card front on narrow phones.

   The stack inside the front face costs a CONSTANT ~169px: 40px logo row, a
   60px points block, a 37px holder row and two 16px gaps, none of which scale.
   The face's height does scale — the card is aspect-[1.586/1] — so the two
   cross at roughly 380px of viewport. Below that the holder row (the customer's
   own name and their tier) hangs past the bottom edge and, because the face is
   overflow-hidden, is simply sliced off. Measured before this rule: 133.6px
   available against 169px needed at 320px, and 156.8 against 169 at 360px.

   Compressing is the only lever. The ratio is the brand's (a credit card), the
   card cannot grow taller, and the row that loses is the one naming the member.

   The cut-off is 383px: every width that actually slices is caught (375, the
   iPhone SE, was over by 2.7px), and 390 — the commonest phone, and the layout
   the client has already signed off — is left exactly as approved. It clears by
   6.7px there, which is thin but stable: nothing in this stack varies with the
   data. The name is `truncate`, so it cannot wrap, and the tier pill is
   shrink-0 with nowrap content, so a long name or a long tier does not add a
   single pixel of height. Only a font fallback with taller metrics could eat
   it, and that would be visible everywhere, not just here.

   Compound selectors throughout: this file is linked BEFORE tailwind.css, so a
   single class ties p-6 / mt-4 / text-4xl on specificity and loses on source
   order — it would parse, match, and be ignored, which looks exactly like a
   rule that never deployed.
   --------------------------------------------------------------------------- */
@media (max-width: 383px) {
    .card-face-front.loyalty-card-bg { padding: 1rem; }
    .loyalty-card-points.mt-4 { margin-top: 0.5rem; }
    .loyalty-card-holder.mt-4 { margin-top: 0.5rem; }
    .loyalty-card-balance.text-4xl { font-size: 1.875rem; line-height: 2.25rem; }
    /* The logo row reserves 2.5rem for a mark that is never that tall here. */
    .card-face-front .loyalty-card-logo { min-height: 2rem; }
}

/* A second step, for the narrowest phones still in use. The rule above brings
   360px from -12.2px to +23.8px of slack but leaves 320px at -1.4px — still
   slicing, just by less, which is the worst of both. Only the widths that
   actually need it are compressed this far; 360 and up keep the roomier
   treatment above. */
@media (max-width: 344px) {
    .card-face-front.loyalty-card-bg { padding: 0.85rem; }
    .loyalty-card-balance.text-4xl { font-size: 1.625rem; line-height: 2rem; }
    /* Horizontal, not vertical, and the reason is the same row. The tier pill
       is shrink-0, so at this width it takes what it needs and the member's own
       name is left with an ellipsis after five letters. The tier name stays;
       the subtitle beside it is decoration, and decoration does not outrank the
       cardholder's name on their own card. */
    .loyalty-card-holder .tier-pill-subtitle { display: none; }
}

/* Same pairing on the QR/scan back face. Two things differ from the front:
   the surface is the white scan panel, so this takes the dark wordmark and a
   darker caption; and the panel lives inside a fixed-aspect, overflow-hidden
   card, so the row must not grow. The logo is capped at 13px — a hair above
   the 8px caption — and the whole row is a flex line so the two never stack.
   Measured after the change: the panel keeps 66px of clearance at 320px and
   114px at 390px, and nothing inside it exceeds its box. */
.cobrand-qr-credit {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.3rem;
    max-width: 100%;
    padding-top: 1px;
}
.cobrand-qr-credit-text {
    font-size: 8px;
    line-height: 1;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    font-weight: 500;
    /* Inherited 0.42 from the plain-text credit this replaced, where the same
       alpha carried a whole sentence. Here it is the only text on the row, at
       8px, on solid white — 0.42 measures 2.69:1. 0.62 clears 4.5:1 and still
       reads as a caption rather than as a second heading. */
    color: rgba(15, 23, 42, 0.62);
    white-space: nowrap;
}
.cobrand-qr-logo {
    height: 13px;
    width: auto;
    flex-shrink: 0;
    opacity: 0.85;
}

/* ---------------------------------------------------------------------------
 * Carousels (news & rewards) — scroll-snap track + paging buttons.
 * ------------------------------------------------------------------------- */
.carousel-track {
    display: flex;
    gap: 1rem;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    padding-bottom: 0.25rem;
}
.carousel-track::-webkit-scrollbar { display: none; }
.carousel-track > * {
    scroll-snap-align: start;
    flex-shrink: 0;
}
.carousel-btn {
    width: 2.25rem;
    height: 2.25rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 9999px;
    background: rgba(20, 23, 26, 0.04);
    border: 1px solid rgba(20, 23, 26, 0.12);
    color: rgba(20, 23, 26, 0.75);
    transition: background 0.2s, opacity 0.2s;
}
.carousel-btn:hover:not(:disabled) {
    background: color-mix(in srgb, var(--brand-primary) 30%, transparent);
}
.carousel-btn:disabled { opacity: 0.35; cursor: default; }

/* News & campaigns cards */
.news-card {
    flex: 0 0 clamp(240px, 80vw, 300px);
    scroll-snap-align: start;
    display: flex;
    flex-direction: column;
    border-radius: 1rem;
    overflow: hidden;
    background: rgba(255, 255, 255, 0.75);
    border: 1px solid rgba(20, 23, 26, 0.08);
    text-decoration: none;
    color: inherit;
    transition: border-color 0.2s, transform 0.2s;
}
a.news-card:hover {
    border-color: color-mix(in srgb, var(--brand-primary) 55%, transparent);
    transform: translateY(-2px);
}
.news-card-media {
    aspect-ratio: 16 / 9;
    position: relative;
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
    background: linear-gradient(135deg,
        color-mix(in srgb, var(--brand-primary) 70%, transparent),
        color-mix(in srgb, var(--brand-secondary) 70%, transparent));
    color: rgba(255, 255, 255, 0.85);
    font-size: 2rem;
}
.news-card-media img { width: 100%; height: 100%; object-fit: cover; }
.news-card-body {
    padding: 1rem 1.1rem 1.2rem;
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
}
/* Joint "Brand x Partner" badge above a news card's title. The card body is
   near-white, so this pairs the brand mark with the partner's DARK wordmark.
   Heights are deliberately unequal: the partner wordmark is a heavy, wide
   slab and the brand mark is a compact stacked glyph, so equal heights make
   the partner visually dominate its own host. Sized so neither reads as the
   senior party — this badge is a pairing, not a hierarchy. */
.news-card-cobrand {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    margin-bottom: 0.15rem;
}
.news-card-cobrand-own { height: 24px; width: auto; flex-shrink: 0; }
.news-card-cobrand-own-text {
    font-weight: 700;
    font-size: 0.8rem;
    letter-spacing: 0.01em;
    color: var(--text-primary);
    /* nowrap with nothing to stop it is the one element in this badge that can
       leave the card: .news-card is overflow:hidden, so a long brand name is
       sliced mid-word rather than wrapped or scrolled. min-width:0 lets the
       flex item shrink at all; the ellipsis is the honest end of it. */
    white-space: nowrap;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}
.news-card-cobrand-x {
    font-size: 0.75rem;
    line-height: 1;
    color: rgba(20, 23, 26, 0.35);
    flex-shrink: 0;
}
/* The cap is the guard: .news-card is overflow:hidden, so a partner asset
   wider than the body is silently sliced rather than scrolled — and because
   this is a row flex item, its min-width:auto resolves to the transferred
   size (17px x the asset's own ratio), so without a cap it refuses to shrink
   at all. The asset path is deployment config, not a constant, so the ratio
   this has to survive is not the one shipped today.
   (A min-width:0 on the wrapper would do nothing here: .news-card-body is a
   COLUMN flex container, so width is its cross axis, where auto is already 0.) */
.news-card-cobrand-partner {
    height: 17px;
    width: auto;
    max-width: 60%;
    object-fit: contain;
    object-position: left center;
}
.news-card-body h3 { font-weight: 600; font-size: 1rem; color: var(--text-primary); }
.news-card-body p { font-size: 0.8rem; color: rgba(20, 23, 26, 0.6); line-height: 1.4; }
.news-card-cta {
    margin-top: 0.35rem;
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--brand-readable);
}

/* ---------------------------------------------------------------------------
 * Mobile bottom tab bar — native-app style primary navigation (md:hidden).
 * ------------------------------------------------------------------------- */
.mobile-bottom-nav {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 40;
    display: flex;
    align-items: stretch;
    justify-content: space-around;
    background: rgba(255, 255, 255, 0.92);
    backdrop-filter: blur(18px) saturate(140%);
    -webkit-backdrop-filter: blur(18px) saturate(140%);
    box-shadow: 0 -10px 30px -16px rgba(20, 23, 26, 0.22);
    border-top: 1px solid var(--border-soft);
    padding-bottom: env(safe-area-inset-bottom, 0px);
}

.mobile-bottom-nav a {
    position: relative;
    flex: 1 1 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    padding: 0.5rem 0 0.45rem;
    font-size: 10px;
    font-weight: 500;
    color: rgba(107, 114, 128, 0.85);
    transition: color 0.18s ease, transform 0.12s ease;
    -webkit-tap-highlight-color: transparent;
}

.mobile-bottom-nav a i {
    font-size: 17px;
    line-height: 1;
}

.mobile-bottom-nav a:active {
    transform: scale(0.9);
}

.mobile-bottom-nav a.is-active {
    /* Brand-primary alone measured only ~2.3:1 on the nav bar's near-white
       fill — still teal, but with enough dark blended in for real contrast. */
    color: color-mix(in srgb, var(--brand-primary) 70%, #14171a 30%);
}

.mobile-bottom-nav a.is-active i {
    filter: drop-shadow(0 0 9px color-mix(in srgb, var(--brand-primary) 65%, transparent));
}

/* Active-tab indicator pill along the top edge of the bar. */
.mobile-bottom-nav a.is-active::before {
    content: "";
    position: absolute;
    top: 0;
    width: 26px;
    height: 3px;
    border-radius: 0 0 3px 3px;
    background: var(--brand-primary);
    box-shadow: 0 0 10px color-mix(in srgb, var(--brand-primary) 70%, transparent);
}

/* Soft fade above the bar so scrolling content dissolves instead of hard-cutting
 * against the bar's top edge. */
.mobile-bottom-nav::after {
    /* Was a dark navy fade tuned for the old dark canvas — content scrolling
       through this 56px band now sits on the teal canvas, and a leftover
       near-black scrim here made dark text passing through it unreadable
       (confirmed: profile's "Λογαριασμός" heading went dark-on-near-black).
       A light frosted fade matching the nav bar's own tone instead. */
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: 100%;
    height: 56px;
    pointer-events: none;
    background: linear-gradient(to top,
        rgba(255, 255, 255, 0.75) 0%,
        rgba(255, 255, 255, 0.35) 32%,
        rgba(255, 255, 255, 0) 100%);
}

/* Scroll clearance so the last content always clears the fixed bottom bar. */
@media (max-width: 767px) {
    main {
        padding-bottom: calc(3.75rem + env(safe-area-inset-bottom, 0px));
    }
}

/* ---------------------------------------------------------------------------
 * Animated announcements banner — admin-managed, rotates at the top (mobile).
 * ------------------------------------------------------------------------- */
.announce-banner {
    position: sticky;
    top: 0;
    z-index: 30;
    flex-shrink: 0; /* it's a flex child of <main>; its items are absolute → don't collapse */
    /* Extend the banner bg up into the notch (standalone PWA) so the colour
     * continues seamlessly instead of a bright break. */
    padding-top: env(safe-area-inset-top, 0px);
    background:
        linear-gradient(90deg,
            color-mix(in srgb, var(--brand-primary) 24%, #0f172a),
            color-mix(in srgb, var(--brand-secondary) 20%, #0f172a));
    border-bottom: 1px solid color-mix(in srgb, var(--brand-primary) 32%, transparent);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
}

.announce-track {
    position: relative;
    height: 46px;
    overflow: hidden;
}

.announce-item {
    /* visibility, not opacity alone: these are real <a href> elements, and an
     * element at opacity 0 keeps its place in the tab order. Before this rule
     * a keyboard user could tab into an announcement that was invisible and
     * unreadable. `visibility: hidden` removes it from the tab order, and
     * unlike `display: none` it still transitions. */
    visibility: hidden;
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    gap: 0.5rem;
    /* Left 1rem, right 3.25rem. The right side is 44px of .announce-dismiss
     * plus an 8px gap, reserved rather than merely overlapped: the item is
     * inset:0 and would otherwise run the full width of the banner, so a long
     * Greek announcement would ellipsise at the screen edge and the last two
     * or three words would sit behind the X. With the padding it ellipsises
     * before the button, which is what .announce-text's text-overflow was
     * always meant to look like. */
    padding: 0 3.25rem 0 1rem;
    font-size: 13px;
    font-weight: 500;
    color: #f8fafc;
    text-decoration: none;
    opacity: 0;
    transform: translateY(7px);
    transition: opacity 0.5s ease, transform 0.5s ease;
    pointer-events: none;
}

.announce-item.is-active {
    visibility: visible;
    opacity: 1;
    transform: translateY(0);
    pointer-events: auto;
}

.announce-emoji {
    font-size: 15px;
    line-height: 1;
    flex-shrink: 0;
    filter: drop-shadow(0 0 6px color-mix(in srgb, var(--brand-primary) 45%, transparent));
}

.announce-text {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    letter-spacing: 0.005em;
}

.announce-arrow {
    font-size: 10px;
    opacity: 0.75;
    flex-shrink: 0;
}

@media (prefers-reduced-motion: reduce) {
    .announce-item { transition: opacity 0.2s ease; transform: none; }
}

/* The X that closes the banner.
 *
 * A sibling of .announce-track rather than a child of an .announce-item: items
 * with a link_url are <a> elements filling the whole banner (inset: 0), and a
 * <button> nested inside one is invalid markup and an unpredictable tap. Out
 * here it is a positioned element declared after the items, so it paints — and
 * takes the tap — above them. The z-index is belt and braces for the day
 * someone gives an item a z-index of its own.
 *
 * bottom: 0 rather than top: 0, on purpose. .announce-banner carries
 * padding-top: env(safe-area-inset-top) so its colour runs up into the notch,
 * and absolute offsets resolve against the PADDING box — anchoring to the top
 * would push the X down by the height of the notch, landing it in the middle
 * of nothing, on exactly the devices this banner exists for.
 *
 * 44 x 46 is the tap target: 46 is .announce-track's own height, so the banner
 * does not grow by a single pixel, and 44 is comfortably over the 40px floor. */
.announce-dismiss {
    position: absolute;
    right: 0;
    bottom: 0;
    z-index: 1;
    width: 44px;
    height: 46px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 0;
    background: transparent;
    color: #f8fafc;
    font-size: 13px;
    line-height: 1;
    opacity: 0.72;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

.announce-dismiss:hover { opacity: 1; }

/* :focus-visible, not :focus. iOS focuses a <button> on touch, so a plain
 * :focus outline would flash a keyboard ring on every thumb tap. */
.announce-dismiss:focus-visible {
    opacity: 1;
    border-radius: 6px;
    outline: 2px solid #f8fafc;
    outline-offset: -5px;
}

/* ---------------------------------------------------------------------------
 * Dismissal — and the mobile header that has to take the banner's place.
 *
 * partials/_mobile_header.html used to be an if/else: announcements OR the
 * mobile header, never both, so removing the banner left a phone with no
 * header at all — no brand mark, no link home, and no sidebar at that width to
 * fall back on. It now emits BOTH, and these three rules decide which one is
 * on screen. `data-announce-dismissed` is stamped on <html> either by the
 * inline pre-paint script in partials/_announcement_banner.html (on load, if
 * this exact set of announcements was already dismissed) or by
 * static/js/announcements.js (when the X is pressed).
 *
 * WHY display + !important and not the `hidden` attribute: that is the trap
 * static/js/messages.js documents. Tailwind's preflight ships
 * `[hidden]{display:none}` at specificity 0,1,0 and `.flex{display:flex}` at
 * the same 0,1,0 in the same stylesheet, utilities declared last — so an
 * element carrying class="flex" and a `hidden` attribute is fully visible
 * while claiming to be hidden. The standby header's classes are
 * `md:hidden … flex`, so `hidden` on it would do precisely nothing. These
 * selectors win on specificity instead (0,3,0 plus !important beats every
 * single-class utility no matter where it lands in the cascade), which is the
 * only kind of win that survives the next `npm run build:css`.
 * ------------------------------------------------------------------------- */
.announce-header-standby { display: none !important; }

:root[data-announce-dismissed="1"] .announce-banner { display: none !important; }

/* `not all and (min-width: 768px)` is the exact complement of Tailwind's `md:`
 * breakpoint, and it is written that way rather than as `max-width: 767px`
 * because those two are NOT the same query. A viewport of 767.5px — a zoomed
 * desktop, a split-screen tablet — satisfies neither `min-width: 768px` nor
 * `max-width: 767px`, so under a max-width rule the member would dismiss the
 * banner and be left with no header, which is the entire bug this block exists
 * to prevent. If tailwind.config.js ever moves the `md` breakpoint, move this
 * with it. */
@media not all and (min-width: 768px) {
    :root[data-announce-dismissed="1"] .announce-header-standby {
        display: flex !important;
    }
}

/* Menta cup mark on the loyalty-card front (replaces the tier star). */
.loyalty-card-cup {
    height: 2.5rem;
    width: auto;
    filter: drop-shadow(0 2px 9px rgba(0, 0, 0, 0.5));
    pointer-events: none;
}

/* ---------------------------------------------------------------------------
 * Intro splash — branded once-per-session loading screen (tiled "Menta" +
 * animated cup, then scales/fades to reveal the app).
 * ------------------------------------------------------------------------- */
:root[data-splash-done="1"] .menta-splash { display: none !important; }

.menta-splash {
    position: fixed;
    inset: 0;
    z-index: 200;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    /* EXPERIMENTAL light theme: Menta's actual dominant color (teal) as the
       splash canvas, not an invented cream — client-specified, see the
       --surface-bg comment above. Radial depth stays inside the teal family
       (lighter center, darker edge) so it still reads as "teal", just not flat. */
    background: radial-gradient(circle at 50% 38%,
        color-mix(in srgb, var(--brand-primary) 78%, white 22%) 0%,
        var(--brand-primary) 52%,
        color-mix(in srgb, var(--brand-primary) 78%, black 22%) 100%);
}
.menta-splash.is-hidden { display: none; }
/* pointer-events, not just opacity: `visibility` animates discretely, so it
   stays `visible` for the WHOLE fade and only flips at the end. Without this
   the overlay keeps swallowing taps and scrolls for the entire second during
   which the app underneath looks fully revealed — the screen says ready and
   the finger says nothing happens. Lengthening the splash stretched that dead
   window from ~0.8s to a full second, which is what made it worth fixing. */
.menta-splash.is-dismissing {
    animation: menta-splash-out 1s cubic-bezier(0.4, 0, 0.2, 1) forwards;
    pointer-events: none;
}
@keyframes menta-splash-out { to { opacity: 0; visibility: hidden; } }

/* Tiled brand wordmark — now actually legible (was 17% → invisible). A radial
   mask keeps it faint behind the logo and lets it read towards the edges. */
.menta-splash-pattern {
    position: absolute;
    inset: 0;                       /* viewport-box so the mask %s land on-screen */
    font-family: var(--brand-font-heading, "Geologica Brand", system-ui, sans-serif);
    font-weight: 800;
    font-size: clamp(3rem, 12vw, 6rem);
    line-height: 1.05;
    letter-spacing: -0.02em;
    /* EXPERIMENTAL light theme: the backdrop is teal now (not cream), so the
       tiled texture needs to be white-on-teal to stay visible, not teal-on-
       near-white. */
    color: rgba(255, 255, 255, 0.16);
    transform: rotate(-16deg) scale(1.4);   /* scale covers the corners the rotation exposes */
    user-select: none;
    pointer-events: none;
    -webkit-mask-image: radial-gradient(circle at 50% 42%, transparent 0%, transparent 17%, #000 54%);
            mask-image: radial-gradient(circle at 50% 42%, transparent 0%, transparent 17%, #000 54%);
    /* settles fast — must finish well before the dismiss, which is 2600ms
       since 2026-09-07 (was 1250ms, and this was 1.5s → never reached full) */
    animation: menta-pattern-in 0.6s ease-out forwards;
}
@keyframes menta-pattern-in {
    from { opacity: 0;    transform: rotate(-16deg) scale(1.52); }
    to   { opacity: 0.14; transform: rotate(-16deg) scale(1.4); }
}
.menta-splash.is-dismissing .menta-splash-pattern {
    animation: menta-pattern-out 0.78s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
@keyframes menta-pattern-out {
    to { opacity: 0; transform: rotate(-16deg) scale(1.9); }
}

/* Soft halo behind the logo — on a teal backdrop the logo mark is ALSO teal,
   so this can no longer be "more teal" (that erased it); it is now a light
   plate that gives the teal wordmark something to contrast against. */
.menta-splash-glow {
    position: absolute;
    top: 42%; left: 50%;
    width: 560px; height: 560px;
    transform: translate(-50%, -50%);
    background: radial-gradient(circle,
        rgba(255, 255, 255, 0.55) 0%,
        transparent 66%);
    filter: blur(30px);
    z-index: 1;
    pointer-events: none;
    animation: menta-glow-pulse 3.2s ease-in-out infinite;
}
@keyframes menta-glow-pulse {
    0%, 100% { opacity: 0.42; transform: translate(-50%, -50%) scale(0.94); }
    50%      { opacity: 0.62; transform: translate(-50%, -50%) scale(1.04); }
}

.menta-splash-center {
    position: relative;
    z-index: 2;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1.35rem;
    animation: menta-center-in 0.7s ease-out both;
}
@keyframes menta-center-in {
    from { opacity: 0; transform: translateY(10px) scale(0.96); }
    to   { opacity: 1; transform: translateY(0) scale(1); }
}
.menta-splash-logo {
    /* The logo mark is itself brand-teal, so on the now-teal splash canvas it
       needs real contrast, not just a soft halo — a solid white plate behind
       it, sized bigger than the visible glyph via padding. */
    width: 168px;
    height: auto;
    padding: 1.1rem 1.35rem;
    background: #ffffff;
    border-radius: 1.75rem;
    box-sizing: content-box;
    box-shadow: 0 16px 32px -8px rgba(11, 60, 58, 0.45);
    animation: menta-cup-bob 1.7s ease-in-out infinite;
}
@keyframes menta-cup-bob {
    0%, 100% { transform: translateY(0); }
    50%      { transform: translateY(-9px); }
}
.menta-splash-tag {
    font-family: var(--brand-font-heading, "Geologica Brand", system-ui, sans-serif);
    font-weight: 700;
    font-size: 0.72rem;
    /* Was 0.62em, sized for the single word "COFFEE" — this now renders
       Brand.tagline (admin-editable, can be a full sentence), so the
       spacing had to come down enough that a long value wraps instead of
       overflowing the viewport on narrow phones. */
    letter-spacing: 0.15em;
    text-transform: uppercase;
    text-align: center;
    max-width: 22rem;
    padding: 0 1.25rem;
    /* Teal-on-teal would vanish now that the backdrop itself is brand-primary. */
    color: #ffffff;
    opacity: 0.92;
    margin-top: -0.6rem;
}
/* "Powered by Buondi Craft" credit on the splash — same wording/logo as the
   card front, shown only when COBRAND_ENABLED (see apps.web.cobrand). */
.menta-splash-cobrand {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    /* Was 0.7rem/80% white and the client reported the credit "gets lost"
       (Bensasson, 2026-09-07). Both halves of that were true: 11px is below
       this design's smallest body size, and 80% white on the teal canvas is
       the faintest text on the screen. Size and opacity move together —
       enlarging faint text just makes it faint and large. */
    font-size: 0.95rem;
    color: rgba(255, 255, 255, 0.96);
    letter-spacing: 0.02em;
}
.menta-splash-cobrand img {
    /* Tracks the caption: the wordmark is the half that carries the partner's
       name, so it must not stay at the old size while the words grow. */
    height: 26px;
    width: auto;
    opacity: 1;
    /* The asset is a solid-white wordmark — its native color already reads
       fine on the teal backdrop, no recolor filter needed here. */
}
/* Determinate loading bar — reads as "loading" far better than 3 dots. */
.menta-splash-bar {
    position: relative;
    width: 150px;
    height: 4px;
    border-radius: 999px;
    background: color-mix(in srgb, var(--brand-primary) 16%, transparent);
    overflow: hidden;
    margin-top: 0.35rem;
}
.menta-splash-bar span {
    position: absolute;
    inset: 0;
    border-radius: 999px;
    background: var(--brand-gradient-h);
    transform-origin: left center;
    transform: scaleX(0);
    /* Duration is pinned to the dismissal timeout in base.html (2600ms). If
       one moves without the other the bar either finishes early and sits full
       while nothing happens, or is still filling when the splash disappears —
       both read as a stall rather than as loading. */
    animation: menta-bar-fill 2.6s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
@keyframes menta-bar-fill {
    0%   { transform: scaleX(0); }
    70%  { transform: scaleX(0.82); }
    100% { transform: scaleX(1); }
}
@media (prefers-reduced-motion: reduce) {
    .menta-splash-logo, .menta-splash-glow, .menta-splash-pattern,
    .menta-splash-center, .menta-splash-bar span { animation-duration: 0.01ms; animation-iteration-count: 1; }
    .menta-splash-bar span { transform: scaleX(1); }
    /* Pinned to the 200ms `fade` this visitor gets in base.html. Left at 1s it
       would outlive the display:none the script applies, cutting the fade off
       partway.
       Both dismissal rules need naming, and both need their selector repeated
       at full strength: the block above lists `.menta-splash-pattern` on its
       own (0,1,0), while the rule that actually animates it on the way out is
       `.menta-splash.is-dismissing .menta-splash-pattern` (0,3,0) and wins. So
       a visitor who asked for less motion was still shown the pattern rotating
       16 degrees and scaling to 1.9 — the exact gesture the request is about.
       Sitting inside the media query grants no extra weight. */
    .menta-splash.is-dismissing { animation-duration: 0.2s; }
    .menta-splash.is-dismissing .menta-splash-pattern { animation-duration: 0.01ms; }
}

/* Password show/hide toggle (wired by static/js/password-toggle.js).
   The button sits inside the field, so the input needs room on the right for
   it; 2.75rem keeps the 44px touch target from ever overlapping the text. */
.password-field {
    position: relative;
    display: block;
}

.password-field > input {
    padding-right: 2.75rem;
}

.password-toggle {
    position: absolute;
    top: 0;
    right: 0;
    height: 100%;
    width: 2.75rem;
    display: flex;
    align-items: center;
    justify-content: center;
    background: none;
    border: 0;
    cursor: pointer;
    color: rgba(148, 163, 184, 0.85);
    border-radius: 0 0.75rem 0.75rem 0;
}

.password-toggle:hover {
    color: var(--text-primary, #14171a);
}

.password-toggle:focus-visible {
    outline: 2px solid var(--brand-secondary);
    outline-offset: -2px;
}

/* Google/Apple sign-in buttons (login + register). Kept visually secondary —
   outlined, not solid — since the email-and-password route is still the main
   one and a solid white Google button would read as the primary action on a
   page whose primary action is the member's own account. */
.social-auth {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    margin-bottom: 1.25rem;
}

.social-auth-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.65rem;
    min-height: 48px;
    padding: 0.75rem 1rem;
    border-radius: 0.75rem;
    border: 1px solid rgba(20, 23, 26, 0.14);
    background: rgba(20, 23, 26, 0.02);
    color: var(--text-primary, #14171a);
    font-size: 0.8125rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    text-decoration: none;
    transition: background-color 0.18s ease, border-color 0.18s ease;
}

.social-auth-btn:hover,
.social-auth-btn:focus-visible {
    background: rgba(20, 23, 26, 0.05);
    border-color: rgba(20, 23, 26, 0.24);
}

.social-auth-mark {
    width: 1.15rem;
    height: 1.15rem;
    flex-shrink: 0;
}

.social-auth-btn[data-provider="apple"] .social-auth-mark {
    color: var(--text-primary, #14171a);
}

.social-auth-divider {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    margin-top: 0.5rem;
    color: #334155;
    font-size: 0.6875rem;
    letter-spacing: 0.12em;
}

.social-auth-divider::before,
.social-auth-divider::after {
    content: "";
    flex: 1;
    height: 1px;
    background: rgba(20, 23, 26, 0.12);
}

/* The identity the provider just confirmed, shown on the consent page. It is
   read-only on purpose — everything the member can still decide sits below it
   in real form controls. */
.social-identity {
    border: 1px solid rgba(20, 23, 26, 0.08);
    background: rgba(20, 23, 26, 0.03);
    border-radius: 0.75rem;
    padding: 0.85rem 1rem;
}

/* Disabled until the terms box is ticked. The attribute is set by script, so
   this rule only ever paints a state the member can undo in one click. */
.social-auth [disabled],
button[data-consent-submit][disabled] {
    opacity: 0.45;
    cursor: not-allowed;
}

/* Featured rewards drop to one column between 768px and 1279px.
 *
 * The grid's own two-up rule is `sm:grid-cols-2`, which asks the *viewport*
 * whether there is room while the grid actually lives in the dashboard's
 * 7-of-12 column. Those two stop agreeing the moment the sidebar appears at
 * 768px: the column is 448px there and 397px at 1024px, so each card gets
 * ~191px, and a card spends 64px on its icon and 44px on its redeem button
 * before the name gets a pixel. The name was rendering into a 33px box —
 * "Δωρεάν Cappuccino" came out as "Δωρ Cap".
 *
 * Written as a plain media query rather than an `xl:grid-cols-2` utility
 * because the constraint is the container's width, not the viewport's. The
 * utility would happen to line up today and stop lining up the first time the
 * column split or the sidebar width changes; this rule says what it means.
 * (`xl:grid-cols-2` is also absent from the compiled stylesheet — a rebuild
 * would add it, and reproduces the committed file byte-for-byte, so that was
 * an available option rather than a blocked one.)
 */
@media (min-width: 768px) and (max-width: 1279px) {
    /* Compound selector on purpose. This file is linked BEFORE tailwind.css,
     * so a single class ties Tailwind's own `.sm\:grid-cols-2` on specificity
     * and loses the tie on source order — the rule parsed, matched, and was
     * overridden, which looks exactly like a rule that was never deployed.
     * `.featured-rewards-grid.grid` is 0,2,0 and wins without !important. */
    .featured-rewards-grid.grid {
        grid-template-columns: minmax(0, 1fr);
    }
}

/* The App Store and Google Play badges (templates/partials/_store_badges.html).
 * Both stores publish the same two rules for their artwork, and the padding
 * and the height here are those rules: the badge is never drawn under 40px
 * tall, and a quarter of its height (10px) stays clear on every side. The
 * padding is on the link, so the clear space is also what makes the target
 * 60px tall to a finger. `width: auto` keeps the artwork's own proportions. */
.store-badges {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
}

.store-badge {
    display: inline-block;
    padding: 10px;
    line-height: 0;
    border-radius: 16px;
}

.store-badge img {
    display: block;
    height: 40px;
    width: auto;
    max-width: none;
}

.store-badge:focus-visible {
    outline: 2px solid var(--brand-primary);
    outline-offset: -2px;
}
