/* Lovi Face Scanner — styles
   ===========================
   Portrait iPad kiosk. Full-screen, no scroll, Inter font. Lovi palette.
*/

@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');

:root {
    /* Brand palette — every CSS var below can be overridden at runtime
       by `applyDesignStyleVars(cfg)` in app.js, which pushes the
       corresponding RUNTIME_DEFAULTS / runtime-config entry onto
       :root. These defaults mirror the config.js defaults so a first
       paint before JS runs still looks right. */
    --brand-accent:           #5163FF;
    --brand-success:          #57DE98;
    --brand-dark:             #0A0A0F;
    --brand-deep:             #151581;
    --gradient-top:           #8DA1FF;
    --gradient-mid:           #B4A6E8;
    --gradient-bottom:        #D4C8F0;
    --gradient-sticky-mid:    #9AAEF0;
    --victory-frame-border:   var(--brand-success);
    --victory-frame-radius-base:   6px;
    --victory-frame-radius-expand: 6px;
    --progress-fill-start:    var(--brand-accent);
    --progress-fill-end:      var(--brand-success);
    --routine-selector-active-bg:   #ffffff;
    --routine-selector-active-text: var(--brand-deep);
    --answer-pill-bg:         rgba(255, 255, 255, 0.18);
    --answer-pill-text:       rgba(255, 255, 255, 0.92);
    --question-title-size:       28px;
    --question-subtitle-size:    15px;
    --option-label-size:         15px;
    --assistant-response-size:   16px;
    --routine-heading-size:      20px;
    --product-name-size:         14px;
    --product-brand-size:        12px;
    --product-step-size:         11px;
    --intro-cta-size:            22px;

    /* Legacy aliases — the rest of the stylesheet (and one or two JS
       callers) still reads these names. Redirected so they track the
       new brand palette without having to sweep the whole file. */
    --color-primary: var(--brand-deep);
    --color-accent:  var(--brand-accent);
    --color-green:   var(--brand-success);
    --color-text: #292824;
    --color-bg:      var(--brand-dark);
    --color-bg-light: #F6F6FA;
    --color-pill-bg: rgba(255, 255, 255, 0.12);
    --color-pill-bg-active: rgba(81, 99, 255, 0.35);
    --color-pill-bg-done: rgba(87, 222, 152, 0.35);
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    -webkit-tap-highlight-color: transparent;
}

html, body {
    /* iOS Safari's 100vh is based on the max viewport (URL bar hidden), so
       the page briefly renders at the wrong height while the URL bar state
       settles. 100dvh tracks the dynamic viewport so the initial paint
       matches what the user actually sees. */
    height: 100vh;
    height: 100dvh;
    width: 100vw;
    overflow: hidden;
    user-select: none;
    -webkit-user-select: none;
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
    background: var(--color-bg);
    color: #fff;
    -webkit-font-smoothing: antialiased;
}

/* ─── Screen framework ─────────────────────────────────────────── */

/* All screens are always in the DOM — visibility toggled via opacity +
   visibility + pointer-events so inactive → active swaps can cross-fade
   without display:none yanking animations mid-transition. */
.screen {
    display: flex;
    position: fixed;
    top: 0;
    left: 0;
    width: 100vw;
    /* See html, body — 100dvh keeps the screen sized to the live viewport
       so it doesn't pop from "small viewport" to full once Safari hides
       the URL bar. */
    height: 100vh;
    height: 100dvh;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 48px 32px;
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity 0.5s ease, visibility 0s linear 0.5s;
}
.screen.active {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transition: opacity 0.5s ease, visibility 0s linear 0s;
}

/* Saved-result fast-path — the inline <head> script sets
   `html.saved-result-boot` the instant the URL carries `?r=<hash>`,
   before the browser has a chance to paint. `bootFromSavedResult`
   mounts the fully-finished victory state synchronously at module
   load, so the intro screen's "Tap anywhere to start" CTA and the
   progress-bar fade-out animation are pure noise on this path:
     1. Hide #screen-intro entirely so it never paints.
     2. Kill the .screen cross-fade so #screen-victory snaps in.
     3. Kill the .victory-progress opacity/max-height transitions —
        bootFromSavedResult lands `.routine-ready` upfront, which
        would otherwise animate the bars out over ~1 s.
   The live-flow (no `?r`) path never gets this class, so its
   animations keep running as normal. */
html.saved-result-boot #screen-intro { display: none; }
html.saved-result-boot .screen { transition: none; }
html.saved-result-boot .victory-progress { transition: none; }

/* ─── Intro ────────────────────────────────────────────────────── */

#screen-intro {
    padding: 0;
    background: #000;
    overflow: hidden;
    cursor: pointer;
    /* Fallback gradient while the video is still loading */
    background-image: linear-gradient(180deg, var(--brand-deep) 0%, var(--brand-dark) 100%);
}
.intro-video {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    z-index: 1;
    background: #000;
    pointer-events: none;
}
.intro-overlay {
    position: absolute;
    inset: 0;
    z-index: 2;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-end;
    padding-bottom: clamp(48px, 15vh, 160px);
    /* Subtle vignette at the bottom so the CTA reads on any frame */
    background: linear-gradient(to bottom, transparent 45%, rgba(0, 0, 0, 0.55) 100%);
    pointer-events: none;
}
.intro-cta {
    font-size: var(--intro-cta-size, 22px);
    font-weight: 600;
    letter-spacing: -0.2px;
    padding: 14px 30px;
    background: rgba(255, 255, 255, 0.18);
    color: #fff;
    border-radius: 999px;
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
    animation: intro-pulse 2.2s ease-in-out infinite;
    max-width: 90vw;
    text-align: center;
}
@keyframes intro-pulse {
    0%, 100% { opacity: 0.72; }
    50%      { opacity: 1;    }
}
@media (max-width: 640px) {
    .intro-cta { font-size: 17px; padding: 11px 22px; }
}

/* ─── Scanner ──────────────────────────────────────────────────── */

#screen-scanner {
    padding: 0;
    /* Periwinkle → lilac gradient echoes the intro video, visible for a
       beat while the camera stream is initialising. */
    background: linear-gradient(180deg, var(--gradient-top) 0%, var(--gradient-mid) 60%, var(--brand-dark) 100%);
}
.camera-video {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    /* Front camera is mirrored for natural selfie view; also apply the
       runtime digital zoom. Both scale-X negative and scale factor are
       combined into a single matrix so browsers don't drop the mirror. */
    transform: scale(calc(-1 * var(--camera-zoom, 1)), var(--camera-zoom, 1));
    transform-origin: center center;
    z-index: 1;
}
.overlay-canvas {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    z-index: 3;
    pointer-events: none;
    /* Mirror + zoom in lockstep with the video so landmarks stay aligned. */
    transform: scale(calc(-1 * var(--camera-zoom, 1)), var(--camera-zoom, 1));
    transform-origin: center center;
}

