/* 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: var(--bs-border-radius-lg); 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 */
/* Darker than the platform's upload tile (0.55): the backdrop's job here is to make the model stand
   out, and at 0.55 a grey studio wall blurred into a grey band the same value as the photo. */
.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.32) saturate(1.2); }
/* The studio's upload tiles (cards 1 and 2) take the same darker backdrop as the result box. */
.st-up .ez-upload-zone--tile.ez-upload-zone--filled::before { filter: blur(24px) brightness(0.32) saturate(1.2); }
.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; } }


/* status strip pinned to the bottom of a result box (video creating / created / failed, look under review).
   Rendered for a long time with no base rule at all, so it landed as an unstyled block AFTER the box and
   stole photo height; this is the "flat strip pinned to the bottom" the markup comment always described. */
.to-slot-status { position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
    display: flex; align-items: center; justify-content: center; gap: 0.4rem;
    padding: 0.4rem 0.6rem; font-size: 0.8rem; line-height: 1.2;
    background: var(--bs-body-bg); color: var(--bs-body-color); border-top: 1px solid var(--bs-border-color); }
.to-slot-status--creating { color: var(--bs-primary); }
.to-slot-status--created { color: var(--bs-success); }
.to-slot-status--error { color: var(--bs-danger); }
/* a look under staff review: the result is withheld, so the strip is the whole message - fill the frame */
.to-slot-status--held { top: 0; flex-direction: column; gap: 0.5rem; padding: 1.25rem; border-top: 0;
    font-size: 0.9rem; color: var(--bs-secondary-color); }
.to-slot-status--held i { font-size: 1.5rem; }

.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; }
/* The price of the action. It was 0.72rem in the secondary ink, which is the treatment this theme
   gives throwaway metadata - and what a look costs is the one number a user checks before pressing
   anything. Body size, body ink. */
/* Body font: this is the price and the user's balance, plain facts, and it was the only line on the
   card set in the monospace face - which made it read like a debug readout. */
.to-credit { font-size: 0.8125rem; color: var(--bs-body-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: var(--bs-border-radius-lg);
    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); }
/* cards: bundled example photos under an EMPTY upload card - a one-tap way to fill the card 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; }
/* An EzButton (the platform has one button component; a tile is a button whose content is a picture).
   Bootstrap's .btn paints hover, active and disabled at a specificity this single-class rule cannot
   reach, so the tile's fill, border and radius are handed to it through Bootstrap's OWN custom
   properties rather than fought for. display:block keeps the fitted image laying out exactly as it did
   inside the old inline-block <button>, instead of becoming a flex item of .btn's inline-flex box. */
.ss-thumb { position: relative; flex: 0 0 auto; display: block; 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(--bs-border-radius, 10px);
    background: var(--bs-secondary-bg); cursor: pointer; transition: border-color 0.15s ease;
    --bs-btn-padding-x: 0; --bs-btn-padding-y: 0;
    --bs-btn-border-radius: var(--bs-border-radius, 10px);
    --bs-btn-bg: var(--bs-secondary-bg); --bs-btn-hover-bg: var(--bs-secondary-bg);
    --bs-btn-active-bg: var(--bs-secondary-bg); --bs-btn-disabled-bg: var(--bs-secondary-bg);
    --bs-btn-border-color: var(--bs-border-color); --bs-btn-hover-border-color: var(--bs-primary);
    --bs-btn-active-border-color: var(--bs-primary); --bs-btn-disabled-border-color: var(--bs-border-color); }
/* 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: var(--bs-border-radius-pill); 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: var(--bs-border-radius); border: 1px solid var(--bs-border-color); background: var(--bs-secondary-bg); }
.ss-adminthumb img { width: 100%; height: 100%; object-fit: contain; }

/* 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, the standing "bring your looks to life" banner above the feed, went with the banner
   itself, and so did the just-kept "Create modelling video" bubble that replaced it.) */

/* modelling-videos manager: a responsive grid of portrait tiles. Each video tile is [pose name on top]
   / [thumbnail] / [share + delete]. With no videos the grid holds only the create card (the empty
   state); otherwise the create BAR (.mv-createbar) sits above it. 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; }

/* The create bar: the same place at every size, above the grid. Styles (when the admin configured more
   than one) on the left, the button and its cost on the right; below Bootstrap md they stack, the
   button full width under the styles. A hairline under it separates "make one" from "what you have". */
.mv-createbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.75rem 1.5rem;
    margin-bottom: 1.25rem;
    padding-bottom: 1.25rem;
    border-bottom: 1px solid var(--bs-border-color);
}
.mv-createbar-styles { flex: 1 1 18rem; min-width: 0; }
/* An empty picker (one style renders nothing) must not leave a gap before the button. */
.mv-createbar-styles:empty { display: none; }
.mv-createbar-go { display: flex; align-items: center; gap: 0.75rem; margin-left: auto; }
/* Bootstrap < md */
@media (max-width: 767.98px) {
    .mv-createbar-go { flex: 1 1 100%; margin-left: 0; }
    .mv-createbar-go .btn { flex: 1 1 auto; }
}
.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: var(--bs-border-radius-lg); overflow: hidden;
    border: 1px solid var(--bs-border-color); background: var(--bs-secondary-bg); }
/* Contain, like every photo: the whole frame, letterboxed on black, never cropped (owner, 2026-09-30).
   A video cannot carry the photos' blurred self-backdrop without decoding itself twice, so black it is. */
.mv-thumb video { width: 100%; height: 100%; object-fit: contain; display: block; background: #000; }
.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; }
/* The per-video "Include in shares" switch: centred under its video, one size down. */
/* Bootstrap hangs a switch in the wrapper's left padding with a negative margin; laid out as a flex row
   instead, so the gap - not a hand-tuned offset - is what separates the switch from its words. */
.mv-share { display: flex; align-items: center; justify-content: center; gap: 0.5rem; padding-left: 0;
    font-size: 0.875rem; }
