/* tryon.css - /try-on page. Flow-1 equation feel (operator chips + generating animation + a blurred
   result image) merged with the ORIGINAL EzUpload dropzones (cards 1 & 2 are untouched). App-specific
   styling (not a platform theme); colours resolve through --bs-* tokens so it adapts to light/dark. */

.to-presets { display: flex; justify-content: center; margin-bottom: 1.25rem; }
.to-presets-inner { width: 100%; max-width: 22rem; }

.to-op { flex: none; align-self: center; display: inline-flex; align-items: center; justify-content: center;
    width: 2rem; height: 2rem; border-radius: 50%; font-size: 0.85rem; color: var(--bs-secondary-color);
    background: var(--bs-secondary-bg); border: 1px solid var(--bs-border-color); }

/* Sweep-beam keyframe, shared by the live studio-track and hair/makeup generating animations below. */
@keyframes to-scany { to { top: 104%; } }

/* centred status line (scan mode): fill the box and centre both axes instead of pinning to the bottom */
.to-cap.to-cap--center { top: 0; bottom: 0; display: flex; align-items: center; justify-content: center;
    white-space: normal; font-size: 0.85rem; color: var(--bs-body-color); }

/* result area (card 3): blurred image / generating / idle - identical box shape to an EzUpload tile */
.to-rbox { position: relative; width: 100%; aspect-ratio: 3 / 4; min-height: 15rem; max-height: 60vh;
    border-radius: 0.75rem; overflow: hidden; border: 1px solid var(--bs-border-color); background: var(--bs-secondary-bg);
    display: flex; align-items: center; justify-content: center; text-align: center; }
/* idle/failed click-zone styling is the shared .to-zone class, defined after .mv-card below (it must
   follow both card bases in the cascade). */

/* blur backdrop (the flow-1 fill) - shows the result's own colours behind the letterbox */
.to-rbox--fill::before { content: ""; position: absolute; inset: 0; background-image: var(--fill);
    background-size: cover; background-position: center; transform: scale(1.2); filter: blur(24px) brightness(0.55) saturate(1.25); }
.to-rbox-open { position: absolute; inset: 0; z-index: 1; display: flex; align-items: center; justify-content: center;
    padding: 0.5rem; border: 0; background: none; cursor: zoom-in; }
.to-rbox img { max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; border-radius: var(--bs-border-radius); }
/* Flat design: a crisp primary ring (0-blur "flat shadow"), no soft glow. */
.to-rbox--done { border-color: var(--bs-primary); box-shadow: 0 0 0 2px rgba(var(--bs-primary-rgb), 0.35); }
.to-pop { animation: to-pop 0.55s cubic-bezier(0.2, 1.4, 0.4, 1) both; }
@keyframes to-pop { from { transform: scale(0.7); opacity: 0; } to { transform: scale(1); opacity: 1; } }


.to-idle { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.8rem; padding: 1.25rem; }
.to-fail i { color: #e8637c; }
.to-credit { font-family: var(--bs-font-monospace); font-size: 0.72rem; color: var(--bs-secondary-color); }

.to-cap { position: absolute; bottom: 0.9rem; left: 0; right: 0; z-index: 2; text-align: center;
    font-family: var(--bs-font-monospace); font-size: 0.76rem; color: var(--bs-secondary-color);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding: 0 1.5rem; }

/* ---- history: "Your looks" feed of kept looks (person + garment = result), newest first ----
   Same equation shape as the form, one row per saved look. Keep flies the trio down into a new row
   here (tryon.js FLIP flight), which then pops with a sparkle burst. */
/* history is grouped by the day each look was kept: a small sticky-free date heading over each day's
   rows, with generous separation between days. Flat - a hairline rule carries the divide, no panel. */
.to-daygroup + .to-daygroup { margin-top: 3.25rem; }
.to-dayhead { font-size: 0.8rem; font-weight: 600; letter-spacing: 0.02em; text-transform: uppercase;
    color: var(--bs-secondary-color); margin: 0 0 1.5rem; padding-bottom: 0.6rem;
    border-bottom: 1px solid var(--bs-border-color); }
.to-rows { display: flex; flex-direction: column; gap: 2.75rem; }
/* the outfit-notes button carries a small dot when the look has any recorded notes (like an unread mark) */
.to-note-dot { position: absolute; top: -1px; right: -1px; width: 0.5rem; height: 0.5rem; border-radius: 50%;
    background: var(--bs-primary); border: 1px solid var(--bs-body-bg); pointer-events: none; }
/* Flat: no row card (no border/background/panel) - the whitespace between rows and the action-bar
   divider separate each look; the photos carry the row. */
.to-row { position: relative; display: grid; grid-template-columns: 1fr auto 1fr auto 1.2fr; gap: 0.7rem;
    align-items: center; justify-items: center; }
.to-row .to-op { width: 1.65rem; height: 1.65rem; font-size: 0.72rem; align-self: center; }
.to-slot { position: relative; width: 100%; aspect-ratio: 3 / 4; max-height: 42vh; border-radius: 0.6rem;
    overflow: hidden; border: 1px solid var(--bs-border-color); background: var(--bs-secondary-bg);
    display: flex; align-items: center; justify-content: center; }
.to-slot img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; z-index: 1; }
/* Blurred backdrop fills the letterbox with a darkened blow-up of the photo (matches the form cards).
   Driven by --fill:url(...); when unset, var(--fill) is invalid so no backdrop shows. */
.to-slot::before { content: ""; position: absolute; inset: 0; z-index: 0;
    background-image: var(--fill); background-size: cover; background-position: center;
    transform: scale(1.2); filter: blur(24px) brightness(0.55) saturate(1.25); }
.to-slot--result { border-color: var(--bs-primary);
    box-shadow: 0 0 0 2px rgba(var(--bs-primary-rgb), 0.35); }
/* steps: bundled example photos under an EMPTY upload card - a one-tap way to fill the step without
   hunting for an image. Flat (no depth shadows); the brand colour is the only hover affordance. */
/* Shared studio "try one of ours" strip (SampleCarousel, used on outfits/makeup/hair). A horizontal,
   scroll-snapping row of example thumbnails with chevron arrows (shown only when the row overflows,
   disabled at each end) and an optional Women | Men filter toggle. Namespaced .ss-* and loaded globally
   with tryon.css, so it works on the styling studios too. Arrow/scroll logic is in sampleStrip.js. */
.ss { margin-top: 0.75rem; }
.ss-head { display: flex; align-items: center; justify-content: center; gap: 0.6rem;
    margin-bottom: 0.4rem; flex-wrap: wrap; }
.ss-label { font-size: 0.78rem; color: var(--bs-secondary-color); }
/* (The Women | Men filter was removed - see SampleCarousel.razor. The photos communicate who an
   example suits faster than a control does, and the toggle cost a whole row of a card where vertical
   space is scarce; that space went to bigger thumbnails instead.) */

.ss-strip { position: relative; display: flex; align-items: center; justify-content: center; }
.ss-track { display: flex; gap: 0.4rem; overflow-x: auto; scroll-snap-type: x proximity; scroll-behavior: smooth;
    scrollbar-width: none; padding: 2px; }
.ss-track::-webkit-scrollbar { display: none; }
.ss-thumb { position: relative; flex: 0 0 auto; width: 3.25rem; aspect-ratio: 0.72; padding: 0; overflow: hidden;
    scroll-snap-align: start; border: 1px solid var(--bs-border-color); border-radius: var(--radius, 10px);
    background: var(--bs-secondary-bg); cursor: pointer; transition: border-color 0.15s ease; }
/* Same treatment as the big photo frames: the whole image is fitted (never cropped), and a blurred,
   stretched copy of it fills whatever is left over - so a portrait and a square sample both read as
   complete pictures instead of tight centre-crops. --ss-fill is set per thumb by SampleCarousel. */
.ss-thumb::before { content: ""; position: absolute; inset: -15%; z-index: 0;
    background-image: var(--ss-fill); background-size: cover; background-position: center;
    filter: blur(6px) saturate(1.1); opacity: 0.55; }