/* Vignette — dims everything outside an oval so the face is visually framed
   during the scan. Sits BETWEEN the video (z:1) and the mesh overlay (z:3)
   so the mesh itself stays fully opaque on top.
   Radii are in vmin units so the oval stays face-shaped in both portrait
   AND landscape (kiosk orientation depends on which edge the camera is on).
   Hidden until the camera stream is actually playing — avoids showing the
   framing oval over the placeholder gradient during camera init. */
.viewport-mask {
    position: absolute;
    inset: 0;
    z-index: 2;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.4s ease;
    background: radial-gradient(
        ellipse
            calc(var(--mask-rx, 0.24) * 100vmin)
            calc(var(--mask-ry, 0.34) * 100vmin)
            at 50% var(--mask-cy, 46%),
        transparent 0%,
        transparent calc(100% - var(--mask-feather, 6%)),
        rgba(0, 0, 0, var(--mask-alpha, 0.55)) 100%
    );
}
.viewport-mask.ready { opacity: 1; }

.scanner-top, .scanner-bottom {
    position: absolute;
    left: 0;
    right: 0;
    z-index: 4;
    pointer-events: none;
    text-align: center;
}
.scanner-top {
    top: calc(env(safe-area-inset-top, 0) + 24px);
}
.scanner-bottom {
    bottom: calc(env(safe-area-inset-bottom, 0) + 28px);
}
.scanner-hint {
    display: inline-block;
    padding: 10px 20px;
    background: rgba(0, 0, 0, 0.35);
    border-radius: 999px;
    font-size: 15px;
    font-weight: 500;
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
}

.region-pills {
    display: inline-flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 8px;
    padding: 10px 14px;
    background: rgba(0, 0, 0, 0.35);
    border-radius: 18px;
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    max-width: 92vw;
}
.pill {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    min-width: 120px;
    padding: 8px 14px;
    border-radius: 14px;
    background: var(--color-pill-bg);
    font-size: 13px;
    font-weight: 500;
    color: rgba(255, 255, 255, 0.78);
    transition: background 0.4s ease, color 0.4s ease;
    white-space: nowrap;
}
.pill-label {
    font-size: 13px;
    font-weight: 500;
}
.pill-debug {
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 10px;
    opacity: 0.7;
    letter-spacing: -0.2px;
    white-space: pre;
    font-variant-numeric: tabular-nums;
}
.pill.capturing {
    background: var(--color-pill-bg-active);
    color: #fff;
}
.pill.done {
    background: var(--color-pill-bg-done);
    color: #fff;
}

/* ─── Compact tuning for small viewports ──────────────────────── */
/* Triggers for either a narrow width (portrait phones ≈ 390 px) OR a
   short height (landscape phones ≈ 375–400 px tall). In landscape kiosk
   mode the iPad keeps the wide layout, but if the scanner is ever loaded
   on a landscape phone we still need compact chrome. */
@media (max-width: 640px), (max-height: 500px) {
    .scanner-top {
        top: calc(env(safe-area-inset-top, 0) + 10px);
    }
    .scanner-bottom {
        bottom: calc(env(safe-area-inset-bottom, 0) + 12px);
    }
    .scanner-hint {
        padding: 7px 14px;
        font-size: 13px;
        line-height: 1.35;
        max-width: 92vw;
    }
    .region-pills {
        gap: 4px;
        padding: 6px 6px;
        border-radius: 12px;
        max-width: 98vw;
        flex-wrap: nowrap;
    }
    .pill {
        min-width: 0;
        flex: 1 1 0;
        padding: 5px 6px;
        border-radius: 9px;
        gap: 1px;
    }
    .pill-label { font-size: 10px; font-weight: 600; }
    .pill-debug { font-size: 9px; }
}

/* ─── Victory ──────────────────────────────────────────────────── */

/* The victory screen is the only screen that scrolls. Its sticky
   header (title + 1×3 image row + progress bars) stays pinned at the
   top, and interpolates its size via the --victory-expand variable
   (0 = fully collapsed, 1 = full). JS writes --victory-expand based
   on screen.scrollTop so shrinking tracks the scroll position exactly. */
#screen-victory {
    /* Pulled toward the intro palette — soft periwinkle → lilac. */
    background: linear-gradient(180deg, var(--gradient-top) 0%, var(--gradient-mid) 55%, var(--gradient-bottom) 100%);
    text-align: center;
    padding: 0;
    overflow-y: auto;
    overflow-x: hidden;
    justify-content: flex-start;
    align-items: center;
    /* --victory-expand is written live from JS. Fallback 1 = expanded,
       which is the correct initial state before anything scrolls. */
    --victory-expand: 1;
    -webkit-overflow-scrolling: touch;
    /* Block rubber-band bounce in both directions. At the top, bounce
       would pull the sticky header (photos + progress) *down* with the
       content because position:sticky's top:0 threshold only activates
       once the natural scroll range is entered; the bounce itself has
       scrollTop clamped to 0, so nothing pins and the whole sticky
       drifts with the gesture. `none` stops that — the top of the
       viewport always belongs to the photos. Also useful at the bottom
       on trackpads where the overshoot momentarily exposes the area
       below the content box. */
    overscroll-behavior: none;
}
.victory-sticky {
    position: sticky;
    top: 0;
    z-index: 10;
    width: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    /* Solid background so content scrolling underneath doesn't bleed
       through the sticky — the screen's gradient already matches. */
    background: linear-gradient(180deg, var(--gradient-top) 0%, var(--gradient-sticky-mid) 100%);
    padding:
        calc(16px + 18px * var(--victory-expand)) 24px
        calc(10px + 6px * var(--victory-expand));
    gap: calc(6px + 6px * var(--victory-expand));
    /* Drop a soft shadow once the user starts scrolling, so the sticky
       reads as a floating header rather than just a frozen strip. */
    box-shadow: 0 calc(2px + 4px * (1 - var(--victory-expand)))
                  calc(8px + 12px * (1 - var(--victory-expand)))
                  rgba(21, 21, 129, calc(0.10 + 0.18 * (1 - var(--victory-expand))));
    transition: padding 0.08s ease, gap 0.08s ease;
}
/* Row around the photo grid. The photo grid keeps its original flex-
   style centering (margin: 0 auto inside a flex row), so its size is
   unchanged from the pre-save-aside layout. The aside is absolutely
   positioned at the right edge of the row, sitting in the empty space
   beside the centered grid — this is the "only as addition" behaviour:
   the photos stay horizontally centered as if nothing else were there.
   On narrow viewports the aside drops under the photos via the media
   query below. */