.mv-share .form-check-input { flex-shrink: 0; margin: 0; }
/* create card: the SAME fixed 3/4 box as a video thumbnail, but styled like the card-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: var(--bs-border-radius-lg); 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; }
/* The sweep beam over a modelling-video card while it generates. Only the reduced-motion switch-off
   for it was ever declared, so the card showed the caption and the spinner with nothing sweeping
   behind them - the same three-part signal every other generating surface gives. Same beam as the
   studio track's .st-photo::after; .mv-card already clips it and is the positioning context. */
.mv-card--scan::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; }

/* full-card click zone (card 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 card 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-wf { pointer-events: auto; }

/* ---- workflow picker (StylePicker.razor) -------------------------------------------------------
   Always-visible rows on the card, not a pill that opens a menu: the facts that decide the choice
   were behind a click, so the user chose before seeing them. Design 08t from the step 3 mockups.

   NO per-fact glyphs, on purpose. VCR renders on pure black, and a 0.65rem glyph there has almost no
   ink and no ground of its own - it reads as lint. The brand rule down the fact block does the
   grouping the icons were meant to do, at one mark per row rather than three. */
.st-wf { display: flex; flex-direction: column; gap: 0.35rem; width: 100%; max-width: 22rem;
    text-align: left; }

.st-wf-row { display: grid; grid-template-columns: auto 1fr; gap: 0.15rem 0.5rem;
    align-items: start; width: 100%; margin: 0; padding: 0.45rem 0.6rem;
    background: var(--bs-body-bg); border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius-lg); cursor: pointer;
    transition: border-color 0.15s, background 0.15s; }
.st-wf-row:hover { border-color: var(--bs-primary); }
.st-wf-row.is-on { border-color: var(--bs-primary); background: rgba(var(--bs-primary-rgb), 0.08); }
.st-wf-row:has(:disabled) { cursor: default; opacity: 0.55; }
.st-wf-row:has(:focus-visible) { outline: 2px solid var(--bs-primary); outline-offset: 2px; }

/* The real radio carries keyboard and assistive-tech behaviour; the ring is what is actually seen. */
.st-wf-radio { position: absolute; opacity: 0; pointer-events: none; }
.st-wf-ring { grid-row: 1; width: 0.85rem; height: 0.85rem; margin-top: 0.15rem;
    border: 2px solid var(--bs-border-color); border-radius: 50%; background: var(--bs-body-bg);
    transition: border-color 0.15s, box-shadow 0.15s; }
.st-wf-row.is-on .st-wf-ring { border-color: var(--bs-primary);
    box-shadow: inset 0 0 0 0.18rem var(--bs-primary); }

/* Wraps rather than clipping: the row is 156px wide inside a phone-width card, so an ellipsis cut most
   workflow names off at the point where they start to differ from each other. */
.st-wf-name { grid-row: 1; font-size: 0.82rem; line-height: 1.35; color: var(--bs-body-color);
    min-width: 0; overflow-wrap: anywhere; }
.st-wf-row.is-on .st-wf-name { color: var(--bs-primary); }

/* 08t: one rule instead of three glyphs. Sits in the name's column so it lines up under the label
   rather than under the radio. */
.st-wf-facts { grid-column: 2; display: flex; flex-direction: column; gap: 0.1rem;
    padding-left: 0.5rem; border-left: 2px solid rgba(var(--bs-primary-rgb), 0.55);
    font-size: 0.7rem; line-height: 1.35; color: var(--bs-secondary-color); }
/* Same reason as the name above: a fact that does not fit wraps onto a second line rather than being
   cut short, which is the only failure mode that loses information. */
.st-wf-fact { min-width: 0; overflow-wrap: anywhere; }

/* A single workflow is not a choice, so it is not drawn as a selectable row: no box, no ring, no
   hover. Just the name over one line of facts, centred like the rest of the idle card. */
.st-wf--single .st-wf-row { grid-template-columns: 1fr; justify-items: center; text-align: center;
    cursor: default; border-color: transparent; background: transparent; padding-block: 0; }
.st-wf--single .st-wf-row:hover { border-color: transparent; }
.st-wf--single .st-wf-facts { grid-column: 1; flex-direction: row; flex-wrap: wrap; justify-content: center;
    gap: 0.1rem 0.4rem; padding-left: 0; border-left: 0; }
.st-wf--single .st-wf-fact + .st-wf-fact::before { content: "\00B7"; margin-right: 0.4rem; }
.st-wf--single.is-off { opacity: 0.55; }

/* Facts stay STACKED at every count - one line per fact is the whole point of 08t, and running them
   inline at three or four options would have meant the design changed shape exactly when there was
   most to compare. Height is handled by scrolling instead: four stacked options exceed the card, so
   the list scrolls rather than the layout degrading.

   The cap has to follow --st-cap, the measured photo frame the picker sits inside. A fixed 17rem is
   taller than the frame on a short phone, so the picker pushed the Generate chip and the price out of
   the card entirely - the two things the choice is made against. Leave room for those (about 7rem of
   chip, caption and price) and never go below a readable two rows. */
.st-wf { max-height: min(17rem, max(8rem, calc(var(--st-cap, 24rem) - 7rem))); overflow-y: auto; }

/* Room for the scrollbar so it never sits on top of a row border, and a little inset so a scrolled
   row does not touch the card edge. */
.st-wf { scrollbar-width: thin; padding-right: 0.15rem; }

/* Bootstrap < md: the equation stacks to one card 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; }
}

/* =====================================================================================
   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.
   ===================================================================================== */
/* A flight TARGET image, hidden until the ghost lands on it. visibility, not display: the element must
   still lay out, because the flight measures the slot it is going to land in. Only the image is
   hidden - the slot's blurred backdrop stays on, so the row never reads as a gap. */
.to-await { visibility: hidden; }

/* A size container, so each star's travel (--dx/--dy) is a share of the frame it plays on (cqw) rather
   than fixed pixels: fixed travel was tuned on a desktop card and crossed a whole phone card almost at
   once, so the stars left the photo before they could be seen. The overlay is absolutely positioned
   and sized by its frame, so size containment costs nothing here. */