.ss-thumb img { position: relative; z-index: 1; display: block; width: 100%; height: 100%; object-fit: contain; }
.ss-thumb:hover:not(:disabled) { border-color: var(--bs-primary); }
.ss-thumb:disabled { opacity: 0.45; cursor: default; }
.ss-thumb-busy { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
    background: rgba(var(--bs-body-bg-rgb), 0.7); }

/* Arrows sit outside the track; hidden unless the row overflows, dimmed + inert at each end. */
.ss-arrow { display: none; flex: 0 0 auto; width: 1.6rem; height: 1.6rem; align-items: center; justify-content: center;
    padding: 0; border: 1px solid var(--bs-border-color); border-radius: 999px; background: var(--bs-body-bg);
    color: var(--bs-body-color); cursor: pointer; font-size: 0.7rem;
    transition: opacity 0.15s ease, border-color 0.15s ease, color 0.15s ease; }
.ss--overflow .ss-arrow { display: inline-flex; }
.ss-arrow:hover:not(.ss-arrow--disabled) { border-color: var(--bs-primary); color: var(--bs-primary); }
.ss-arrow--disabled { opacity: 0.3; cursor: default; pointer-events: none; }
.ss-arrow-prev { margin-right: 0.25rem; }
.ss-arrow-next { margin-left: 0.25rem; }

/* Admin sample-image list: a small portrait thumbnail beside each row. */
.ss-adminthumb { display: inline-flex; flex: 0 0 auto; width: 2.6rem; aspect-ratio: 0.72; overflow: hidden;
    border-radius: 0.4rem; border: 1px solid var(--bs-border-color); background: var(--bs-secondary-bg); }
.ss-adminthumb img { width: 100%; height: 100%; object-fit: cover; }

/* history: ONE simple tip (EzAlert) at the top of the feed, pointing at the per-row modelling buttons.
   No CTA of its own - the play icon under each look is the action. The wrapper owns the spacing
   (the alert's built-in bottom margin is zeroed so the gap is exact). */
.to-histtip { margin-bottom: 1.75rem; }
.to-histtip .alert { margin-bottom: 0; }

/* modelling-videos manager: a responsive grid of portrait tiles. Each video tile is [pose name on top]
   / [thumbnail] / [share + delete]; the last tile is the create card (or the generating spinner). The
   first frame shows via preload=metadata + a #t seek; pressing play sends the video fullscreen (tryon.js). */
.mv-grid { display: flex; flex-wrap: wrap; gap: 1.1rem;
    align-items: flex-start; justify-content: center; padding-bottom: 1.5rem; }