.victory-sticky-row {
    position: relative;
    width: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
}
/* No-op spacer — kept in the DOM for narrow-viewport layouts where
   we swap to flex-column, so the aside has a predictable slot order. */
.victory-save-spacer { display: none; }
.victory-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: calc(6px + 6px * var(--victory-expand));
    width: 100%;
    /* Smoothly interpolates from ~340px (collapsed, each item ~80px
       wide) to 760px (expanded, each item ~180px wide). The grid now
       holds four zoomed crops (forehead, midface, L/R cheek) instead
       of three full-face captures — the extra column widens the
       max-width a touch so each thumb stays readable. */
    max-width: calc(340px + 420px * var(--victory-expand));
    margin: 0;
}

/* Save-the-results QR aside. Pinned on the right side of the sticky
   header row. Width shrinks with --victory-expand so the collapsed
   strip doesn't carry a big QR; the QR is legible at ~110 px. The
   ::before label sits above the QR panel and mirrors the "place
   here" position in the spec mockup — concise caption above the
   square so the QR itself dominates.
   Hidden (with `hidden` attr) until saveVictorySnapshot resolves a
   hash; on the /result/<hash> rehydration path it's unhidden the
   moment the screen mounts. */
.victory-save-aside {
    /* Floats at the right edge of the sticky row; the photos stay flex-
       centered in the row above/behind it. Vertically centered against
       the photo row so the QR sits at the same height as the photos.
       Width interpolates with --victory-expand so the QR is legible when
       the header is expanded (~140 px, comfortable scan distance from a
       phone) and shrinks cleanly on collapse. */
    position: absolute;
    right: 0;
    top: 50%;
    transform: translateY(-50%);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: calc(4px + 4px * var(--victory-expand, 1));
    width: calc(60px + 80px * var(--victory-expand, 1));
    opacity: calc(0.4 + 0.6 * var(--victory-expand, 1));
    transition: opacity 0.2s ease;
}
.victory-save-aside[hidden] { display: none; }
.victory-save-qr-code {
    width: 100%;
    aspect-ratio: 1 / 1;
    background: #fff;
    border-radius: calc(6px + 4px * var(--victory-expand, 1));
    padding: calc(3px + 3px * var(--victory-expand, 1));
    display: flex;
    align-items: center;
    justify-content: center;
    /* Match the photo frames' faint outer ring so the QR reads as
       part of the same row of artifacts rather than floating chrome. */
    box-shadow: 0 1px 2px rgba(21, 21, 129, 0.18);
}
.victory-save-qr-code a,
.victory-save-qr-code img {
    width: 100%;
    height: 100%;
    display: block;
    image-rendering: pixelated;
}
.victory-save-qr-code a { text-decoration: none; }
.victory-save-qr-label {
    font-size: calc(9px + 3px * var(--victory-expand, 1));
    font-weight: 500;
    color: rgba(255, 255, 255, calc(0.5 + 0.4 * var(--victory-expand, 1)));
    text-align: center;
    line-height: 1.2;
    max-height: calc(var(--victory-expand, 1) * 32px);
    overflow: hidden;
}
/* Mobile-only tap-through pill, injected into the aside by
   `renderVictoryAsideQR` on saved-result pages. Hidden by default —
   only the <=640 px block below reveals it (and hides the QR + label
   it replaces). Desktop / tablet keep the QR as the primary affordance
   because scanning a kiosk QR is the whole point on those form
   factors. */
.victory-save-cta { display: none; }
/* Phone portrait (≈390 px wide). The 4×1 desktop row crushed each
   thumb to ~70 px; instead stack photos 2×2 across the full sticky
   width (~160 × 120 each), drop the Save-QR underneath as a tap
   pill ("Get Lovi app"), and cut the italic Playfair quote — at
   phone width it wraps to 4–5 lines and buries the routine. */
@media (max-width: 640px) {
    .victory-sticky {
        padding:
            calc(10px + 8px * var(--victory-expand, 1)) 14px
            calc(6px + 4px * var(--victory-expand, 1));
        gap: calc(4px + 4px * var(--victory-expand, 1));
    }
    .victory-sticky-row {
        flex-direction: column;
        align-items: center;
        justify-content: center;
        gap: calc(6px + 4px * var(--victory-expand, 1));
    }
    .victory-grid {
        grid-template-columns: repeat(2, 1fr);
        gap: calc(3px + 3px * var(--victory-expand, 1));
        /* Grid width tracks --victory-expand so photos shrink with the
           sticky on scroll instead of pinning the collapsed header at a
           tall fixed size. Expanded (1) ≈ 300 px → ~147 × 110 thumbs;
           collapsed (0) ≈ 200 px → ~97 × 73 thumbs. */
        max-width: calc(200px + 100px * var(--victory-expand, 1));
        width: 100%;
        flex: none;
    }
    .victory-item-label {
        font-size: calc(8px + 3px * var(--victory-expand, 1));
        max-height: calc(var(--victory-expand, 1) * 16px);
    }
    .victory-save-aside {
        /* Full-width row below the photo block. The QR + label are
           hidden (below) and the CTA pill is the only visible child,
           centered. Opacity stays full — no desktop-style fade. */
        position: static;
        transform: none;
        width: 100%;
        flex: none;
        opacity: 1;
        gap: 0;
    }
    .victory-save-aside .victory-save-qr-code,
    .victory-save-aside .victory-save-qr-label { display: none; }
    .victory-save-cta {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        gap: 6px;
        padding: 9px 20px;
        border-radius: 999px;
        background: rgba(255, 255, 255, 0.22);
        border: 1px solid rgba(255, 255, 255, 0.35);
        color: #fff;
        font-family: inherit;
        font-size: 13px;
        font-weight: 600;
        letter-spacing: -0.1px;
        text-decoration: none;
        backdrop-filter: blur(8px);
        -webkit-backdrop-filter: blur(8px);
        /* Collapse with the sticky so the pill disappears in the slim
           scrolled state rather than carrying dead chrome past the
           fold. The photo block still reads on its own. */
        opacity: var(--victory-expand, 1);
        max-height: calc(var(--victory-expand, 1) * 40px);
        overflow: hidden;
        transition: opacity 0.08s ease, max-height 0.08s ease;
    }
    .victory-save-cta::after {
        content: '→';
        opacity: 0.85;
        font-weight: 500;
    }
    /* Routine body — tighter gutters and no italic quote. */
    .victory-body {
        padding: 14px 14px 40px;
        gap: 10px;
    }
    .victory-routine { margin-bottom: 12px; }
    .victory-assistant-text { display: none; }
    .routine-selector { padding: 10px 0 20px; gap: 4px; }
    .routine-selector-btn {
        padding: 6px 12px;
        font-size: 12px;
    }
}
.victory-item {
    display: flex;
    flex-direction: column;
    gap: 3px;
}
/* Two <img>s layered — the captured photo sits below, the visualization
   overlay on top with opacity 0 until the swap triggers. The per-item
   transition duration is driven by --viz-transition-seconds, set from
   runtime config in app.js so setup.html can tune it without a rebuild. */