.to-keepfx { position: absolute; inset: 0; z-index: 30; pointer-events: none; container-type: size; }
.kf-p { position: absolute; width: 9px; height: 9px; border-radius: 50%; }
/* shooting stars streak with trails */
.to-keepfx--star-shower .kf-p { width: 4px; height: 4px; 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: 1px; width: 40px; 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; } }
/* The fourteen stars. Their start point, travel and delay used to be drawn at random in C# and written
   onto each span as a style attribute - the one thing this codebase never does. The set is fixed here
   instead: fourteen authored variants read exactly like fourteen random ones (nobody counts a shower
   twice), and the component is left with nothing to emit but a class. */
/* Travel in cqw on BOTH axes (not cqh for --dy), so every streak keeps the 150deg angle its trail is
   drawn at whatever the frame's shape. The values are the old desktop pixels over a 26rem card. */
.to-keepfx--star-shower .kf-p--1  { left: 44%; top: -6%; --dx: -57cqw; --dy: 37cqw; --i: 0; }
.to-keepfx--star-shower .kf-p--2  { left: 58%; top: 12%; --dx: -68cqw; --dy: 47cqw; --i: 4; }
.to-keepfx--star-shower .kf-p--3  { left: 71%; top: -2%; --dx: -51cqw; --dy: 32cqw; --i: 1; }
.to-keepfx--star-shower .kf-p--4  { left: 86%; top: 20%; --dx: -76cqw; --dy: 54cqw; --i: 7; }
.to-keepfx--star-shower .kf-p--5  { left: 52%; top: 27%; --dx: -60cqw; --dy: 42cqw; --i: 2; }
.to-keepfx--star-shower .kf-p--6  { left: 95%; top:  5%; --dx: -71cqw; --dy: 35cqw; --i: 9; }
.to-keepfx--star-shower .kf-p--7  { left: 63%; top: -9%; --dx: -54cqw; --dy: 50cqw; --i: 5; }
.to-keepfx--star-shower .kf-p--8  { left: 78%; top: 31%; --dx: -80cqw; --dy: 39cqw; --i: 3; }
.to-keepfx--star-shower .kf-p--9  { left: 41%; top: 16%; --dx: -63cqw; --dy: 56cqw; --i: 8; }
.to-keepfx--star-shower .kf-p--10 { left: 90%; top: -4%; --dx: -49cqw; --dy: 45cqw; --i: 6; }
.to-keepfx--star-shower .kf-p--11 { left: 67%; top: 24%; --dx: -74cqw; --dy: 30cqw; --i: 0; }
.to-keepfx--star-shower .kf-p--12 { left: 49%; top:  8%; --dx: -66cqw; --dy: 52cqw; --i: 9; }
.to-keepfx--star-shower .kf-p--13 { left: 82%; top: 34%; --dx: -56cqw; --dy: 38cqw; --i: 2; }
.to-keepfx--star-shower .kf-p--14 { left: 74%; top: 10%; --dx: -70cqw; --dy: 48cqw; --i: 6; }


/* 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: var(--bs-border-radius-pill);
    background: rgba(var(--bs-primary-rgb), 0.1); border: 1px solid rgba(var(--bs-primary-rgb), 0.35); }
.sp-title { margin-bottom: 0.25rem; }
/* The caption is whatever the sharer typed, and people paste URLs into it. An unbreakable 60-character
   token has no wrap opportunity, so at 320px it pushed the whole public page sideways. */
.sp-lede { color: var(--bs-secondary-color); margin: 0; overflow-wrap: anywhere; }
.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: var(--bs-border-radius-lg); 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; }

/* One card per look (Deck in Pages/S/Public.razor): the framed 3:4 tile, with the look's photo as the
   first slide and each shared video as another, side by side in a native scroll-snap track - so a
   swipe works with no script, and a look with videos takes exactly the room of one without. The tile's
   blurred self-fill sits behind every slide, so a video letterboxes into the same backdrop. */
/* The CARD: one bordered box around the look and everything that belongs to it - its photo / video
   strip, its vote tally, its "Pick this" - so nobody has to guess which look a row of buttons or a
   percentage refers to. The photo frame inside keeps its own rounded corners. */
.sp-card { padding: 0.5rem; background: var(--bs-body-bg); border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius-xl); transition: border-color 0.15s ease, box-shadow 0.15s ease; }
.sp-card .sp-deck-frame { border-color: transparent; }
.sp-card .sp-result-meta { padding: 0 0.25rem 0.25rem; }
.sp-deck-track { position: absolute; inset: 0; z-index: 1; display: flex; overflow-x: auto;
    overscroll-behavior-x: contain; scroll-snap-type: x mandatory; scrollbar-width: none; }
.sp-deck-track::-webkit-scrollbar { display: none; }
.sp-deck-track:focus-visible { outline: 2px solid var(--bs-primary); outline-offset: -2px; }
.sp-slide { position: relative; flex: 0 0 100%; height: 100%; scroll-snap-align: start; scroll-snap-stop: always; }
.sp-slide-video { display: block; width: 100%; height: 100%; object-fit: contain; background: transparent; }
.sp-deck-frame .sp-flag { z-index: 2; }
/* Arrows for a mouse: a finger swipes, so they only appear where there is hover. */
.sp-deck-nav { position: absolute; top: 50%; z-index: 2; display: none; align-items: center; justify-content: center;
    width: 2.25rem; height: 2.25rem; border: 0; border-radius: 50%; color: #fff; background: rgba(0, 0, 0, 0.45);
    transform: translateY(-50%); transition: opacity 0.2s ease; }
.sp-deck-nav--prev { left: 0.5rem; }
.sp-deck-nav--next { right: 0.5rem; }
.sp-deck-nav:disabled { opacity: 0; pointer-events: none; }
@media (hover: hover) {
    .sp-deck-nav { display: inline-flex; opacity: 0; }
    .sp-deck:hover .sp-deck-nav:not(:disabled),
    .sp-deck-nav:focus-visible { opacity: 1; }
}
/* The strip under the card is the explanation: a photo icon, then a play icon per video. The lit one is
   the slide on screen; tapping one goes there. A photo-only card in a share that has videos reserves the
   same height (--empty), so every card and what sits under it stays level. */