.mv-tile { display: flex; flex-direction: column; gap: 0.4rem; width: 12rem; min-width: 0; }
.mv-pose { font-size: 0.8rem; color: var(--bs-secondary-color); text-align: center;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mv-thumb { width: 100%; aspect-ratio: 3 / 4; border-radius: 0.6rem; overflow: hidden;
    border: 1px solid var(--bs-border-color); background: var(--bs-secondary-bg); }
.mv-thumb video { width: 100%; height: 100%; object-fit: cover; display: block; background: #000; }
/* object-fit:cover crops the thumbnail nicely, but a video keeps it in fullscreen too and clips the
   figure - reset to contain (letterboxed, whole frame) while fullscreen. */
.mv-video:fullscreen { object-fit: contain; background: #000; }
.mv-video:-webkit-full-screen { object-fit: contain; background: #000; }
.mv-actions { display: flex; align-items: center; justify-content: center; gap: 0.4rem; }
/* create card: the SAME fixed 3/4 box as a video thumbnail, but styled like the step-3 result card -
   it hosts .to-idle (pose picker + Create + credit), .to-cap (creating) or .to-fail (failed),
   so every state matches card 3 and lines up with the video tiles. */
.mv-card { position: relative; width: 100%; aspect-ratio: 3 / 4; border-radius: 0.6rem; overflow: hidden;
    border: 1px solid var(--bs-border-color); background: var(--bs-secondary-bg);
    display: flex; align-items: center; justify-content: center; text-align: center; }
.mv-card .to-idle { padding: 0.9rem; gap: 0.6rem; }

/* full-card click zone (step 3's idle + failed states, the modelling create card's failed state):
   looks and acts like an empty EzUpload dropzone - dashed border, brand highlight on hover, dimmed
   + neutral while disabled. Defined AFTER .to-rbox/.mv-card so it wins their solid 1px border.
   A zone hosting the style picker cannot BE a button (below), so it carries .is-off instead of
   :disabled - every zone rule below has to answer to both spellings. */
.to-zone { padding: 0; cursor: pointer; color: inherit; border: 2px dashed var(--bs-border-color);
    transition: border-color 0.15s ease-in-out, background-color 0.15s ease-in-out; }
.to-zone:hover:not(:disabled):not(.is-off) { border-color: var(--bs-primary); }
/* hovering ANYWHERE on the zone puts the visual action chip into its normal button hover state (the
   chip is a span, so its own :hover never fires for the rest of the card) */
.to-zone:hover:not(:disabled):not(.is-off) .btn-primary { color: var(--bs-btn-hover-color);
    background-color: var(--bs-btn-hover-bg); border-color: var(--bs-btn-hover-border-color); }
.to-zone:disabled,
.to-zone.is-off { cursor: default; opacity: 0.65; }

/* ---- a click-zone card that HOSTS a control (the style picker) --------------------------------
   Once a card contains a real control it cannot be a <button> - one cannot nest in another - so the
   whole-card click zone becomes a transparent button laid underneath, and the content on top is
   click-through. Only the picker takes its own clicks back. Used by step 3's idle result box and by
   the create card in the modelling-videos manager. */
.to-hit { position: absolute; inset: 0; z-index: 0; padding: 0; border: 0; background: transparent;
    cursor: pointer; }
.to-hit:disabled { cursor: default; }
.to-idle--hosted { width: 100%; pointer-events: none; }
.to-idle--hosted .st-style { pointer-events: auto; }

/* ---- style picker (StylePicker.razor) ---------------------------------------------------------
   A flat pill that reads as part of the card it sits in - no label above it, the pill states the
   choice - opening a menu of the admin-configured styles. max-width keeps it proportionate on the
   widest hosts (the full-width phone tile below is up to 26rem across); .to-idle centres it. */
.st-style { position: relative; width: 100%; max-width: 15rem; }
.st-style-trigger { display: flex; align-items: center; gap: 0.4rem; width: 100%;
    padding: 0.3rem 0.6rem; font-size: 0.78rem; line-height: 1.35; color: var(--bs-body-color);
    background: var(--bs-body-bg); border: 1px solid var(--bs-border-color); border-radius: 2rem;
    cursor: pointer; transition: color 0.15s, border-color 0.15s, background 0.15s; }
.st-style-trigger:hover:not(:disabled),
.st-style.is-open .st-style-trigger { color: var(--bs-primary); border-color: var(--bs-primary);
    background: rgba(var(--bs-primary-rgb), 0.06); }
.st-style-trigger:disabled { cursor: default; }
.st-style-trigger > i:first-child { flex-shrink: 0; font-size: 0.72rem; color: var(--bs-primary); }
.st-style-name { flex: 1 1 auto; min-width: 0; text-align: left;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.st-style-caret { flex-shrink: 0; font-size: 0.62rem; color: var(--bs-secondary-color);
    transition: transform 0.15s, color 0.15s; }
.st-style.is-open .st-style-caret { transform: rotate(180deg); color: var(--bs-primary); }

/* The panel goes in the fixed layer, anchored to the pill, so neither the card's own overflow:hidden
   nor a modal's scroll box clips it - the same technique as EzActionMenu, whose look it matches.
   anchor-name / position-anchor are emitted per instance as inline styles by the component. */
.st-style-backdrop { position: fixed; inset: 0; z-index: 20; }
.st-style-panel { z-index: 21; padding: 0.3rem; min-width: 10rem;
    max-width: min(16rem, calc(100vw - 2rem)); max-height: 14rem; overflow-y: auto;
    text-align: left; background: var(--bs-body-bg); border: 1px solid var(--border-strong);
    border-radius: var(--radius-lg); box-shadow: var(--bs-box-shadow); }
@supports not (anchor-name: --test) {
    /* No anchor positioning: fall back to a panel inside the card. It is clipped by the card, so it
       opens downward from the pill, where there is room for a few rows. */
    .st-style-panel { position: absolute; top: calc(100% + 0.35rem); left: 0; right: 0; }
}
@supports (anchor-name: --test) {
    .st-style-panel { position: fixed; top: calc(anchor(bottom) + 0.35rem); left: anchor(left);
        min-width: anchor-size(width); position-try-fallbacks: flip-block; }
}
.st-style-item { display: flex; align-items: center; gap: 0.5rem; width: 100%;
    padding: 0.4rem 0.5rem; font-size: 0.82rem; line-height: 1.35; text-align: left;
    color: var(--bs-tertiary-color); background: transparent; border: 0;
    border-radius: var(--radius-sm); cursor: pointer; transition: color 0.15s, background 0.15s; }
.st-style-item:hover { color: var(--bs-primary); background: rgba(var(--bs-primary-rgb), 0.08); }
.st-style-item.is-on { color: var(--bs-primary); background: rgba(var(--bs-primary-rgb), 0.12); }
/* The tick holds its column on every row, so the labels stay on one left edge as the choice moves. */
.st-style-tick { flex-shrink: 0; width: 1em; font-size: 0.7rem; visibility: hidden; }
.st-style-item.is-on .st-style-tick { visibility: visible; }
.st-style-label { flex: 1 1 auto; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.st-style-cost { display: inline-flex; align-items: center; gap: 0.25rem; flex-shrink: 0;
    font-family: var(--bs-font-monospace); font-size: 0.7rem; color: var(--bs-secondary-color); }
.st-style-item:hover .st-style-cost,
.st-style-item.is-on .st-style-cost { color: inherit; }

/* modelling "creating" box: one box, so a single repeating downward sweep (no
   sequence), matching steps 1 & 2. The card already clips (overflow:hidden) and is position:relative. */
.mv-card--scan::after {
    content: ""; position: absolute; left: 0; right: 0; top: -20%; height: 16%; z-index: 3; pointer-events: none;
    background: linear-gradient(to bottom, transparent, rgba(var(--bs-primary-rgb), 0.35), transparent);
    border-bottom: 2px solid var(--bs-primary); animation: to-scany 1.6s ease-in-out infinite; }

/* history feed: the video status strip, pinned flat across the bottom of the look's result image.
   Solid theme-tinted fills (flat design - no soft shadows): primary while creating, success / danger
   for the outcome. Clipped by the slot's rounded corners; pointer-events off so click-to-zoom still
   works; a small slide-up entrance so a status appearing mid-view feels placed, not popped. */
.to-slot-status { position: absolute; left: 0; right: 0; bottom: 0; z-index: 3; pointer-events: none;
    display: flex; align-items: center; justify-content: center; gap: 0.45rem;
    padding: 0.45rem 0.6rem; font-size: 0.78rem; font-weight: 600; letter-spacing: 0.01em;
    color: #fff; text-align: center; animation: to-statusup 0.3s ease-out; }
.to-slot-status i { font-size: 0.85rem; }
.to-slot-status--creating { background: rgba(var(--bs-primary-rgb), 0.92); }
.to-slot-status--created { background: rgba(var(--bs-success-rgb), 0.92); }
.to-slot-status--error { background: rgba(var(--bs-danger-rgb), 0.92); }
@keyframes to-statusup { from { transform: translateY(100%); } to { transform: translateY(0); } }

/* ---- admin compare overlay: full-screen takeover (same family as the EzImageViewer lightbox),
   comparing step 1's photo with step 3's result. Always dark - it hosts photos edge to edge - so the
   captions/labels use fixed light inks rather than theme tokens. ---- */
.to-cmp { position: fixed; inset: 0; z-index: 1090; display: flex; flex-direction: column; align-items: center;
    gap: 1rem; padding: 1.25rem; background: rgba(0, 0, 0, 0.9); cursor: zoom-out;
    animation: to-cmp-fade 0.15s ease-out; }
@keyframes to-cmp-fade { from { opacity: 0; } to { opacity: 1; } }
.to-cmp-close { position: fixed; top: 1rem; right: 1.25rem; z-index: 2; }
.to-cmp:hover .ez-media-actions { opacity: 1; }
.to-cmp-toggle { display: flex; align-items: center; gap: 0.5rem; cursor: default; }
/* zoom cluster: separated from the mode buttons by a thin divider (flat, matching the dark overlay) */
.to-cmp-zoomctl { display: inline-flex; align-items: center; gap: 0.35rem;
    margin-left: 0.5rem; padding-left: 0.85rem; border-left: 1px solid rgba(255, 255, 255, 0.25); }
/* one body box for both modes: fills the remaining height and CLIPS the zoomed stage. The stage
   inside carries the zoom/pan transform (tryon.js); the labels sit on the body so they never scale. */
.to-cmp-body { position: relative; flex: 1 1 0; min-height: 0; width: 100%; overflow: hidden;
    display: flex; align-items: center; justify-content: center; cursor: default; touch-action: none; }
/* tryon.js publishes the zoom/pan as --cmp-z / --cmp-x / --cmp-y; the transform is declared here. */
.to-cmp-stage { display: flex; align-items: center; justify-content: center; height: 100%; width: 100%;
    will-change: transform;
    transform: translate(var(--cmp-x, 0px), var(--cmp-y, 0px)) scale(var(--cmp-z, 1)); }
.to-cmp-zoomed, .to-cmp-zoomed .to-cmp-wipe { cursor: grab; }
/* side by side: the two contain-fitted photos sit FLUSH against each other (no gap) - each element
   hugs its painted image (height-driven width), so the seam is real edge-to-edge contact. Only the
   outer corners are rounded, reading as one combined before/after frame. */
.to-cmp-img { height: 100%; max-width: 44vw; object-fit: contain; }
.to-cmp-img--left { border-radius: var(--bs-border-radius) 0 0 var(--bs-border-radius); }
.to-cmp-img--right { border-radius: 0 var(--bs-border-radius) var(--bs-border-radius) 0; }
/* wipe slider: both images stacked in one contain-fitted box; the photo on top is clipped from the
   right by --cmp (photo left, result right) and the divider tracks the same var. Pointer drags are
   handled in tryon.js (wipe vs pan); the invisible range input remains for keyboard arrows only.

   The box must HUG the painted photo, never be a wider frame the photo is letterboxed inside: the clip,
   the divider and the drag maths in tryon.js are all expressed against the box, so any empty band
   beside the photo is a band the divider can be dragged into (a portrait photo in the old full-width
   box left most of the travel outside the image). tryon.js measures both images and publishes the
   narrower aspect ratio as --cmp-ar, which sizes the box: height-driven normally, and width-driven via
   max-height once the width cap binds - either way an exact fit with no letterbox to escape into. */
.to-cmp-wipe { position: relative; flex: none; --cmp: 50%; --cmp-ar: 0.75; cursor: ew-resize;
    height: 100%; width: auto; aspect-ratio: var(--cmp-ar);
    max-width: min(92vw, 60rem); max-height: calc(min(92vw, 60rem) / var(--cmp-ar)); }
.to-cmp-wipe img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
.to-cmp-over { clip-path: inset(0 calc(100% - var(--cmp)) 0 0); }
.to-cmp-range { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0;
    pointer-events: none; }
/* Flat design: solid 2px divider + a solid brand grip chip, no glow/shadow. pointer-events off so the
   range input beneath receives every drag. */
.to-cmp-bar { position: absolute; top: 0; bottom: 0; left: var(--cmp); width: 2px; margin-left: -1px;
    background: var(--bs-primary); pointer-events: none; }
.to-cmp-grip { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
    width: 2.2rem; height: 2.2rem; border-radius: 50%; background: var(--bs-primary); color: #fff;
    display: flex; align-items: center; justify-content: center; font-size: 0.85rem; }
/* keyboard focus on the invisible range shows as a ring on the grip chip (flat 0-blur ring) */
.to-cmp-range:focus-visible ~ .to-cmp-bar .to-cmp-grip { box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.85); }
/* labels: pinned to the body either side of the centre seam (outside the stage, so zooming the
   photos never scales them) - "Your photo" always left, "Result" always right in both modes */
.to-cmp-tag { position: absolute; bottom: 0.6rem; z-index: 1; padding: 0.2rem 0.55rem; border-radius: 0.4rem;
    font-size: 0.72rem; font-weight: 600; color: #fff; background: rgba(0, 0, 0, 0.55); pointer-events: none; }
.to-cmp-tag--left { right: calc(50% + 0.6rem); }
.to-cmp-tag--right { left: calc(50% + 0.6rem); }

/* Bootstrap < md: the equation stacks to one step per row - each card fills the page width (capped
   so a landscape phone doesn't get a giant portrait card) and centres, with the operator chips
   rotated to read downward. The history rows stack identically: one image per row, same width cap,
   centred, with each image immediately followed by its own action bar and the +/-> chips kept
   between the image groups so each look still reads as photo + outfit = result. */
@media (max-width: 767.98px) {
    .to-op i { transform: rotate(90deg); }
    /* .to-row is a 2-row grid on desktop (images on row 1, per-image action bars on row 2). At phone
       width switch it to a single-column flex stack and use `order` to interleave the fixed-order
       children into image -> its bar -> operator groups. Child order in the DOM is stable:
       1 person img, 2 (+) chip, 3 outfit img, 4 (->) chip, 5 result img, 6 person bar (--a),
       7 outfit bar (--b), 8 result bar. The delete-confirmation modal reuses .to-row with only the
       first five (no bars), which the same rules stack correctly. */
    .to-row { display: flex; flex-direction: column; align-items: center; }
    .to-row .to-slot { width: 100%; max-width: 26rem; }
    .to-row > :nth-child(1) { order: 1; }  /* person image */
    .to-row > :nth-child(6) { order: 2; }  /* person action bar (--a) */
    .to-row > :nth-child(2) { order: 3; }  /* + chip */
    .to-row > :nth-child(3) { order: 4; }  /* outfit image */
    .to-row > :nth-child(7) { order: 5; }  /* outfit action bar (--b) */
    .to-row > :nth-child(4) { order: 6; }  /* -> chip */
    .to-row > :nth-child(5) { order: 7; }  /* result image */
    .to-row > :nth-child(8) { order: 8; }  /* result action bar */
    /* .mv-grid descendant selector (not bare .mv-tile): the Styling module ships its own
       styling.css that also defines a plain `.mv-tile { width: 12rem }` and loads after this
       file, so a bare `.mv-tile` override here loses the cross-sheet tie. The extra class wins
       it (and gives the Styling modelling modal the same full-width tiles on phones). */
    .mv-grid .mv-tile { width: 100%; max-width: 26rem; }
}
@media (prefers-reduced-motion: reduce) {
    .mv-card--scan::after { display: none; }
    .to-slot-status { animation: none; }
    .to-cmp { animation: none; }
    .st-decide { animation: none; }
}

/* =====================================================================================
   KEEP CELEBRATION (star shower) - the effect that erupts on a look as it lands in the
   feed; the trio-flies-down flight itself is separate and always runs. Rendered by the
   shared TryOnKeepFx component. Particles carry --dx/--dy (px), --i (index). Overlay
   fills the row; pointer-events off.
   ===================================================================================== */
.to-keepfx { position: absolute; inset: 0; z-index: 30; pointer-events: none; }
.kf-p { position: absolute; width: 9px; height: 9px; border-radius: 50%; }
/* shooting stars streak with trails */
.to-keepfx--star-shower .kf-p { width: 3px; height: 3px; border-radius: 50%; opacity: 0; background: #fff;
    box-shadow: 0 0 6px 2px rgba(255, 255, 255, 0.9); animation: kf-star 1s ease-in forwards; animation-delay: calc(var(--i) * 0.06s); }
.to-keepfx--star-shower .kf-p::after { content: ""; position: absolute; left: 2px; top: 0.5px; width: 34px; height: 2px;
    transform-origin: left center; transform: rotate(150deg); background: linear-gradient(to right, #fff, transparent); }
@keyframes kf-star { 0% { transform: translate(0, 0); opacity: 0; } 15% { opacity: 1; } 100% { transform: translate(var(--dx), var(--dy)); opacity: 0; } }


/* honour reduced-motion: no keep celebration (the caption and the flight still read fine) */
@media (prefers-reduced-motion: reduce) {
    .to-keepfx *, .to-keepfx *::before, .to-keepfx *::after {
        animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; }
    .to-keepfx { display: none; }
}

/* ===== public share page (/s/{token}) - .sp-* ==================================================
   Anonymous, public-facing gallery / poll page rendered inside PublicShareLayout. One normalised
   structure for every state: hero header (.sp-head), framed look tiles (.sp-tile, same blurred
   self-fill letterbox as .to-rbox--fill), poll option cards, ranked results, and the about-us
   pitch banner (.sp-promo). Flat design: crisp primary rings, no depth shadows. */

.sp-head { max-width: 36rem; margin: 0 auto 2rem; }
.sp-eyebrow { display: inline-flex; align-items: center; gap: 0.5rem; margin-bottom: 0.75rem;
    padding: 0.25rem 0.75rem; font-size: 0.72rem; font-weight: 600; letter-spacing: 0.12em;
    text-transform: uppercase; color: var(--bs-primary); border-radius: 50rem;
    background: rgba(var(--bs-primary-rgb), 0.1); border: 1px solid rgba(var(--bs-primary-rgb), 0.35); }
.sp-title { margin-bottom: 0.25rem; }
.sp-lede { color: var(--bs-secondary-color); margin: 0; }
.sp-alert { max-width: 36rem; margin-left: auto; margin-right: auto; }

/* look tile: fixed portrait frame; the image is contained (never cropped) over a blurred copy of
   itself so odd aspect ratios still fill the frame edge-to-edge - the established try-on look. */
.sp-tile { position: relative; display: block; width: 100%; aspect-ratio: 3 / 4; margin: 0;
    overflow: hidden; border-radius: 0.75rem; border: 1px solid var(--bs-border-color);
    background: var(--bs-secondary-bg); }
.sp-tile::before { content: ""; position: absolute; inset: 0; background-image: var(--fill);
    background-size: cover; background-position: center; transform: scale(1.2);
    filter: blur(24px) brightness(0.55) saturate(1.25); }
.sp-tile img { position: absolute; inset: 0; margin: auto; max-width: 100%; max-height: 100%;
    width: auto; height: auto; object-fit: contain; }

/* poll option card: the real radio stays in the DOM (focusable, native required-validation) but is
   visually hidden; the card carries hover / checked / focus state via :has(). */
.sp-option { position: relative; display: block; cursor: pointer; }
.sp-option-radio { position: absolute; left: 50%; bottom: 0.5rem; width: 1px; height: 1px; opacity: 0; }
.sp-option .sp-tile { transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease; }
.sp-option:hover .sp-tile { border-color: var(--bs-primary); transform: translateY(-2px); }
.sp-option-pick { display: flex; align-items: center; justify-content: center; gap: 0.5rem;
    margin-top: 0.5rem; padding: 0.25rem 0.5rem; font-size: 0.85rem; font-weight: 500;
    color: var(--bs-secondary-color); border: 1px solid var(--bs-border-color); border-radius: 50rem;
    transition: color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease; }
.sp-option:has(.sp-option-radio:checked) .sp-tile { border-color: var(--bs-primary);
    box-shadow: 0 0 0 2px rgba(var(--bs-primary-rgb), 0.35); }
.sp-option:has(.sp-option-radio:checked) .sp-option-pick { color: #fff;
    background: var(--bs-primary); border-color: var(--bs-primary); }
.sp-option:has(.sp-option-radio:focus-visible) .sp-tile { outline: 2px solid var(--bs-primary); outline-offset: 2px; }
.sp-vote-btn { min-width: 10rem; }

/* poll results: ranked tiles with a share-of-votes bar; the leader gets the flat primary ring */
.sp-result--winner .sp-tile { border-color: var(--bs-primary);
    box-shadow: 0 0 0 2px rgba(var(--bs-primary-rgb), 0.35); }
.sp-result-meta { margin-top: 0.5rem; }
.sp-result-bar { height: 0.5rem; }
.sp-result-votes { display: flex; align-items: baseline; gap: 0.5rem; margin-top: 0.25rem;
    font-size: 0.85rem; color: var(--bs-secondary-color); }
.sp-result-pct { font-weight: 600; color: var(--bs-body-color); }
.sp-flag { position: absolute; top: 0.5rem; z-index: 1; display: inline-flex; align-items: center;
    gap: 0.5rem; padding: 0.25rem 0.5rem; font-size: 0.72rem; font-weight: 600; border-radius: 50rem;
    background: var(--bs-body-bg); border: 1px solid var(--bs-border-color); color: var(--bs-body-color); }
.sp-flag--winner { left: 0.5rem; border-color: var(--bs-primary); color: var(--bs-primary); }
.sp-flag--mine { right: 0.5rem; }

/* unavailable / expired link */
.sp-empty { max-width: 26rem; margin: 2rem auto 0; }
.sp-empty-icon { display: flex; align-items: center; justify-content: center; width: 4rem; height: 4rem;
    margin: 0 auto 1rem; font-size: 1.5rem; border-radius: 50%; color: var(--bs-secondary-color);
    background: var(--bs-secondary-bg); border: 1px solid var(--bs-border-color); }

/* about-us pitch banner: flat primary-tinted panel with the CTA back to the app */
.sp-promo { max-width: 36rem; margin: 3rem auto 0; padding: 2rem 1.5rem;
    border: 1px solid rgba(var(--bs-primary-rgb), 0.35); border-radius: 1rem;
    background: rgba(var(--bs-primary-rgb), 0.06); }
.sp-promo-icon { display: flex; align-items: center; justify-content: center; width: 3rem; height: 3rem;
    margin: 0 auto 1rem; font-size: 1.25rem; border-radius: 50%; background: var(--bs-primary); color: #fff; }
.sp-promo-title { margin-bottom: 0.5rem; }
.sp-promo-pitch { color: var(--bs-secondary-color); margin-bottom: 1.5rem; }
.sp-promo-cta i { margin-right: 0.25rem; }
/* The free-start offer, on a quiet line under the CTA. Rendered only when the admin's promo copy
   has not placed the {{FreeOffer}} token itself, so the offer is stated exactly once. */
.sp-promo-offer { display: flex; align-items: center; justify-content: center; gap: 0.375rem;
    margin: 0.75rem 0 0; font-size: 0.875rem; color: var(--bs-secondary-color); }
.sp-promo-offer i { color: var(--bs-primary); }

/* ---- studio mobile-economy pass (2026-07) ---------------------------------------------------- */
/* The one guidance line, under the step-1 heading where it is actionable. */
.to-stephint { color: var(--bs-secondary-color); font-size: 0.8125rem; text-align: center; margin: 0 0 0.5rem; }
/* Mobile sticky generate: floats the swap action above the bottom tab bar once both photos are in
   (the stacked layout puts step 3 several screens down). Flat by design - no shadow. */
.to-sticky {
    position: fixed;
    left: 0;
    right: 0;
    bottom: calc(3.75rem + env(safe-area-inset-bottom));
    display: flex;
    justify-content: center;
    padding: 0 1rem;
    z-index: 1045;
    pointer-events: none;
}
.to-sticky-btn { pointer-events: auto; display: inline-flex; align-items: center; gap: 0.5rem; }
.to-sticky-cost { font-size: 0.75rem; font-weight: 500; opacity: 0.85; }

/* ============================================================================================
   Studio card track (2026-07) - the responsive three-step studio, shared VERBATIM by all three
   studios (/outfits, /makeup, /hair) and by every kept look on /looks.
   ONE track, three modes:
     below md   focus - one full-width step at a time, swipe or arrows
     md to lg   peek  - the current step centred, neighbours peeking and dimmed
     lg and up  row   - all three steps side by side, no scrolling, no arrows
   The photo cap (--st-cap) is MEASURED AT RUNTIME by studio.js (real chrome above the track, the
   card's own header/bar, the bottom tab bar) - the rem values here are only the no-JS fallback,
   because a fixed budget under-measures tall chrome (MAUI WebView header, safe-area insets). The
   card WIDTH derives from the cap, so on short screens the whole card shrinks rather than clipping.
   The 3/4 photo frame is the common phone-camera aspect ratio, which is why every studio uses it.
   ============================================================================================ */

.st-studio {
    --st-cap: max(12rem, calc(100svh - 22.5rem));
    --st-pull: 0px;
    /* Single source of truth for the action bar's height: the CSS reserves it and studio.js budgets
       it into the photo cap, so an empty card (whose bar collapses) never resizes the photo. */
    --st-barh: 48px;
    /* Radius scale, largest on the outside: card > inset elements > controls (the icon buttons bring
       their own 8px). Inner values come from the platform --radius token rather than being invented
       per element. Anything OPAQUE that fills the photo area edge to edge gets no radius at all - the
       card already clips it, and rounding it again just notches card background into the photo. */
    --st-radius-card: 14px;
    --st-radius-inner: var(--radius, 10px);
    --st-avail: calc(100vw - 2.5rem);
    --st-w: min(var(--st-avail), calc(var(--st-cap) * 0.75));
    /* Both measured by studio.js and published as custom properties (it writes no style declarations):
       --st-band is the height of the band the card centres in, --st-overpull the residual overflow
       under the fixed tab bar that has to be absorbed. Both are 0 at lg+, where nothing is pulled. */
    --st-band: 0px;
    --st-overpull: 0px;
    margin-top: calc(var(--st-pull) * -1);
    margin-bottom: calc(var(--st-overpull) * -1);
}
@media (min-width: 768px) and (max-width: 991.98px) {
    .st-studio { --st-w: min(calc(var(--st-avail) * 0.72), calc(var(--st-cap) * 0.75)); }
}
@media (min-width: 1200px) {
    .st-studio { --st-cap: max(12rem, calc(100svh - 19rem)); }
}

/* Below xl the active bottom tab already names the page, so the page heading is pure chrome -
   hide it and give the space to the card. :has() scopes this to the studio page only (tryon.css
   is loaded in the global head, so an unscoped rule would strip every page's heading). */
@media (max-width: 1199.98px) {
    body:has(.st-studio) .page-header .page-title { display: none; }
}

/* studio.js gives this the exact height of the band between the header's ink and the tab bar (below
   lg only), so the card centres in it - the leftover splits evenly above and below whether the cap
   or the viewport width is the binding constraint. */
.st-wrap { position: relative; display: flex; flex-direction: column; justify-content: center;
    min-height: var(--st-band, 0px); }

/* Do not show the card until studio.js has measured it. --st-pull, --st-band and --st-cap all start at
   the fallbacks declared on .st-studio and are then replaced with measured pixel values a frame later,
   so the unmeasured card sits in a different PLACE and at a different SIZE to the one the user is
   meant to see. Painting it first and correcting it afterwards is a visible jump on every load - and
   it happens twice, because the circuit taking over re-renders the card and studio.js re-fits it.
   Hiding that first frame costs nothing: the reveal is the same frame the measured layout lands on.

   The animation is a dead-man's switch, not decoration. If studio.js never runs (module blocked, a JS
   error, an old WebView), nothing would ever add .st-ready - so after 400ms the card reveals itself
   anyway and falls back to the rem-based sizes in .st-studio. The studio can never end up permanently
   invisible because of this rule. */
.st-studio:not(.st-ready) .st-wrap {
    visibility: hidden;
    animation: st-reveal-unmeasured 0s linear 0.4s forwards;
}
@keyframes st-reveal-unmeasured { to { visibility: visible; } }
/* Card width comes from the viewport, NOT container percentages: a %-based width used in both
   grid-auto-columns and the centring padding resolves against two different boxes (the padding
   shrinks the grid content box) and compounds. */
.st-track {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: var(--st-w);
    /* Cards differ in height (an empty card collapses its action bar), so centre them on each other
       rather than letting the grid stretch the short one and re-open a dead band inside it. */
    align-items: center;
    gap: 0.65rem;
    padding-inline: max(0rem, calc((100% - var(--st-w)) / 2));
    scroll-padding-inline: max(0rem, calc((100% - var(--st-w)) / 2));
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
}
.st-track::-webkit-scrollbar { display: none; }
.st-track > * { scroll-snap-align: center; }
@media (min-width: 992px) {
    .st-track {
        grid-auto-flow: row;
        grid-template-columns: repeat(3, minmax(0, calc(var(--st-cap) * 0.75)));
        justify-content: center;
        gap: 0.9rem;
        overflow: visible;
        scroll-snap-type: none;
        padding-inline: 0;
    }
}

/* One-colour slab: the card header, photo frame, and action bar all share a single surface.
   body-bg is the raised-surface token in BOTH themes (white on the light lavender wash; #141416 on
   the dark wash) - tertiary/secondary melt into the page. The resting border stays transparent (but
   present, so the active ring never shifts layout); only the active card draws its brand outline. */
.st-card {
    display: flex;
    flex-direction: column;
    min-width: 0;
    border: 1px solid transparent;
    border-radius: var(--st-radius-card);
    overflow: hidden;
    background: var(--bs-body-bg);
}
@media (max-width: 991.98px) {
    .st-card {
        opacity: 0.55;
        transform: scale(0.975);
        transition: opacity 0.25s ease, transform 0.25s ease, border-color 0.25s ease, box-shadow 0.25s ease;
    }
    .st-card.is-active {
        opacity: 1;
        transform: none;
        border-color: var(--bs-primary);
        box-shadow: 0 0 0 3px rgba(var(--bs-primary-rgb), 0.14);
    }
    /* Until studio.js has marked the snapped card, show every card at full strength - otherwise the
       first paint (and the frame after any Blazor re-render that rewrites a card's class) flashes
       all three dimmed. */
    .st-studio:not(.st-ready) .st-card { opacity: 1; transform: none; }
}
@media (min-width: 992px) {
    /* Desktop needs a resting outline: nothing is dimmed here and no card carries the brand ring by
       default, so without one the three cards dissolve into the page. The current step still lights
       up - the old equation layout highlighted it at every width, and it is just as useful here as a
       "what to do next" cue. */
    .st-card { border-color: var(--bs-border-color); }
    .st-card.is-active {
        border-color: var(--bs-primary);
        box-shadow: 0 0 0 3px rgba(var(--bs-primary-rgb), 0.14);
    }
}

/* Step strip: three slim segments above the track, one per step - the story-bar pattern from the
   design mocks, promoted out of the cards. The cards themselves carry NO heading now: the photos say
   what they are, and the strip says where you are in making a look. It reads left to right like
   progress, which a per-card label never could, and it costs one 30px row for all three steps
   instead of a row inside each card. */
/* NOTE the class names: styling.css (also loaded globally) owns the .st-picker / .st-tile / .st-swatch
   family for the makeup + hair style picker. Keep every rule in this block .st-progress-* / .st-card /
   .st-photo / .st-bar so the two stylesheets never collide - a bare .st-step here once silently
   restyled the styling studios' own headings. */
.st-progress {
    --st-progress-gap: 0.4rem;
    position: relative;
    display: flex;
    gap: var(--st-progress-gap);
    margin: 0 0 0.55rem;
    padding: 0;
}
/* One lit bar that SLIDES between the three faded tracks, rather than three bars switching on and
   off - the movement itself is what says "you moved". studio.js owns data-step (it follows the card
   actually on screen, so swiping, the arrows and tapping a label all drive it). */
.st-progress-slider {
    position: absolute;
    top: 0;
    left: 0;
    height: 3px;
    width: calc((100% - var(--st-progress-gap) * 2) / 3);
    border-radius: 3px;
    background: var(--bs-primary);
    box-shadow: 0 0 0 3px rgba(var(--bs-primary-rgb), 0.14);
    transform: translateX(0);
    transition: transform 0.32s cubic-bezier(0.4, 0, 0.2, 1);
    pointer-events: none;
}
/* translateX is a % of the SLIDER's own width, which is exactly one segment - so each step is one
   segment plus one gap further along. */
.st-progress[data-step="2"] .st-progress-slider {
    transform: translateX(calc(100% + var(--st-progress-gap)));
}
.st-progress[data-step="3"] .st-progress-slider {
    transform: translateX(calc(200% + var(--st-progress-gap) * 2));
}
@media (prefers-reduced-motion: reduce) {
    .st-progress-slider { transition: none; }
}
.st-progress-step {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    padding: 0;
    margin: 0;
    border: 0;
    background: none;
    cursor: pointer;
}
.st-progress-bar {
    display: block;
    height: 3px;
    border-radius: 3px;
    /* Dull = the brand colour at low alpha, not a grey: one hue across the strip, so the lit segment
       reads as the same bar turned up rather than a different thing. A solid dark grey vanished
       against the page. */
    background: rgba(var(--bs-primary-rgb), 0.22);
    transition: background-color 0.25s ease, box-shadow 0.25s ease;
}
.st-progress-lbl {
    display: block;
    font-size: 0.68rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    line-height: 1.25;
    color: var(--bs-secondary-color);
    text-align: center;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    transition: color 0.25s ease;
}
/* The bars themselves stay faded on EVERY step - the sliding indicator above is the only lit one.
   (A middle "completed" tint was tried first and read as a legend to decode rather than an answer
   to "where am I".) Only the label brightens with the current step. */
.st-progress-step.is-current .st-progress-lbl { color: var(--bs-body-color); font-weight: 700; }
.st-progress-step:hover .st-progress-lbl { color: var(--bs-body-color); }

/* The photo frame: a 3/4 box capped by --st-cap. Cards 1-2 stack the EzUpload tile (which grows)
   over the sample strip (natural height, gone once a photo is set); card 3 holds the result box. */
.st-photo {
    position: relative;
    display: flex;
    flex-direction: column;
    aspect-ratio: 3 / 4;
    max-height: var(--st-cap);
    overflow: hidden;
}
.st-up { flex: 1 1 auto; min-height: 0; display: flex; }
/* Empty only: the dashed zone is a control, so it sits inset from the card edges - matching the
   sample strip's inset below it - while a real photo still bleeds edge to edge. No bottom inset:
   the sample strip supplies that gap itself. */
.st-card:not([data-filled]) .st-up { padding: 0.55rem 0.55rem 0; }
/* The shared tile insets its image by 0.5rem, which framed every photo in a uniform band of the
   blurred backdrop. Here the image fills the tile, so the blur only appears where the photo's own
   shape genuinely leaves space - and its corners are the card's to clip, not the image's. */
.st-up .ez-upload-tile-link { padding: 0; }
.st-up .ez-upload-tile-img { border-radius: 0; }
.st-up .ez-upload { flex: 1; min-height: 0; display: flex; flex-direction: column; }
/* The shared tile carries its own aspect-ratio/min-height; inside the card the frame governs. */
.st-up .ez-upload-zone--tile {
    flex: 1;
    aspect-ratio: auto;
    min-height: 0;
    max-height: none;
    /* Empty: a transparent dashed zone, so it reads as an inset control and keeps its corners. */
    border-radius: var(--st-radius-inner);
}
/* --result is the outfit/styling result card; --pick is the styling studios' step 2 (the chosen style's
   catalog tile, which opens the picker). Both fill their frame with a .to-rbox instead of an upload
   tile, so they share this whole block. */
.st-photo--result .to-rbox,
.st-photo--pick .to-rbox {
    flex: 1;
    aspect-ratio: auto;
    min-height: 0;
    max-height: none;
    /* Opaque and edge to edge (the photo, the blurred fill, the generating box): the card's own
       clipping owns the corners here - rounding again would cut card background into the image. */
    border-radius: 0;
}
.st-up .ez-upload-zone--tile.ez-upload-zone--filled { border-radius: 0; }
/* Same two overrides as the upload tile above, for the result photo: the base box insets its image
   by 0.5rem and rounds it, which left step 3 framed in a rim of blurred backdrop while its two
   neighbours bled to the card edge. */
.st-photo--result .to-rbox-open,
.st-photo--pick .to-rbox-open { padding: 0; }
.st-photo--result .to-rbox img,
.st-photo--pick .to-rbox img { border-radius: 0; }
/* The idle / failed click zones are transparent dashed boxes, not filled ones - same inset and
   corners as the upload dropzone, so all three cards read alike before anything is in them. */
.st-photo--result .to-rbox.to-zone,
.st-photo--pick .to-rbox.to-zone {
    border-radius: var(--st-radius-inner);
    margin-inline: 0.55rem;
    /* The base .to-rbox is width:100%; left as-is the margins would push it past the right edge. */
    width: auto;
}
/* (Step 2 used to wait on step 1 - that lock is gone: either input can be filled first, and the
   cards only go inert while a generation runs. State still comes from Blazor as the data-filled
   ATTRIBUTE rather than a class, because studio.js owns the is-active class on the same elements
   and Blazor rewrites the whole class attribute when its own expression changes.) */

/* Sample strip inside the card: the shared SampleCarousel, tightened for the narrower frame. */
/* Top padding runs slightly larger than the bottom so the visible gaps match: the bottom one is
   measured to the card's outer edge and so picks up its 1px border. */
.st-samples { flex: 0 0 auto; padding: 0.7rem 0.55rem 0.6rem; }
.st-samples .ss { margin-top: 0; }
.st-samples .ss-head { margin-bottom: 0.35rem; }
.st-samples .ss-label { font-size: 0.68rem; text-wrap: balance; text-align: center; }
/* The removed filter row went here: thumbnails are the content a user actually has to judge (is that
   outfit worth a credit / is that the photo of me I want), and they get harder to read as the strip
   fills with the user's own saved photos rather than catalog shots. */
.st-samples .ss-thumb { width: 4.9rem; }

/* Action bar: a constant height so the cards stay aligned whatever state they are in. */
.st-bar {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 0.4rem;
    padding: 0 0.55rem;
    height: var(--st-barh);
    flex: 0 0 auto;
}
/* An empty card has no actions. At lg+ the row keeps the space reserved so the three cards stay the
   same height; below lg it collapses entirely and the reclaimed height goes straight to the photo
   area, so the dropzone grows and the card's TOTAL height is identical whether empty or filled -
   nothing jumps when a photo lands. The cap already budgets the bar either way. */
.st-card:not([data-filled]) .st-bar { visibility: hidden; }
/* ...except step 3 while a look is rendering: there is no result yet, but the bar carries the Cancel
   button, which is the only way out of a generation that has hung. The bar's height is already
   reserved, so nothing moves when it appears. */
.st-card--result[data-generating] .st-bar { visibility: visible; }
@media (min-width: 992px) {
    /* Same intent as the mobile rule below - the sample strip sits at the card's base rather than
       above a dead reserved strip - but the height cannot be computed here: at lg+ the card's width
       comes from the grid column, which may be narrower than the cap-derived --st-w. So instead the
       row stretches every card to the tallest (the filled one) and the empty card's photo grows into
       the space its collapsed bar left behind. With no card filled yet, all three are photo-only and
       already equal, so nothing has to grow. */
    .st-track { align-items: stretch; }
    .st-card:not([data-filled]):not(.st-card--result) .st-bar { display: none; }
    .st-card:not([data-filled]):not(.st-card--result) .st-photo { flex: 1 1 auto; max-height: none; }
}
@media (max-width: 991.98px) {
    .st-card:not([data-filled]):not(.st-card--result) .st-bar { display: none; }
    .st-card:not([data-filled]):not(.st-card--result) .st-photo {
        aspect-ratio: auto;
        max-height: none;
        height: calc(min((var(--st-w) - 2px) * 4 / 3, var(--st-cap)) + var(--st-barh));
    }
}
/* Arrows (scrolling modes only), on the card's midpoint. Not the photo's: an empty card collapses
   its action bar and grows its photo to compensate, so the photo's centre differs slightly between
   empty and filled cards. One shared pair cannot track both; the card's own centre lands well inside
   the photo either way. */
.st-nav {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    z-index: 5;
    width: 2.6rem;
    height: 2.6rem;
    border-radius: 50%;
    border: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: rgba(0, 0, 0, 0.45);
    color: #fff;
    font-size: 1rem;
    backdrop-filter: blur(2px);
    transition: opacity 0.2s ease;
}
.st-nav:disabled { opacity: 0.25; }
.st-nav--prev { left: 0.4rem; }
.st-nav--next { right: 0.4rem; }
@media (min-width: 992px) { .st-nav { display: none; } }

/* Phone in landscape: short and wide (~410px tall). There is no vertical room to give a card the
   screen one at a time, but plenty of width - so all three sit side by side like the desktop row,
   and the chrome that only exists to support swiping (arrows, dimmed neighbours) goes away. Keyed
   on height + orientation so a real tablet or desktop is untouched. Placed after the max-width
   rules above so it wins on source order. */
@media (orientation: landscape) and (max-height: 32rem) {
    .st-track {
        grid-auto-flow: row;
        grid-template-columns: repeat(3, minmax(0, calc(var(--st-cap) * 0.75)));
        justify-content: center;
        align-items: stretch;
        gap: 0.6rem;
        overflow: visible;
        scroll-snap-type: none;
        padding-inline: 0;
    }
    .st-nav { display: none; }
    .st-card { opacity: 1; transform: none; border-color: var(--bs-border-color); }
    .st-card.is-active {
        border-color: var(--bs-primary);
        box-shadow: 0 0 0 3px rgba(var(--bs-primary-rgb), 0.14);
    }
    /* Same empty-card treatment as the desktop row: the collapsed bar's height goes to the photo so
       the samples reach the card's base. The explicit height the portrait rule sets is replaced by
       flex growth, since the row's card width no longer comes from --st-w. */
    .st-card:not([data-filled]):not(.st-card--result) .st-photo {
        aspect-ratio: auto;
        height: auto;
        flex: 1 1 auto;
        max-height: none;
    }
}

/* Keep / Discard on step 3: overlaid on the result image, hugging its lower edge, so the decision sits
   on the thing being decided instead of competing with the view/zoom chrome in the bar below. The strip
   itself is click-through - only the two buttons take pointer events - so the image stays zoomable
   around them. Above .to-rbox-open (z-index 1) and the scan beam (3). */
.st-decide {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0.6rem;
    z-index: 4;
    display: flex;
    justify-content: center;
    gap: 0.5rem;
    pointer-events: none;
    animation: to-pop 0.4s ease-out;
}
.st-decide .btn { pointer-events: auto; }

/* Generating spinner on step 3: sits just above the status caption (.to-cap owns bottom 0.9rem), so
   it never collides with the centred effect whichever animation the admin picked. */
.st-genspin {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 2.9rem;
    z-index: 2;
    display: flex;
    justify-content: center;
    color: var(--bs-primary);
    pointer-events: none;
}
/* Sized well above Bootstrap's default so it reads as the card's "working" signal from across the
   room, not a footnote under the caption. */
.st-genspin .spinner-border {
    width: 3rem;
    height: 3rem;
    border-width: 0.3em;
}
/* Same three-part signal (beam + caption + spinner) inside a modelling-video tile, which is a fraction
   of a studio card's size - so the spinner is scaled to the tile instead of filling it. */
.mv-card .st-genspin { bottom: 2.4rem; }
.mv-card .st-genspin .spinner-border { width: 1.9rem; height: 1.9rem; border-width: 0.22em; }

/* ============================================================================================
   Looks feed (2026-07) - /looks, the home of the kept-looks gallery (moved off the studio page).
   Every look renders the SAME three-step card track as the studio: below md one step at a time,
   md to lg the neighbours peeking, lg and up a static row of three - so a saved look reads exactly
   like the studio that made it, at every width. The .st-track / .st-card / .st-photo / .st-bar /
   .st-nav / .st-progress rules above are shared VERBATIM; this block only supplies the tokens and
   the feed's own trimmings.
   The tokens live on the FEED root, not on each look, so every look shares ONE photo cap - looks.js
   measures it once per page and sets --st-cap here. (The studio declares the same set on .st-studio,
   where the cap is measured against the app header as well; the feed's header scrolls away.)
   ============================================================================================ */
.lk-feed {
    --st-cap: max(12rem, calc(100svh - 14rem));
    --st-pull: 0px;
    --st-barh: 48px;
    --st-radius-card: 14px;
    --st-radius-inner: var(--radius, 10px);
    --st-avail: calc(100vw - 2.5rem);
    --st-w: min(var(--st-avail), calc(var(--st-cap) * 0.75));
}
@media (min-width: 768px) and (max-width: 991.98px) {
    .lk-feed { --st-w: min(calc(var(--st-avail) * 0.72), calc(var(--st-cap) * 0.75)); }
}
/* Same reasoning as the studio's .st-ready guard: until looks.js has marked each track's card, show
   every card at full strength so the first paint does not flash all three dimmed. */
@media (max-width: 991.98px) {
    .lk-feed:not(.st-ready) .st-card { opacity: 1; transform: none; }
}

/* Studio filter above the feed: All / Outfits / Makeup / Hair. Sits outside .lk-feed so it is not
   affected by the feed's own tokens, and wraps on a narrow phone rather than scrolling sideways. */
.lk-filter { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 1.25rem 0; }

/* One photo treatment for all three cards: the .to-rbox letterbox (the whole image fitted over a
   blurred blow-up of itself) fills the card's photo frame edge to edge, exactly as the studio's
   result card does - the card already clips the corners, so the box gets no radius of its own. */
.lk-look .st-photo .to-rbox {
    flex: 1;
    aspect-ratio: auto;
    min-height: 0;
    max-height: none;
    border-radius: 0;
    border-color: transparent;
}
/* Only the result keeps the brand outline - it is the card the look is about. */
.lk-look .st-photo .to-rbox--done { border-color: var(--bs-primary); }

/* The result card carries every action for the look (view / videos / share / notes / delete, plus the
   admin QA pair), which is more than the studio's fixed-height bar can hold on a phone - so the feed's
   bar wraps to a second line instead of clipping buttons off the card. */
.lk-bar {
    height: auto;
    min-height: var(--st-barh);
    flex-wrap: wrap;
    padding-block: 0.35rem;
}

/* The video-count pill riding the modelling button: smaller than a stock Bootstrap badge, which
   overwhelms a sm icon button. */
.lk-count {
    font-size: 0.6rem;
}

/* When the look was kept. The day heading above carries the date, so this is the time alone. */
.lk-time {
    margin: 0.35rem 0 0;
    text-align: center;
    font-size: 0.75rem;
    color: var(--bs-secondary-color);
}

/* Empty feed: the you + outfit = look equation in placeholder form, centred, with the headline under
   it. Flat by design - dashed neutral tiles, the result tile carrying the same hairline brand ring the
   real result cards get, no panel and no shadow. The operator chips are the studio's own .to-op. */
.lk-empty { max-width: 26rem; margin: 0 auto; padding: 2.5rem 0 3.5rem; text-align: center; }
.lk-empty-eq { display: flex; align-items: center; justify-content: center; gap: 0.5rem; margin-bottom: 1.75rem; }
.lk-empty-tile { display: flex; align-items: center; justify-content: center; width: 3.5rem;
    aspect-ratio: 3 / 4; border-radius: 0.6rem; font-size: 1.05rem; color: var(--bs-secondary-color);
    border: 1px dashed var(--bs-border-color); background: var(--bs-secondary-bg); }
.lk-empty-tile--result { color: var(--bs-primary); border-style: solid;
    border-color: rgba(var(--bs-primary-rgb), 0.5); background: rgba(var(--bs-primary-rgb), 0.06); }
.lk-empty-title { margin: 0 0 0.5rem; }
.lk-empty-text { margin: 0; color: var(--bs-secondary-color); font-size: 0.9rem; line-height: 1.6; }

/* The step strip is a NAVIGATOR here, not a progress report - so it goes away in the layouts where
   all three cards are already on screen and there is nothing to navigate. (The studio keeps it at
   every width to say which step the workflow is up to; a kept look has no workflow left.) */
@media (min-width: 992px) {
    .lk-look .st-progress { display: none; }
}
@media (orientation: landscape) and (max-height: 32rem) {
    .lk-look .st-progress { display: none; }
}

/* Generating: all three cards are scanned at once, continuously - one beam per card, same timing,
   no delay and a linear curve so there is no pause at either end and they stay in step. (The old
   equation layout staggered them into a 1-2-3 sequence.) The beam is clipped by .st-photo's
   overflow:hidden, and on step 3 it sweeps over the spinner and the generating effect. */
.st-track.is-busy .st-photo::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: -20%;
    height: 16%;
    z-index: 4;
    pointer-events: none;
    background: linear-gradient(to bottom, transparent, rgba(var(--bs-primary-rgb), 0.35), transparent);
    border-bottom: 2px solid var(--bs-primary);
    animation: to-scany 1.6s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
    .st-track.is-busy .st-photo::after { animation: none; opacity: 0; }
}

/* ---- /shares: the owner's view of their public galleries and polls ----------------------
   Rows deliberately reuse the PUBLIC page's .sp-tile / .sp-result / .sp-result-bar family so the owner
   sees exactly what a visitor sees; only the surrounding chrome (header, meta, actions) is .sh-*. */
.sh-share {
    padding-block: 1.25rem;
    border-bottom: 1px solid var(--bs-border-color);
}
.sh-share:last-of-type { border-bottom: 0; }

.sh-head {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: 0.75rem;
    margin-bottom: 0.75rem;
}
.sh-ident { min-width: 0; }
.sh-title { margin-bottom: 0.25rem; }

.sh-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 0;
    font-size: 0.875rem;
    color: var(--bs-secondary-color);
}

/* Wraps rather than clipping: on a phone this is a status badge, a studio badge, a date, a count and an
   expiry, which is more than one line holds. */
.sh-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
}

.sh-search { max-width: 14rem; }

/* The dynamic tally width arrives as a custom property, so no rule is ever written inline. */
.sh-bar { width: var(--pct, 0%); }

/* Divides the studio / mode / status groups in the filter strip. */
.lk-filter-sep {
    width: 1px;
    align-self: stretch;
    min-height: 1.5rem;
    background: var(--bs-border-color);
}

/* ---- /looks selection: always-visible tick + selected ring + the action bar ---------------
   The tick sits TOP-LEFT of the result frame (the bottom edge is the video status strip, and top-left is
   where every photo picker puts selection). Selected state is driven by [data-selected] on .lk-look, never
   a class on the cards - looks.js owns .is-active there. */
.lk-pick {
    position: absolute;
    top: 0.5rem;
    left: 0.5rem;
    z-index: 3;
    display: inline-flex;
    padding: 0.25rem;
    border-radius: var(--bs-border-radius);
    background: rgba(var(--bs-body-bg-rgb), 0.75);
    cursor: pointer;
}
.lk-pick .form-check-input {
    margin: 0;
    cursor: pointer;
    width: 1.25rem;
    height: 1.25rem;
}

/* The ring hangs off the LOOK, so a selected look still reads as selected at step 1 or 2, where its
   checkbox has been swiped out of view. */
.lk-look[data-selected] .st-photo--result .to-rbox {
    outline: 2px solid var(--bs-primary);
    outline-offset: 2px;
}

/* Occupies the filter row's slot, so nothing shifts when a selection starts. Sticky, because the feed is
   long; top rather than bottom, because below the sidebar breakpoint the bottom is the tab bar. */
.lk-selbar {
    position: sticky;
    top: 0;
    z-index: 5;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    padding-block: 0.5rem;
    margin-bottom: 0.75rem;
    background: var(--bs-body-bg);
}
.lk-selcount { font-weight: 600; }
.lk-selnote {
    font-size: 0.875rem;
    color: var(--bs-secondary-color);
}