.victory-item-frame {
    position: relative;
    width: 100%;
    aspect-ratio: 4 / 3;
    /* Smaller radius + thinner border on collapse — the big green frame
       reads as decoration when the image is thumbnail-sized. The base
       + expand components are both runtime-configurable so the design
       team can reshape the frame without touching CSS. */
    border-radius: calc(var(--victory-frame-radius-base, 6px) + var(--victory-frame-radius-expand, 6px) * var(--victory-expand, 1));
    overflow: hidden;
    border: calc(1px + 1px * var(--victory-expand, 1)) solid var(--victory-frame-border);
    background: rgba(255, 255, 255, 0.06);
}
.victory-item-img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
.victory-item-img--viz {
    opacity: 0;
    transition: opacity var(--viz-transition-seconds, 0.7s) ease;
}
.victory-item.viz-shown .victory-item-img--viz { opacity: 1; }
/* Mesh overlay — a static canvas sitting on top of both the original and
   viz layers, drawn once at victory-screen mount using the landmarks we
   snapshotted at capture time. Fades in together with the viz layer (on
   `.viz-shown`) so the mesh only appears at the moment of the swap —
   before that the captured photo reads as a plain selfie, matching the
   "raw" state; once the analysis arrives, mesh + viz reveal together.
   Pointer-events disabled so taps land on the frame itself. */
.victory-item-mesh {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    pointer-events: none;
    display: block;
    opacity: 0;
    transition: opacity var(--viz-transition-seconds, 0.7s) ease;
}
.victory-item.viz-shown .victory-item-mesh { opacity: 1; }
.victory-item-label {
    font-size: calc(9px + 4px * var(--victory-expand, 1));
    opacity: calc(0.15 + 0.6 * var(--victory-expand, 1));
    /* Collapse to zero when fully shrunk so the label doesn't clutter
       the compact strip — image alone is enough visual context. */
    max-height: calc(var(--victory-expand, 1) * 20px);
    overflow: hidden;
    line-height: 1.25;
}

/* ─── Victory progress bars ────────────────────────────────────
   Live inside the sticky header alongside the image row. Each bar
   eases to 90% over its target window, then snaps to 100% on the real
   completion event (Phase B, Phase C, or /routine_recommendation).
   All three bars stay visible at 100% after completion — the routine
   renders below in .victory-body and the page auto-scrolls so the
   sticky collapses into its compact state. */
/* Progress-bar block — fades out when `.routine-ready` lands on the sticky.
   Timing values are pushed as custom properties from JS (applyStageTimings)
   with the fallbacks below matching RUNTIME_DEFAULTS so the CSS still reads
   correctly before any JS runs. */
.victory-progress {
    width: 100%;
    max-width: calc(300px + 340px * var(--victory-expand, 1));
    display: flex;
    flex-direction: column;
    gap: calc(5px + 5px * var(--victory-expand, 1));
    margin: 0;
    max-height: 320px;
    overflow: hidden;
    /* Phase 1: fade opacity (fast). Phase 2: collapse max-height (delayed
       so the bars don't snap-drop out of the sticky while still visible). */
    transition:
        opacity    var(--bars-fade-out, 350ms) ease,
        max-height 400ms ease var(--bars-collapse-delay, 600ms);
}
.victory-sticky.routine-ready .victory-progress {
    opacity: 0;
    pointer-events: none;
    max-height: 0;
}

/* Assistant response — rendered inside .victory-routine right under the
   "Your personalized routine" heading. Italic Playfair for quote-like
   voice, centered, balanced wrap. Target width is ~60 % larger than the
   760 px routine column (~896 px on wide screens); positioned around
   the parent's midline with left:50% + translateX(-50%) so the bleed
   stays symmetric even when max-width clamps the natural width (a plain
   negative-margin bleed shifts the box left because CSS max-width clips
   from the right edge). Capped to the viewport with 16 px breathing on
   narrow screens. */
.victory-assistant-text {
    position: relative;
    left: 50%;
    transform: translateX(-50%);
    margin: 0 0 6px;
    width: calc(100% + 336px);
    max-width: min(896px, calc(100vw - 32px));
    padding: 4px 8px 0;
    font-family: 'Playfair Display', 'Georgia', serif;
    font-style: italic;
    font-weight: 400;
    font-size: var(--assistant-response-size, 16px);
    line-height: 1.55;
    letter-spacing: 0.15px;
    color: rgba(255, 255, 255, 0.88);
    text-align: center;
    text-wrap: balance;
}
.progress-row {
    display: flex;
    flex-direction: column;
    gap: calc(3px + 3px * var(--victory-expand, 1));
}
.progress-row[hidden] { display: none; }
.progress-label {
    font-size: calc(10px + 3px * var(--victory-expand, 1));
    font-weight: 500;
    color: rgba(255, 255, 255, calc(0.5 + 0.35 * var(--victory-expand, 1)));
    text-align: left;
    letter-spacing: -0.1px;
    line-height: 1.25;
    /* 14px floor at collapse leaves room for the 10px line + descenders
       while still reading as "compact" next to the track. */
    max-height: calc(14px + 8px * var(--victory-expand, 1));
    overflow: hidden;
}
.progress-track {
    position: relative;
    height: calc(3px + 3px * var(--victory-expand, 1));
    width: 100%;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.18);
    overflow: hidden;
}
.progress-fill {
    position: absolute;
    inset: 0 auto 0 0;
    width: 0%;
    background: linear-gradient(90deg, var(--progress-fill-start) 0%, var(--progress-fill-end) 100%);
    border-radius: 999px;
    transition: none;
}

/* Body block below the sticky header — holds description, continue
   button, routine, Start Over. Scrolls under the sticky. Width fits
   the routine (560 px) + the sticky QR aside (180 px) + 24 px gap. */
.victory-body {
    width: 100%;
    max-width: 800px;
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 20px 24px 48px;
    gap: 14px;
}

/* ─── Victory — routine slot ─────────────────────────────────
   Holds the heading, assistant response, variant selector, then a
   two-column layout: rituals on the left, sticky QR panel on the
   right. The QR panel position:sticky inside the victory scroll
   container (offset by --victory-sticky-height, written live from
   JS via a ResizeObserver) so it pins to just below the shrunken
   header as the user scrolls the rituals. */