.sp-deck-strip { display: flex; justify-content: center; gap: 0.4rem; min-height: 1.75rem; margin-top: 0.5rem; }
.sp-deck-dot { display: inline-flex; align-items: center; justify-content: center; width: 1.75rem; height: 1.75rem;
    padding: 0; font-size: 0.7rem; color: var(--bs-secondary-color); background: transparent;
    border: 1px solid var(--bs-border-color); border-radius: var(--bs-border-radius-pill);
    transition: color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease; }
.sp-deck-dot:hover { color: var(--bs-primary); border-color: var(--bs-primary); }
/* Black on the brand fill: white on the light brand purple is about 2.6:1, black about 8:1. */
.sp-deck-dot.is-active { color: #000; background: var(--bs-primary); border-color: var(--bs-primary); }

/* poll option card: the PHOTO slide is a submit button (name="option" value="{id}"), so one tap on the
   look casts the vote, and "Pick this" under the card is a second submit for the same option. The video
   slides sit outside any button. .is-choosing (on the card) marks the tapped option while
   poll-captcha.js solves the challenge. */
.sp-option { position: absolute; inset: 0; display: block; width: 100%; height: 100%; padding: 0; border: 0;
    background: none; color: inherit; font: inherit; cursor: pointer; }
.sp-option-card:hover { border-color: var(--bs-primary); }
.sp-option-pick { display: flex; align-items: center; justify-content: center; gap: 0.5rem; width: 100%;
    margin-top: 0.5rem; padding: 0.25rem 0.5rem; font: inherit; font-size: 0.85rem; font-weight: 500; cursor: pointer;
    color: var(--bs-secondary-color); background: none; border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius-pill);
    transition: color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease; }
.sp-option-pick:hover { color: var(--bs-primary); border-color: var(--bs-primary); }
.sp-option-card.is-choosing { border-color: var(--bs-primary);
    box-shadow: 0 0 0 2px rgba(var(--bs-primary-rgb), 0.35); }
.sp-option-card.is-choosing .sp-option-pick, .sp-option-pick:active { color: #000;
    background: var(--bs-primary); border-color: var(--bs-primary); }
.sp-option:focus-visible { outline: 2px solid var(--bs-primary); outline-offset: -2px; }
.sp-option-pick:focus-visible { outline: 2px solid var(--bs-primary); outline-offset: 2px; }

/* poll results: ranked tiles with a share-of-votes bar; the leader gets the flat primary ring */
/* The leader's whole card is ringed, not just its photo: the tally under it belongs to it too. */
.sp-result--winner { 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; justify-content: center; 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: var(--bs-border-radius-pill);
    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: var(--bs-border-radius-xl);
    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 card-1 heading where it is actionable. */
.to-cardhint { color: var(--bs-secondary-color); font-size: 0.8125rem; text-align: center; margin: 0 0 0.5rem; }
/* (The mobile sticky generate bar - .to-sticky / -btn / -cost - went with the equation layout it was
   written for. The studio card track scrolls card 3 into view itself, so nothing floats over the tab
   bar any more and no markup references those classes. Do not re-add them.) */

/* ============================================================================================
   Studio card track (2026-07) - the responsive three-card 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 card at a time, swipe or arrows
     md to lg   peek  - the current card centred, neighbours peeking and dimmed
     lg and up  row   - all three cards 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;
    /* What a card reserves under its photo, budgeted into the photo cap by studio.js. 0: every action
       now sits ON the photo (.st-media-actions top right, .st-decide along the bottom), so the three
       photos are the same height in every state. Kept as a token rather than deleted so the JS budget
       and this file cannot disagree if anything is ever put back under the photo. */
    --st-barh: 0px;
    /* Radius scale, largest on the outside: card > inset elements > controls (the icon buttons bring
       their own 8px). Inner values come from the platform --bs-border-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: var(--bs-border-radius-xl);
    --st-radius-inner: var(--bs-border-radius, 10px);
    --st-avail: calc(100vw - 2.5rem);
    /* A ceiling on the card, because every other term grows with the WINDOW: on a tall ultra-wide
       screen the cap-derived width ran past any size a portrait photo is worth showing at, and the
       three cards spread to the edges of the room. 26rem is the same cap the stacked history rows
       have always used for one photo. */
    --st-wmax: 26rem;
    --st-w: min(var(--st-avail), calc(var(--st-cap) * 0.75), var(--st-wmax));
    /* 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);
}
/* Bootstrap sm to md: the phone rule hands the card the whole viewport, which on a 600-760px tablet
   leaves the neighbours completely off screen and no hint that the track scrolls. This range was
   simply missing between the phone rule and the md-to-lg peek below, so it fell back to full width. */
@media (min-width: 576px) and (max-width: 767.98px) {
    .st-studio { --st-w: min(calc(var(--st-avail) * 0.84), calc(var(--st-cap) * 0.75), var(--st-wmax)); }
}
@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), var(--st-wmax)); }
}
@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); }
/* Bootstrap < md: the card is WIDTH-bound on a phone (full width, 3:4), so centring split a tall band's
   leftover evenly and parked a wide gap between the strip and its card - the strip read as detached
   from what it labels. Drop the band so the wrap is just the card, hung under the strip; the leftover
   goes to the bottom, above the tab bar. Shrinking the wrap (rather than top-aligning inside the band)
   keeps the .st-nav arrows, which centre on the wrap, on the card's midpoint. The strip's own bottom
   margin is the gap. */
@media (max-width: 767.98px) {
    .st-wrap { min-height: 0; }
}

/* 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;
        /* --st-wmax applies here too: the column is cap-derived, so without it a tall window makes
           each card grow without limit. */
        grid-template-columns: repeat(3, minmax(0, min(calc(var(--st-cap) * 0.75), var(--st-wmax, 26rem))));
        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 card 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);
    }
}

/* Card strip: three slim segments above the track, one per card - 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 cards
   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 so the two stylesheets never collide - a bare .st-card 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-card (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 card is one
   segment plus one gap further along. */