.victory-routine {
    width: 100%;
    max-width: 760px;
    margin: 0 0 18px;
    display: flex;
    flex-direction: column;
    gap: 14px;
    text-align: left;
}
.victory-routine[hidden] { display: none; }

/* Variant selector — three pills in a row. Pinned at the top of the
   scroll area (just under .victory-sticky) so the user can hop
   between Essential / Advanced / All at any scroll depth.
   The selector box carries a vertical gradient that is opaque at the
   top (matching the page background colour at this Y, so product
   cards scrolling in from below are hidden as they pass behind the
   pills) and fades to fully transparent at the bottom, so there's no
   visible rectangular edge — the routine below bleeds up into the
   fade zone rather than cutting off against a hard shelf.
   Its height is written by JS into --routine-selector-height for the
   sticky layers (ritual headers, QR aside) below it to offset from. */
.routine-selector {
    display: flex;
    justify-content: center;
    gap: 6px;
    width: 100%;
    position: sticky;
    top: var(--victory-sticky-height, 120px);
    z-index: 5;
    /* Extra bottom padding gives the fade room to play out below the
       pill row. Top padding keeps the pills' top edge off the sticky
       photo-strip header above. */
    padding: 12px 0 28px;
    background: transparent;
}
/* Page-gradient-matching backdrop on a pseudo-element so the pills
   themselves stay untouched. The ::before replays the SAME screen
   gradient as #screen-victory, sized to the full viewport height and
   offset upward by the selector's current viewport top so its visible
   slice lines up exactly with the page gradient behind it. The offset
   var `--selector-viewport-top` is written from JS on every scroll /
   resize / sticky-height change (see setVictoryExpand in app.js).
   Previously this used `background-attachment: fixed`, which iOS
   Safari paints unreliably inside a position:fixed scroll container —
   the whole gradient palette ended up compressed into the selector's
   ~84 px box, producing the lilac band you'd see mid-scroll. A mask
   fades the backdrop to transparent at the bottom, producing the soft
   edge without any visible rectangular band. */
.routine-selector::before {
    content: '';
    position: absolute;
    inset: 0;
    pointer-events: none;
    background: linear-gradient(180deg, var(--gradient-top) 0%, var(--gradient-mid) 55%, var(--gradient-bottom) 100%);
    background-size: 100% 100dvh;
    background-repeat: no-repeat;
    background-position: 0 calc(-1 * var(--selector-viewport-top, 0px));
    -webkit-mask-image: linear-gradient(to bottom, black 0%, black 65%, transparent 100%);
            mask-image: linear-gradient(to bottom, black 0%, black 65%, transparent 100%);
}
/* Pills sit on top of the ::before backdrop. Without explicit
   positioning they'd still paint on top (::before comes first in
   paint order), but `position: relative` guarantees it across
   edge cases and avoids the `z-index: -1` dance. */
.routine-selector-btn {
    position: relative;
}
.routine-selector-btn {
    appearance: none;
    border: 1px solid rgba(255, 255, 255, 0.35);
    background: rgba(255, 255, 255, 0.12);
    color: rgba(255, 255, 255, 0.88);
    padding: 8px 16px;
    border-radius: 999px;
    font-size: 13px;
    font-weight: 600;
    letter-spacing: -0.1px;
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
    font-family: inherit;
}
.routine-selector-btn:hover {
    background: rgba(255, 255, 255, 0.2);
}
.routine-selector-btn.active {
    background: var(--routine-selector-active-bg);
    color: var(--routine-selector-active-text);
    border-color: var(--routine-selector-active-bg);
}

/* Two-column layout: rituals on the left, sticky QR on the right.
   grid-template-columns: 1fr auto — main grows, aside is its own
   fixed width. align-items: start so the aside can sticky-stick
   at the top of its grid cell rather than be stretched. Wrap under
   560 px viewport — on narrow screens the QR moves below. */
.routine-layout {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: 20px;
    align-items: start;
    width: 100%;
}
.routine-main {
    display: flex;
    flex-direction: column;
    /* No flex gap between sections — adjacent sections are visually
       separated by the border-top + padding-top on
       .routine-section--with-header + .routine-section--with-header.
       A flex gap would create a "no pinned header" window during the
       sticky handoff from one ritual to the next. */
    gap: 0;
    min-width: 0;
}
.routine-qr-sticky {
    position: sticky;
    /* Offset by the live sticky-header height + the pinned routine-
       selector bar + a small gap. JS writes both
       --victory-sticky-height and --routine-selector-height via
       ResizeObservers on .victory-sticky and .routine-selector. */
    top: calc(var(--victory-sticky-height, 120px)
              + var(--routine-selector-height, 52px)
              + 12px);
    width: 180px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    padding: 12px;
    border-radius: 12px;
    background: rgba(255, 255, 255, 0.14);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
}
.routine-qr-sticky[hidden] { display: none; }
.routine-qr-title {
    font-size: 12px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    color: rgba(255, 255, 255, 0.9);
    text-align: center;
}
.routine-qr-code {
    width: 100%;
    aspect-ratio: 1 / 1;
    background: #fff;
    border-radius: 8px;
    padding: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
}
.routine-qr-link,
.routine-qr-img {
    width: 100%;
    height: 100%;
    display: block;
    image-rendering: pixelated;
}
.routine-qr-link { text-decoration: none; }
.routine-qr-hint {
    font-size: 11px;
    color: rgba(255, 255, 255, 0.78);
    text-align: center;
    line-height: 1.3;
}
/* Mobile-only "Buy whole routine on Amazon" pill, injected into the
   sticky aside by `renderRoutineQR` on saved-result pages. Hidden by
   default; the <=640 px block below shows it and hides the QR panel
   chrome (title + code + hint) so the whole slot becomes a tappable
   primary CTA at the end of the routine. */