.st-progress[data-card="2"] .st-progress-slider {
    transform: translateX(calc(100% + var(--st-progress-gap)));
}
.st-progress[data-card="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-card {
    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 card - 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 card. */
.st-progress-card.is-current .st-progress-lbl { color: var(--bs-body-color); font-weight: 700; }
.st-progress-card:hover .st-progress-lbl { color: var(--bs-body-color); }
/* Bootstrap < md: one card fills the screen, so the strip is the only wayfinding and has room to spare
   - the labels are one word each, so they can be read at arm's length instead of squinted at. */
@media (max-width: 767.98px) {
    /* A little more air under the labels than the 0.55rem default, now that the card hangs straight
       under the strip instead of being centred away from it. */
    .st-progress { margin-bottom: 1rem; }
    .st-progress-card { gap: 0.5rem; }
    .st-progress-lbl { font-size: 0.9375rem; letter-spacing: 0.01em; }
}

/* 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; }
/* ONE photo treatment on every card, tile and dialog photo: the image is contain-fitted EDGE TO EDGE
   over a dark blurred blow-up of itself (.to-rbox--fill / EzUpload's --tile-fill). A 3:4 photo fills
   the frame completely; the backdrop only shows where a photo of another shape leaves space. The
   frame's own clipping owns the corners, so the image carries no radius. --st-inset is 0 today and is
   kept as the one knob that would bring a band back on every surface at once. */
.st-up .ez-upload-tile-link { padding: var(--st-inset, 0); }
.st-up .ez-upload-tile-img,
.st-photo .to-rbox img,
.lk-tile .to-rbox img { border-radius: 0; }
/* ONE frame per card: the card's own rounded border. The photo boxes inside it (the filled upload tile,
   the result box with its brand ring, the style tile) each drew a square-cornered border of their own
   1px inside the card's, which read as the photo stopping short of the edge and as broken corners where
   the square inner line met the card's curve. Inside a card they draw nothing; the card clips them. */
.st-photo .ez-upload-zone--tile.ez-upload-zone--filled,
.st-photo .to-rbox:not(.to-zone) {
    border: 0;
    box-shadow: none;
}
/* Every photo is framed the same way, results included: the whole image, contain-fitted, over the dark
   blurred blow-up of itself (.to-rbox--fill / .lk-detail-bg). Results used to be cover-cropped so a
   "3:4-ish" model output (896x1200 is 0.747) filled the card without a thin band - but that cropped the
   edges of the look and made card 3 read differently from cards 1 and 2 beside it (owner, 2026-09-30).
   One treatment everywhere; the backdrop fills whatever band a result leaves. */
.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' card 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 inset as the upload tile above (see --st-inset), so card 3 and the styling pick card frame their
   photo exactly like cards 1 and 2. */
.st-photo--result .to-rbox-open,
.st-photo--pick .to-rbox-open { padding: var(--st-inset, 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;
}
/* (Card 2 used to wait on card 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; }

/* ---- Card actions ride the photo ------------------------------------------------------------
   There is no action bar under the photo any more. It was an empty strip on cards 1 and 2 (one menu
   holding two items) and a taller, wrapping one on card 3, so the three photos were never the same
   height. Now every card is photo-only, and its actions sit on the photo.
   THE placement standard, the same on every studio card, History tile and dialog photo. Everything is
   measured from the photo's own edge (--st-edge), so every control sits the same distance in on every
   card:
     top left     - ONE action menu (.st-photo-menu; EzUpload's TileActionsAsMenu on cards 1-2) holding
                    every tool for that photo: view, download or replace, change, remove, admin QA.
                    Outline trigger at full strength - never faint.
     top right    - selection (History tiles only)
     bottom       - the card's purpose, centred: Keep / Discard, Cancel, or a look's videos / share /
                    notes (labels that grow on hover, .lk-grow); a caption or status chip sits above it
   The photo frame is the positioning context for all of them. */
.st-studio,
.lk-feed,
.lk-detail { --st-inset: 0px; --st-edge: calc(var(--st-inset) + 0.5rem); }
.st-photo-menu,
.st-up .ez-upload-tile-menu {
    position: absolute;
    top: var(--st-edge, 0.5rem);
    left: var(--st-edge, 0.5rem);
    z-index: 3;
}
/* While its menu is OPEN the cluster rises above everything laid over the photos. The panel lives inside
   this z-index:3 wrapper, and the foot rows (.lk-tile-foot, also 3, later in the DOM - this tile's AND
   the neighbouring tile's) and the studio's Keep/Discard (.st-decide, 5) painted over the open list.
   Only while open, so at rest the cluster keeps its place under the result's own overlays. */
.st-photo-menu:has(.ez-action-menu__panel),
.st-up .ez-upload-tile-menu:has(.ez-action-menu__panel) { z-index: 10; }

/* Thumbs up / down on the fresh result, top right - the corner the History gives to selection, which
   card 3 does not have. Two sibling icon buttons in the shared .ez-media-actions family (faint at rest,
   solid on hover, focus and touch): an optional opinion, so it stays quiet next to Keep / Discard. */
.st-rate {
    position: absolute;
    top: var(--st-edge, 0.5rem);
    right: var(--st-edge, 0.5rem);
    z-index: 3;
    display: flex;
    gap: 0.25rem;
}

/* Buttons over a photo that GROW their label: icon-only at rest (a square, like EzButton's own icon
   buttons), the word sliding open on hover or keyboard focus. Replaced floating tooltips, which sat over
   the neighbouring button's count badge and the tile's status line. The label is a real EzButton Label
   the whole time - only its width animates - so it stays the button's accessible name. Touch screens
   have no hover, and the icon stays a square there. */
/* The motion: the button widens on a soft ease-out, then the word fades and slides in a beat later, so
   it arrives into space that is already there instead of being squeezed out of a closing gap. Closing
   runs the other way round - the word leaves first, quickly, then the button tucks back in. */
.lk-grow {
    --lk-ease: cubic-bezier(0.22, 1, 0.36, 1);
    --bs-btn-padding-x: 0;
    min-width: calc(var(--bs-btn-line-height) * 1em + var(--bs-btn-padding-y) * 2 + var(--bs-btn-border-width) * 2);
    gap: 0;
    /* Keeps the button's own colour transitions, which a bare "transition: padding" would drop. */
    transition:
        padding 0.3s var(--lk-ease) 0.05s,
        color 0.15s ease-in-out,
        background-color 0.15s ease-in-out,
        border-color 0.15s ease-in-out;
}
.lk-grow > span:not(.ez-btn-badge) {
    max-width: 0;
    overflow: hidden;
    opacity: 0;
    transform: translateX(-0.3rem);
    transition:
        max-width 0.3s var(--lk-ease) 0.05s,
        margin 0.3s var(--lk-ease) 0.05s,
        opacity 0.1s ease-in,
        transform 0.1s ease-in;
}
.lk-grow:is(:hover, :focus-visible) {
    transition-delay: 0s, 0s, 0s, 0s;
}
.lk-grow:is(:hover, :focus-visible) > span:not(.ez-btn-badge) {
    transition:
        max-width 0.3s var(--lk-ease),
        margin 0.3s var(--lk-ease),
        opacity 0.2s ease-out 0.1s,
        transform 0.25s var(--lk-ease) 0.1s;
}
@media (hover: hover) {
    .lk-grow:hover { --bs-btn-padding-x: 0.65rem; }
    .lk-grow:hover > span:not(.ez-btn-badge) { max-width: 6rem; opacity: 1; transform: none; margin-left: 0.4rem; }
}
.lk-grow:focus-visible { --bs-btn-padding-x: 0.65rem; }
.lk-grow:focus-visible > span:not(.ez-btn-badge) { max-width: 6rem; opacity: 1; transform: none; margin-left: 0.4rem; }
@media (prefers-reduced-motion: reduce) {
    .lk-grow,
    .lk-grow:is(:hover, :focus-visible),
    .lk-grow > span:not(.ez-btn-badge),
    .lk-grow:is(:hover, :focus-visible) > span:not(.ez-btn-badge) { transition: none; }
}
.st-decide {
    position: absolute;
    left: 0;
    right: 0;
    bottom: var(--st-edge, 1.15rem);
    z-index: 5;
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    gap: 0.5rem;
    padding-inline: 0.5rem;
}
/* While a look renders the scan beam and the status caption own the lower part of the box; the
   Cancel button sits under the caption rather than on top of it. */
.st-card--result[data-generating] .to-cap { bottom: calc(var(--st-edge, 1.15rem) + 2.5rem); }
.st-card--result[data-generating] .st-genspin { bottom: calc(var(--st-edge, 1.15rem) + 4.5rem); }

@media (min-width: 992px) {
    /* The row stretches every card to the tallest, and an empty card's photo grows into it - so the
       sample strip sits at the card's base rather than above a dead band. */
    .st-track { align-items: stretch; }
    .st-card:not([data-filled]):not(.st-card--result) .st-photo { flex: 1 1 auto; max-height: none; }
}
@media (max-width: 991.98px) {
    /* An empty card 1 or 2 holds the dropzone AND the sample strip, so it is given the full photo
       height explicitly (the 3/4 ratio would squeeze the dropzone by the strip's height). */
    .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 vertical midpoint either side of the track (owner
   decision 2026-09-27: kept here, not beside the strip). */
.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, min(calc(var(--st-cap) * 0.75), var(--st-wmax, 26rem))));
        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;
    }
}