.routine-qr-cta { display: none; }
@media (max-width: 640px) {
    .routine-layout {
        grid-template-columns: 1fr;
        gap: 14px;
    }
    .routine-qr-sticky {
        /* Panel chrome is gone on phone — the QR is hidden and the
           slot becomes a full-width primary CTA pill ("Buy whole
           routine on Amazon"). Keep position:static so it flows at
           the end of the routine main column. */
        position: static;
        width: 100%;
        padding: 0;
        background: transparent;
        border-radius: 0;
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
        align-self: stretch;
        gap: 0;
    }
    .routine-qr-sticky .routine-qr-title,
    .routine-qr-sticky .routine-qr-code,
    .routine-qr-sticky .routine-qr-hint { display: none; }
    .routine-qr-cta {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        gap: 8px;
        width: 100%;
        padding: 14px 22px;
        border-radius: 999px;
        border: none;
        background: var(--brand-accent);
        color: #fff;
        font-family: inherit;
        font-size: 15px;
        font-weight: 600;
        letter-spacing: -0.1px;
        text-decoration: none;
        box-shadow: 0 4px 14px rgba(21, 21, 129, 0.28);
    }
    .routine-qr-cta::after {
        content: '→';
        opacity: 0.9;
        font-weight: 500;
    }
}
.routine-answers {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    justify-content: center;
}
.routine-answer-pill {
    display: inline-block;
    padding: 5px 12px;
    border-radius: 999px;
    background: var(--answer-pill-bg);
    font-size: 12px;
    font-weight: 500;
    color: var(--answer-pill-text);
    letter-spacing: -0.1px;
}
.routine-heading {
    font-size: var(--routine-heading-size, 20px);
    font-weight: 600;
    letter-spacing: -0.4px;
    text-align: center;
    color: #fff;
}
.routine-empty, .routine-error {
    font-size: 14px;
    color: rgba(255, 255, 255, 0.85);
    text-align: center;
    line-height: 1.5;
    padding: 12px 16px;
    background: rgba(0, 0, 0, 0.22);
    border-radius: 12px;
}
/* Default: flat section (used by the essential variant — no header).
   Advanced/all get the 2-column modifier below. */
.routine-section {
    display: flex;
    flex-direction: column;
    gap: 8px;
}
/* Section with a day-part header — narrow label column on the left,
   product grid on the right. Header items align with the TOP of the
   grid (not centred) so the first product card sits level with the
   icon; the QR aside alongside the routine-main column therefore
   lines up with the first product too, not with an empty header row. */
.routine-section--with-header {
    display: grid;
    grid-template-columns: 92px 1fr;
    gap: 14px;
    align-items: start;
}
/* Separator between adjacent sections — a faint horizontal rule that
   spans both the header column and the product column so the Morning
   / Evening split reads clearly. Only applies between sections, not
   above the first one. */
.routine-section--with-header + .routine-section--with-header {
    padding-top: 16px;
    border-top: 1px solid rgba(255, 255, 255, 0.22);
}
.routine-when {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 4px;
    padding-top: 4px;
    font-size: 13px;
    font-weight: 600;
    color: rgba(255, 255, 255, 0.92);
    letter-spacing: -0.1px;
    /* Sticky within the grid cell — pins below the routine-selector
       for as long as the section is on screen, then scrolls up with
       the section's bottom when the next ritual takes over. Offset
       stacks: .victory-sticky + .routine-selector + a 12 px gap. */
    position: sticky;
    top: calc(var(--victory-sticky-height, 120px)
              + var(--routine-selector-height, 52px)
              + 12px);
    z-index: 3;
    align-self: start;
}
.routine-when-icon {
    font-size: 22px;
    line-height: 1;
    opacity: 0.9;
}
.routine-when-label {
    line-height: 1.2;
}
/* Narrow screens — the main layout already collapses to single
   column (see .routine-layout @media block); match here so the
   day-part header returns to its pre-2-column position (above the
   products) rather than being stuck in a cramped 92 px column. */
@media (max-width: 640px) {
    .routine-section--with-header {
        grid-template-columns: 1fr;
        gap: 6px;
    }
    .routine-section--with-header + .routine-section--with-header {
        padding-top: 12px;
    }
    .routine-when {
        /* Drop the sticky pin on phone. With a 1-col layout the ritual
           header sits inline above its products; stickying it made the
           icon hover visually detached from the product image below
           when a ritual section started scrolling under the selector. */
        position: static;
        flex-direction: row;
        align-items: center;
        gap: 10px;
        padding: 4px 4px 2px;
        font-size: 15px;
    }
    .routine-when-icon {
        font-size: 18px;
    }
    /* Product cards — 96 px media leaves ~60 % of the card width for
       title + brand + description + offer pill. The 128 px iPad default
       crushed the body on a phone and wrapped every product name to
       three lines. */
    .product-card { gap: 10px; padding: 8px; }
    .product-media {
        flex: 0 0 96px;
        width: 96px;
        height: 96px;
    }
    .product-image-placeholder { font-size: 26px; }
    .product-name { font-size: 13px; }
    .product-description { font-size: 11px; }
}
.product-grid {
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.product-card {
    display: flex;
    gap: 12px;
    padding: 10px;
    border-radius: 12px;
    background: rgba(255, 255, 255, 0.15);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
}
/* Product media wrapper. Size comes from --product-image-size
   (written from the runtime config by applyProductCardStyleVars);
   128 px is the default and also the fallback when no config loaded.
   The wrapper is the positioning context for the fit-score overlay. */
.product-media {
    position: relative;
    flex: 0 0 var(--product-image-size, 128px);
    width: var(--product-image-size, 128px);
    height: var(--product-image-size, 128px);
}
.product-image {
    width: 100%;
    height: 100%;
    border-radius: 10px;
    object-fit: cover;
    background: rgba(255, 255, 255, 0.35);
    display: block;
}
.product-image-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    /* Scales with the media wrapper so a smaller image still shows a
       reasonably-proportioned initial. 0.28 × size ≈ 36 px at 128 px. */
    font-size: calc(var(--product-image-size, 128px) * 0.28);
    font-weight: 600;
}
.product-body { flex: 1; min-width: 0; }
.product-card-head {
    display: flex;
    justify-content: space-between;
    gap: 8px;
    align-items: flex-start;
}
.product-step {
    font-size: var(--product-step-size, 11px);
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.4px;
    color: rgba(255, 255, 255, 0.75);
}
.product-name {
    font-size: var(--product-name-size, 14px);
    font-weight: 600;
    color: #fff;
    line-height: 1.25;
}
.product-brand {
    font-size: var(--product-brand-size, 12px);
    color: rgba(255, 255, 255, 0.7);
    margin-top: 1px;
}
/* Fit-score overlay pinned to the image's top-left. Frosted-glass
   chip so the catalog photo still reads through; the Pora palette
   colour goes on the number (text) rather than the chip, keeping
   the chip visually consistent across product variety. */
.product-fit-score {
    position: absolute;
    top: 6px;
    left: 6px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    border-radius: 10px;
    padding: 3px 8px;
    min-width: 40px;
    line-height: 1.05;
    background: rgba(255, 255, 255, 0.78);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
    pointer-events: none;
}
.fit-score-value {
    font-size: 15px;
    font-weight: 700;
}
.fit-score-label {
    font-size: 8px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    opacity: 0.7;
}
/* Description clamps to ~3 lines so the card's lower edge lines up
   with the 128 px image. Only when the text actually overflows does
   JS add `clampable`, turning on the pointer cursor and wiring the
   click handler.

   Animating between clamped and full text is done by pinning
   `max-height` inline on the element (from current height → target
   height) while `.transitioning` drops the webkit-box clamp so the
   height interpolates smoothly — `-webkit-line-clamp` itself isn't
   a transitionable property, so animating it directly snaps. After
   `transitionend`, JS either adds `.expanded` (fully open) or
   returns to the plain collapsed state (clamp + ellipsis). */