/* Generating spinner on card 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; }

/* ============================================================================================
   History (the kept looks under each studio): a GRID of result tiles.
   Each look used to render the studio's whole three-card track, a full screen per look on a phone.
   Now a look is one 3/4 tile of its result - two across on a phone, three from md, four from lg -
   with every action on the photo: the select tick top left, the menu top right (.st-media-actions,
   shared with the studio cards), and the look's three purposes along its foot (.lk-tile-foot). The
   two inputs live in the tile's detail dialog (.lk-detail).
   ============================================================================================ */
.lk-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.75rem;
}
/* Bootstrap md */
@media (min-width: 768px) {
    .lk-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem; }
}
/* Bootstrap lg */
@media (min-width: 992px) {
    .lk-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
.lk-tile { position: relative; min-width: 0; }
/* The tile's photo: the shared .to-rbox letterbox (the whole image over a blurred blow-up of itself),
   filling the tile edge to edge. The fixed 3/4 frame is what keeps every tile the same height whatever
   shape the result came out. */
/* Same corner radius as a studio card (--st-radius-card), so a kept look reads as the card it came from. */
.lk-tile .to-rbox {
    aspect-ratio: 3 / 4;
    min-height: 0;
    max-height: none;
    border-radius: var(--bs-border-radius-xl);
}
.lk-tile .to-rbox-open { padding: var(--st-inset, 0); }
/* The foot: the video status strip (static here, not pinned like the studio's) over the icon row. */
.lk-tile-foot {
    position: absolute;
    left: var(--st-edge, 0.5rem);
    right: var(--st-edge, 0.5rem);
    bottom: calc(var(--st-edge, 0.5rem) - 0.4rem);
    z-index: 3;
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
}
/* The status strip becomes a rounded chip inside the photo's edge, above the actions, rather than a
   bar across the whole frame - same "everything inside the image" rule as the clusters. */
.lk-tile-foot .to-slot-status {
    position: static;
    align-self: center;
    font-size: 0.75rem;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius-pill);
}
/* The gap and top padding leave room for the video count pill, which straddles its button's corner. */
.lk-tile-actions {
    display: flex;
    justify-content: center;
    gap: 0.8rem;
    padding: 0.5rem 0 0.4rem;
}
/* The video-count pill riding the modelling button: smaller than a stock Bootstrap badge, which
   overwhelms a sm icon button, and lifted above the neighbouring button so it is never painted over. */
.lk-count {
    z-index: 2;
    font-size: 0.6rem;
    pointer-events: none;
}