.product-description {
    font-size: 12px;
    color: rgba(255, 255, 255, 0.82);
    line-height: 1.4;
    margin-top: 6px;
    display: -webkit-box;
    -webkit-line-clamp: var(--product-desc-clamp-lines, 3);
    -webkit-box-orient: vertical;
    overflow: hidden;
    transition: max-height var(--product-desc-transition, 0.32s) ease;
}
.product-description.clampable { cursor: pointer; }
.product-description.transitioning,
.product-description.expanded {
    display: block;
    -webkit-line-clamp: unset;
    overflow: hidden;
}
.product-description.expanded { overflow: visible; }

/* Offer / price tag — display-only (no link wrap), deliberately
   quiet so it reads as reference info rather than a CTA. Subtle
   dark-glass pill by default, no border, normal-weight price. All
   knobs below are driven by a CSS variable written from
   `applyOfferStyleVars(runtimeCfg)` in app.js, so colors / sizes /
   padding / border can be tuned live in /setup.html. */
.product-offer {
    display: inline-flex;
    align-items: center;
    gap: var(--offer-gap, 6px);
    margin-top: 8px;
    padding: var(--offer-pad-v, 3px) var(--offer-pad-h, 8px);
    border-radius: var(--offer-radius, 999px);
    background: var(--offer-bg, rgba(0, 0, 0, 0.28));
    border: var(--offer-border-width, 0) solid
            var(--offer-border-color, transparent);
    align-self: flex-start;
    max-width: 100%;
    white-space: nowrap;
    overflow: hidden;
}
.product-offer-host-img {
    width:  var(--offer-host-img-size, 16px);
    height: var(--offer-host-img-size, 16px);
    flex:   0 0 var(--offer-host-img-size, 16px);
    object-fit: contain;
    border-radius: 3px;
    /* Subdued chip so the icon sits on a surface without becoming a
       bright rectangle competing with the price. */
    background: rgba(255, 255, 255, 0.55);
}
.product-offer-price {
    font-size: var(--offer-price-size, 12px);
    color:     var(--offer-price-color, #FFD46B);
    font-weight: 600;
    letter-spacing: -0.1px;
    font-variant-numeric: tabular-nums;
}

/* ─── Questionnaire ────────────────────────────────────────────
   Stepped one-per-screen layout that mirrors booth_kiosk. Pre-selection
   shows as a "Based on your scan" badge on the recommended option. */
/* Questionnaire layout is anchored from the top of the viewport so
   each question (2, 4, or 6 options) renders with the title and the
   first answer row at the SAME vertical position. Achieved by:
     1. #screen-questionnaire uses flex-start, not center — content
        doesn't drift down/up based on how many options there are.
     2. Progress dots / title / subtitle all have min-heights that
        reserve enough room for the longest version, so short copy
        doesn't collapse the block and push the grid up.
     3. .option-label reserves exactly 2 lines of height, so single-
        line labels ("Oily") and two-line labels ("Wrinkles & fine
        lines") produce identical button heights.
     4. .option-btn uses justify-content: center so the label + badge
        cluster is vertically centred inside every button. */
#screen-questionnaire {
    background: linear-gradient(180deg, var(--gradient-top) 0%, var(--gradient-mid) 55%, var(--gradient-bottom) 100%);
    justify-content: flex-start;
    gap: 16px;
    /* Leave breathing room under the top-left logo + above the content. */
    padding: clamp(56px, 14vh, 160px) 24px 32px;
}
.question-progress {
    display: flex;
    gap: 8px;
    margin-bottom: 4px;
    min-height: 10px; /* dots are 8 px tall; pin the row */
}
.progress-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.3);
    transition: background 0.2s ease, transform 0.2s ease;
}
.progress-dot.active {
    background: #fff;
    transform: scale(1.2);
}
.progress-dot.done {
    background: var(--color-green);
}
.question-title {
    font-size: var(--question-title-size, 28px);
    font-weight: 700;
    letter-spacing: -0.6px;
    line-height: 1.2;
    color: #fff;
    text-align: center;
    max-width: 520px;
    /* Reserve 2 lines so a short title ("Is your skin sensitive?")
       takes the same vertical space as a long one — the subtitle and
       grid below don't shift between questions. */
    min-height: calc(2 * 1.2em);
    display: flex;
    align-items: center;
    justify-content: center;
}
.question-subtitle {
    font-size: var(--question-subtitle-size, 15px);
    color: rgba(255, 255, 255, 0.8);
    text-align: center;
    max-width: 480px;
    margin-bottom: 12px;
    line-height: 1.4;
    /* 1 line reserved so the grid below pins at a stable Y. */
    min-height: 1.4em;
}
.options-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(140px, 220px));
    gap: 12px;
    max-width: 520px;
    width: 100%;
    /* Center the two tracks inside the container — without this the
       columns left-anchor when they can't fill max-width, pushing the
       buttons off-centre on wide viewports. */
    justify-content: center;
    /* Top padding reserves room for the pin on the top-row buttons
       (which extends ~10 px above each button). Without it the pin
       could brush the subtitle on tight layouts. */
    padding-top: 12px;
}
.option-btn {
    position: relative;
    padding: 14px 14px;
    border-radius: 14px;
    border: 2px solid rgba(255, 255, 255, 0.25);
    background: rgba(255, 255, 255, 0.18);
    color: #fff;
    font-family: inherit;
    font-size: var(--option-label-size, 15px);
    font-weight: 600;
    cursor: pointer;
    text-align: center;
    transition: transform 0.08s ease, background 0.15s ease,
                border-color 0.15s ease;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center; /* vertically centre the label */
    line-height: 1.25;
    /* No gap — the badge is now an absolutely-positioned pin
       anchored to the top edge (see .option-badge below), so the
       label is the only flex item and fills the interior entirely. */
}
.option-btn:hover { background: rgba(255, 255, 255, 0.28); }
.option-btn:active { transform: scale(0.97); }
.option-btn.recommended {
    border-color: var(--color-green);
    background: rgba(87, 222, 152, 0.25);
}
.option-btn.selected {
    background: var(--color-accent);
    border-color: var(--color-accent);
    transform: scale(0.97);
}
.option-label {
    font-size: var(--option-label-size, 15px);
    line-height: 1.25;
    /* Always reserve 2 lines' worth of height. Single-line labels get
       flex-centred inside that height; two-line labels fill it. Both
       produce identical button heights so a grid row with a wrapped
       answer doesn't inflate its row relative to another question. */
    height: calc(2 * 1.25em);
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    overflow: hidden;
}
/* Badge is a pin-style tag anchored to the top edge of the button,
   half above / half overlapping the border so it visually reads as
   an attached label rather than in-button content. Being absolute-
   positioned means the button interior is owned by the label alone —
   which now flex-centres cleanly whether or not a badge is present. */