/* The look's detail dialog: the three photos side by side, as a scroll-snap row on a phone. */
.lk-detail {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 1rem;
}
/* Bootstrap < md */
@media (max-width: 767.98px) {
    .lk-detail {
        grid-template-columns: none;
        grid-auto-flow: column;
        grid-auto-columns: 78%;
        overflow-x: auto;
        scroll-snap-type: x mandatory;
        overscroll-behavior-x: contain;
    }
    .lk-detail-fig { scroll-snap-align: center; }
}
.lk-detail-fig { margin: 0; min-width: 0; }
.lk-detail-photo {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    aspect-ratio: 3 / 4;
    overflow: hidden;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius-xl);
    background: var(--bs-secondary-bg);
}
/* The backdrop: a blown-up, darkened blur of the same image - the .to-rbox--fill treatment, done with
   an element instead of a per-image custom property. */
.lk-detail-bg {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    transform: scale(1.2);
    filter: blur(24px) brightness(0.32) saturate(1.2);
    pointer-events: none;
}
.lk-detail-open {
    position: absolute;
    inset: 0;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--st-inset, 0);
    border: 0;
    background: none;
    cursor: zoom-in;
}
.lk-detail-open img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
}
.lk-detail-photo--result .lk-detail-open { padding: 0; }
.lk-detail-photo--result { border-color: var(--bs-primary); }
.lk-detail-photo--ph { font-size: 2rem; color: var(--bs-secondary-color); }
/* The label over each dialog photo: the studio strip's own bar + label (.st-progress-bar /
   .st-progress-lbl), with the look itself lit, exactly as the studio shows it. */
.lk-detail-head {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    margin-bottom: 0.55rem;
}
.lk-detail-head.is-current .st-progress-bar { background: var(--bs-primary); }
.lk-detail-head.is-current .st-progress-lbl { color: var(--bs-body-color); font-weight: 700; }

/* The History heading, under the studio's own three steps. Outside .lk-feed so it is not affected by
   the feed's tokens; the saved count sits on the baseline beside it rather than under it. */
.lk-histhead {
    display: flex;
    align-items: baseline;
    gap: 0.75rem;
    margin: 2.5rem 0 1rem;
}
.lk-histhead h2 { margin: 0; }

/* Room above a look when it is scrolled to. The History heading and the tail of the studio's own steps
   stay in frame, so the look that just arrived reads as having ARRIVED somewhere rather than as the
   page having jumped to an unlabelled photo. */
.lk-tile { scroll-margin-top: 4.5rem; }

/* The just-kept look, for the couple of seconds after Keep. This is the moment the whole move exists
   for: the look the user just made arrives at the top of their history, where the share, video and
   notes buttons are. A ring plus a settle, not a flash - the eye needs to be led there, not startled.
   Reduced motion keeps the ring and drops the movement. */
@keyframes lk-kept-in {
    from { transform: translateY(-0.75rem); opacity: 0; }
    to   { transform: none; opacity: 1; }
}
.lk-tile[data-kept] {
    animation: lk-kept-in 420ms ease-out both;
}
.lk-tile[data-kept] .to-rbox {
    outline: 2px solid var(--bs-primary);
    outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
    .lk-tile[data-kept] { animation: none; }
}

/* When the look was kept, under the photos in its detail dialog. */
.lk-time {
    margin: 0.75rem 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: var(--bs-border-radius-lg); 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; }

/* 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 sync. (The old
   equation layout staggered them into a 1-2-3 sequence.) The beam is clipped by .st-photo's
   overflow:hidden, and on card 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-*. */
/* The list: one full-width card per share, stacked, at every screen size (owner decision 2026-09-28 -
   side-by-side columns were tried and read as unrelated boxes). The card's tiles grid is what uses the
   extra width on a wide screen. */
.sh-list {
    display: flex;
    flex-direction: column;
    gap: 1rem;
}
.sh-card {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    min-width: 0;
    padding: 1rem;
    background: var(--bs-body-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius-xl);
}
/* A dead share (expired or stopped) steps back: its looks dim, its controls stay at full strength so
   "Share again" is still the obvious thing to do. */
.sh-card--ended .sh-tiles { opacity: 0.55; }

/* Menu, then Link. */
.sh-toolbar { display: flex; align-items: center; gap: 0.5rem; }
.sh-confirm { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.75rem; }
.sh-confirm-actions { display: inline-flex; gap: 0.5rem; margin-left: auto; }

/* Same caption, same pasted URLs - see .sp-lede. */
.sh-title { margin-bottom: 0; overflow-wrap: anywhere; }
/* Bootstrap < sm: one step down, the same as the modal titles, so a share name stays on one line. */
@media (max-width: 575.98px) {
    .sh-title { font-size: 1.25rem; line-height: 1.2; letter-spacing: -0.02em; }
}

/* The link's lifetime: the two dates side by side at either end, the bar spanning the full width under
   them, and the time left beneath it. (A three-column version with the words squeezed BETWEEN the dates
   truncated to "30 day..." on a phone.) No frame: three small facts do not need a box of their own, and
   the box made them the tallest thing on the card. */
.sh-life {
    display: flex;
    flex-direction: column;
    gap: 0.375rem;
}
.sh-life-ends { display: flex; justify-content: space-between; gap: 1rem; }
.sh-life-end { display: flex; flex-direction: column; gap: 0.125rem; min-width: 0; }
.sh-life-end--right { text-align: right; }
.sh-life-label {
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--bs-secondary-color);
}
.sh-life-date { font-size: 0.875rem; font-weight: 600; white-space: nowrap; }
.sh-life-left { font-size: 0.8125rem; font-weight: 600; text-align: center; }

/* "No votes yet", said once above an unvoted poll's photos instead of "0% 0 votes" under each. */
.sh-novotes { display: flex; align-items: center; gap: 0.5rem; margin: 0; font-size: 0.875rem;
    color: var(--bs-secondary-color); }
/* The unfilled track in the brand colour at low alpha, the same treatment as the studio strip's bars -
   the stock track is near-black on this theme, so a young link's sliver of fill had nothing to sit on. */
.sh-life-bar { height: 0.375rem; background: rgba(var(--bs-primary-rgb), 0.22); }

/* The looks, sized by the CARD rather than the viewport: two across in a phone-width card, more as the
   card widens. The public page's own .sp-card / .sp-tile treatment inside. */