.option-badge {
    position: absolute;
    top: -10px;
    left: 50%;
    transform: translateX(-50%);
    font-size: 10px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    color: #0A0A0F;
    background: var(--color-green);
    padding: 3px 10px;
    border-radius: 999px;
    box-shadow: 0 2px 6px rgba(10, 10, 15, 0.28);
    white-space: nowrap;
    max-width: 90%;
    overflow: hidden;
    text-overflow: ellipsis;
    pointer-events: none;
    z-index: 1;
}
/* Non-recommended options render an .option-badge--placeholder node
   so the DOM stays symmetric, but it's out of the flow entirely —
   the pin slot above each button is occupied only when that option
   is the scan's recommendation. */
.option-badge--placeholder { display: none; }
@media (max-width: 640px), (max-height: 500px) {
    #screen-questionnaire {
        padding: clamp(40px, 10vh, 100px) 18px 24px;
        gap: 12px;
    }
    .question-title { font-size: 22px; min-height: calc(2 * 1.2em); }
    .question-subtitle { font-size: 13px; }
    .options-grid { grid-template-columns: 1fr 1fr; gap: 8px; }
    .option-btn { padding: 10px 10px; font-size: 13px; }
    .option-label { font-size: 13px; height: calc(2 * 1.25em); }
}

/* ─── Error ────────────────────────────────────────────────────── */

#screen-error {
    background: #0A0A0F;
    text-align: center;
    gap: 16px;
}
.error-title {
    font-size: 28px;
    font-weight: 600;
}
.error-message {
    font-size: 15px;
    opacity: 0.7;
    max-width: 420px;
    line-height: 1.55;
}

/* ─── Buttons ──────────────────────────────────────────────────── */

.btn-primary {
    margin-top: 12px;
    padding: 14px 36px;
    border-radius: 999px;
    border: none;
    background: var(--color-accent);
    color: #fff;
    font-family: inherit;
    font-size: 17px;
    font-weight: 600;
    cursor: pointer;
    transition: transform 0.1s ease, background 0.15s ease;
}
.btn-primary:hover { background: #4150e0; }
.btn-primary:active { transform: scale(0.97); }

/* ─── Global Start Over button ────────────────────────────────
   Pinned to the bottom-right of the viewport. Subtle glass pill so
   it's always reachable without competing with primary content on
   any screen. Hidden on intro + error via `:has()` — the intro is
   tap-anywhere-to-start (nothing to "restart"), and the error
   screen already owns the recovery CTA ("Try again"). */
.btn-restart-global {
    position: fixed;
    right: calc(env(safe-area-inset-right, 0) + 14px);
    bottom: calc(env(safe-area-inset-bottom, 0) + 14px);
    padding: 9px 18px;
    border-radius: 999px;
    border: 1px solid rgba(255, 255, 255, 0.26);
    background: rgba(0, 0, 0, 0.38);
    color: rgba(255, 255, 255, 0.88);
    font-family: inherit;
    font-size: 13px;
    font-weight: 500;
    letter-spacing: -0.1px;
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    cursor: pointer;
    z-index: 100;
    transition: background 0.15s ease, color 0.15s ease, transform 0.1s ease;
    user-select: none;
    -webkit-tap-highlight-color: transparent;
}
.btn-restart-global:hover { background: rgba(0, 0, 0, 0.55); color: #fff; }
.btn-restart-global:active { transform: scale(0.97); }
body:has(#screen-intro.active) .btn-restart-global,
body:has(#screen-error.active) .btn-restart-global,
body.saved-result .btn-restart-global { display: none; }
@media (max-width: 640px) {
    .btn-restart-global {
        right: calc(env(safe-area-inset-right, 0) + 10px);
        bottom: calc(env(safe-area-inset-bottom, 0) + 10px);
        padding: 7px 14px;
        font-size: 12px;
    }
}

/* ─── Brand mark ───────────────────────────────────────────────── */
/* Single brand mark pinned to the top-left corner of the viewport.
   Uses position: fixed and sits above every screen (z-index > .screen /
   .victory-sticky) so it never moves between intro / scanner / victory
   / questionnaire. Renders an <img> when `brandLogoUrl` resolves; falls
   back to the Caveat-font text "lovi" otherwise. */
.brand-mark--fixed {
    position: fixed;
    top: calc(env(safe-area-inset-top, 0) + 14px);
    left: calc(env(safe-area-inset-left, 0) + 18px);
    z-index: 100;
    pointer-events: none;
    user-select: none;
    display: flex;
    align-items: center;
}
.brand-mark-img {
    /* Driven live from --brand-logo-height (written by applyBrandLogo
       in app.js from runtime config). Width auto to preserve the
       source image's aspect ratio. */
    height: var(--brand-logo-height, 30px);
    width: auto;
    display: block;
    filter: drop-shadow(0 1px 6px rgba(10, 10, 15, 0.35));
}
.brand-mark-img[hidden] { display: none; }
.brand-mark-text {
    font-family: 'Caveat', 'Inter', cursive;
    font-size: 30px;
    font-weight: 600;
    color: rgba(255, 255, 255, 0.92);
    letter-spacing: 0.5px;
    text-shadow: 0 1px 6px rgba(10, 10, 15, 0.35);
    line-height: 1;
}
.brand-mark-text[hidden] { display: none; }
@media (max-width: 640px) {
    .brand-mark--fixed {
        top: calc(env(safe-area-inset-top, 0) + 10px);
        left: calc(env(safe-area-inset-left, 0) + 14px);
    }
    .brand-mark-text { font-size: 24px; }
}

/* ─── On-screen debug log ──────────────────────────────────────
   Hidden by default. Enable by appending ?debug=1 to the URL (app.js
   flips the `visible` class on). Top-anchored, monospace, semi-opaque
   black so it reads over the camera feed. */
.debug-log {
    display: none;
    position: fixed;
    top: env(safe-area-inset-top, 0);
    left: 0;
    right: 0;
    max-height: 45vh;
    overflow: hidden;
    margin: 0;
    padding: 8px 12px;
    z-index: 9999;
    pointer-events: none;
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 11px;
    line-height: 1.35;
    color: #E8DFFF;
    background: rgba(0, 0, 0, 0.65);
    white-space: pre-wrap;
    word-break: break-word;
}
.debug-log.visible { display: block; }