.sh-tiles {
    display: grid;
    /* 8rem, not 9: a phone card's inner width (~18.5rem) only fit ONE 9rem column plus the gap. */
    grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
    gap: 0.75rem;
}
/* Bootstrap md+: the card is full width, so an 8rem floor would line up ten stamp-sized looks across a
   desktop. 13rem keeps them photo-sized (about four to six across). */
@media (min-width: 768px) {
    .sh-tiles { grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr)); gap: 1rem; }
}

/* The public link in the share dialog. A share URL is longer than any field the dialog can give it,
   and this dialog exists precisely BECAUSE the clipboard was unavailable - so the link is shown as
   selectable text that wraps, not a readonly field showing its first third. */
.sh-link {
    display: block;
    margin-bottom: 0.5rem;
    padding: 0.5rem;
    font-family: var(--bs-font-monospace);
    font-size: 0.8125rem;
    color: var(--bs-body-color);
    overflow-wrap: anywhere;
    word-break: break-all;
    background: var(--bs-secondary-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
}

.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);
}

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

/* The share's own settings at the top of the link dialog (poll, show my name): switches, boxed so they
   read as settings OF this link rather than more text about it. */
.sh-settings {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    margin-bottom: 1rem;
    padding: 0.75rem 1rem;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius-lg);
}
/* The filter strip itself. It was written with separators and a trailing search box but never had a
   rule of its own, so the chips ran together with no gaps, the 1px separators had nothing to stretch
   against (zero height, invisible) and the search input dropped onto a line of its own. */
.lk-filter {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 1rem;
}
/* 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);
}

/* ---- History selection: always-visible tick + selected ring + the action bar ---------------
   The tick sits TOP-RIGHT of the tile (top-left is the action menu on every photo in the app, and the
   foot is the look's own actions). Selected state is driven by [data-selected] on
   .lk-tile, never a class expression. */
.lk-pick {
    position: absolute;
    top: var(--st-edge, 0.5rem);
    right: var(--st-edge, 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;
}

.lk-tile[data-selected] .to-rbox {
    outline: 2px solid var(--bs-primary);
    outline-offset: 2px;
}

/* FIXED to the bottom of the viewport while anything is selected, so the actions stay under the thumb
   that is doing the ticking however far down the History it has gone. (It was a sticky row at the top
   and scrolled away with the page.) Below xl it sits on top of the bottom tab bar - the same 4.25rem
   clearance the app surface reserves for that bar - and the History reserves its own height at the end
   so the last row of tiles can still scroll clear of it. */
/* Floats over the feed, fixed to the bottom of the screen, centred on the FEED COLUMN rather than the
   window: looks.js publishes the column's left edge and width (--lk-col-*). Sticky cannot do this - an
   ancestor of the feed clips - and before the script measures, it falls back to centring on the window. */
.lk-selbar {
    position: fixed;
    left: calc(var(--lk-col-left, 0px) + var(--lk-col-width, 100vw) / 2);
    bottom: calc(1rem + env(safe-area-inset-bottom));
    z-index: 1040;
    transform: translateX(-50%);
    width: min(40rem, calc(var(--lk-col-width, 100vw) - 2rem));
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem 0.75rem;
    padding: 0.5rem 0.75rem;
    background: var(--bs-body-bg);
    border: 1px solid var(--bs-primary);
    border-radius: var(--bs-border-radius-lg);
}
/* Bootstrap < xl: the bottom tab bar is showing. */
@media (max-width: 1199.98px) {
    .lk-selbar { bottom: calc(4.25rem + env(safe-area-inset-bottom) + 0.5rem); }
}
/* Room under the last row so the floating bar never covers it. */
body:has(.lk-selbar) .lk-feed { padding-bottom: 6rem; }
/* Toasts also live at the bottom (ez-platform.css .ez-toast-host), so while this bar floats there
   they stack just ABOVE it instead of covering it - a Delete's Undo toast lands right by the bar that
   raised it. The anchor() line wins wherever anchor positioning is supported; the calc before it is
   the fallback, sized for a one-line bar above the tab bar. The leading `html` outranks the platform's
   own tab-bar lift (same shape, and stylesheet order between the two is not guaranteed). */
.lk-selbar { anchor-name: --lk-selbar; }
html body:has(.lk-selbar) .ez-toast-host {
    bottom: calc(4.25rem + env(safe-area-inset-bottom) + 5rem);
    bottom: calc(anchor(--lk-selbar top) + 0.5rem);
}
.lk-selcount { font-weight: 600; }
/* The two share actions stay together and push to the end of the bar, wrapping as a pair. */
.lk-selactions { display: inline-flex; flex-wrap: wrap; align-items: center; justify-content: flex-end;
    gap: 0.5rem 0.75rem; margin-left: auto; }
/* The share-name switch sits inline with the buttons: no field-stack margin, and smaller, muted text so
   the two actions stay the loudest things in the bar. */
.lk-selname { margin-bottom: 0; font-size: 0.875rem; color: var(--bs-secondary-color); }
.lk-selnote {
    font-size: 0.875rem;
    color: var(--bs-secondary-color);
}

/* The History tile's video button, which the video-count pill hangs off (.lk-count). The "Create
   modelling video" bubble that also hung here after a Keep was removed (owner decision 2026-09-28). */
.kfb-anchor { position: relative; display: inline-flex; }

/* ---- 44px touch targets (WCAG 2.2 AA, 2.5.8) -------------------------------------------------
   Three controls here are drawn smaller than a finger: the sample-strip chevrons (1.6rem), the studio
   card arrows (2rem) and the look-selection tick (its chip is 30px). Each keeps the size it is DRAWN
   at and grows an invisible, centred 44px target instead - padding would have inflated the circle and
   the chip along with it, and these sit over photographs where the visible mark is deliberately small.
   (Only the statically positioned ones are given position:relative - .st-nav and .lk-pick are already
   absolute, and overriding that here would drop them out of their frames.) */
.ss-arrow { position: relative; }
.ss-arrow::after,
.st-nav::after,
.lk-pick::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 44px;
    height: 44px;
    transform: translate(-50%, -50%);
}
