/*
    File: carousel/css/standalone-carousel.css
    Version: 2.0.0 - SIMPLIFIED: Generate button moved to generate-button.css
    Date: 2026-01-09
    
    ~160 lines (was ~300)
    Carousel layout only - generate button styles in generate-button.css
*/

/* ═══════════════════════════════════════════════════════════════════════════
   CSS VARIABLES - Aspect ratio driven by data attributes
═══════════════════════════════════════════════════════════════════════════ */

/* --image-width / --image-height / --image-aspect set on .carousel-standalone
   by the Django template from IMAGE_BASE. Fallbacks here for safety. */
.carousel-standalone {
    --default-width: var(--image-width, 512px);
    --default-height: var(--image-height, 768px);
    /* UNIFIED with the :root seed (#1a1a2e, fieldsets-base.css) — this scope used
       to override to #161618, so every color-mix/box/gradient/translucent-bubble
       backdrop resolved against a DIFFERENT base on the cards than on the page
       chat fieldset: the whole card read duller/darker with a shifted hue. One
       base everywhere = card colors match the page. */
    --card-base: #1a1a2e;
    /* Lighter work surface for the card faces — keeps dark slot colours visible
       instead of blending into near-black card-base (what the old hardcoded #22222a
       did, but tokenized + neutral). Tune the white % to taste. */
    --card-surface: color-mix(in srgb, white 6%, var(--card-base));
    --card-gap: 20px;
    --card-zoom: 1;
    /* End-card centering runway, CSS-owned and card-size-agnostic. 50% each side lets ANY
       card — including the narrow height-driven grid card — scroll until its centre meets
       the strip centre. LIVE percentage: recomputes on rotate/resize, and scroll-snap
       (mandatory, align:center) re-snaps the centred card for free — that's the rotate
       self-correction the measure-once JS padding used to break. The old value
       (50% - IMAGE_WIDTH/4) was tuned to one card size and drifted once the card derived
       its width from the grid row. scroll-snap keeps every card dead-centre; the extra
       runway is unreachable slack it corrects, never empty space you rest in.
       Mobile portrait overrides to 4px below (full-width card needs no runway). */
    --strip-padding: 50%;
}

/* ═══════════════════════════════════════════════════════════════════════════
   ASPECT RATIO SYSTEM — derives from --image-width (portrait base)
   Landscape: height becomes width (swap), Square: base x base
═══════════════════════════════════════════════════════════════════════════ */

/* Per-element role re-resolution — the fix for "every card takes the centered
   card's colour." Roles are seeded on .aurora-standalone (body) so AMBIENT chrome
   (page bg, header) follows the centered card. But a custom property's var()
   resolves on the element where the ROLE is DECLARED — so a card reading
   var(--secondary) would get the BODY's (centered/ambient) --slot-2, not its own.
   Redeclare the roles HERE, on every element that carries its OWN inline --slot-N,
   so they resolve against THIS card/slot. In-carousel HAL cards carry no inline
   slots → var(--slot-N) inherits the ambient value → they stay ambient, as intended. */
.strip-card,
.roster-strip-thumb {
    --primary: var(--slot-1);
    --secondary: var(--slot-2);
}

.strip-card {
    --card-width: var(--image-width, 512px);
    --card-aspect: var(--image-aspect, 2 / 3);
    --card-ar: 0.6667;   /* same ratio as --card-aspect, as a plain number
                            (width ÷ height) so calc() can turn an available
                            height into a width — used by the mobile fit. */
}

.strip-card[data-aspect-ratio="landscape"] {
    --card-width: var(--image-height, 768px);
    --card-aspect: 3 / 2;
    --card-ar: 1.5;
}

.strip-card[data-aspect-ratio="square"] {
    --card-width: var(--image-width, 512px);
    --card-aspect: 1 / 1;
    --card-ar: 1;
}

/* ═══════════════════════════════════════════════════════════════════════════
   FILM STRIP CONTAINER
═══════════════════════════════════════════════════════════════════════════ */

.film-strip-container {
    width: 100%;
    overflow: hidden;
    position: relative;
    display: flex;
    flex-direction: column;
}

/* ═══════════════════════════════════════════════════════════════════════════
   SCROLLING STRIP
═══════════════════════════════════════════════════════════════════════════ */

.scrolling-strip {
    order: 2;
    display: flex;
    align-items: center;            /* No-op when height = card height; kept for the mobile-landscape variant that height-drives the cards */
    gap: var(--card-gap);
    overflow-x: auto;
    overflow-y: hidden;
    scroll-behavior: smooth;
    scroll-snap-type: x mandatory;
    padding-top: 0px;
    padding-bottom: 5px;
    padding-left: var(--strip-padding);   /* Space so first card can center + filmstrip runway */
    padding-right: var(--strip-padding);  /* Space so last card can center */
    /* Strip height = card height. The earlier calc(100vh - 60px) left
       inches of whitespace above and below the cards and pushed the
       preview fieldset past the viewport by the transport bar's height
       on every page (gallery, character detail, scene detail). Mobile
       portrait already overrode this to height:auto for the same
       reason; mobile landscape still height-drives the cards. */
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    visibility: hidden;             /* Hidden until JS filmstrip positions and reveals */
}

/* TRAILING RUNWAY so the LAST card can scroll to centre. padding-right can't do this —
   flex scroll containers clip their end padding out of the scroll range, so the right
   50% gave the last card nothing to scroll into (the "brick wall", last card only). A
   ::after is a REAL flex item, so it extends the scroll range like a card would — "the
   only thing that drives the conveyor is more product." And it's ALWAYS the last flex
   child, so generated cards appendChild'd to the strip land BEFORE it: no trailing
   spacer wedged between the old cards and the new one (the gap bug from before). 50% =
   half the strip, more than the (stripW - cardW)/2 any card needs. CSS, so it recomputes
   on rotate. Mobile portrait needs none (full-width card centres with no runway). */
.scrolling-strip::after {
    content: '';
    flex: 0 0 50%;
    pointer-events: none;
}
@media (max-width: 700px) {
    .scrolling-strip::after { flex-basis: 4px; }
}

.scrolling-strip::-webkit-scrollbar { display: none; }

/* ═══════════════════════════════════════════════════════════════════════════
   STRIP CARD
═══════════════════════════════════════════════════════════════════════════ */

.strip-card {
    flex: 0 0 auto;
    width: calc(var(--card-width) * var(--card-zoom, 1));
    max-width: calc(100vw - 40px);
    aspect-ratio: var(--card-aspect);
    scroll-snap-align: center;
    border-radius: 12px;
    overflow: visible;
    position: relative;
    cursor: pointer;
    transition: transform 0.3s ease-out, box-shadow 0.3s ease-out, opacity 220ms ease;
    background: transparent;
}

/* Pure-HTML navigation overlay: non-centered scene cards have an
   <a> anchor stretched across the card. Browser follows the href
   on click — no JS. Buttons inside cards stack above via z-index
   so they remain clickable. The centered card never renders this
   anchor (rendered conditionally in template), so its existing
   click/flip behavior is unaffected. */
.strip-card-nav-link {
    position: absolute;
    inset: 0;
    z-index: 1;
    display: block;
    text-decoration: none;
    border-radius: inherit;
}
/* Z-index protection only on non-centered cards — that's where the
   .strip-card-nav-link <a> exists. On the centered card, absolute-
   positioned controls (.card-front-reset, .card-front-chat-tts,
   etc.) need to keep their own `position:
   absolute`; a blanket .strip-card button { position: relative } here
   beats those single-class selectors on specificity and flattens them
   into the flex flow.

   EXCEPTION: .prompt-card-close (the × on PromptCard cards — auth,
   upgrade, confirm) is a corner-overlay button that MUST stay
   position:absolute. PromptCard cards have no .strip-card-nav-link, so
   they don't need this protection. Without the :not() the × flattens to
   the top-LEFT while the card slides in (before it's .centered), then
   snaps right once centered — the left-flash. */
.strip-card:not(.centered) button:not(.prompt-card-close),
.strip-card:not(.centered) input,
.strip-card:not(.centered) select,
.strip-card:not(.centered) .deck-orb,
.strip-card:not(.centered) .builder-tag {
    position: relative;
    z-index: 2;
}

/* v9.4: FILM STRIP DEPTH SYSTEM
   ═══════════════════════════════════════════════════════════════════════════
   
   THE STORY:
   1. Side cards: small & recessed (waiting in the wings)
   2. Hover side card: perks up slightly ("pick me!")
   3. Centered card: full size, hero moment (front of the strip)
   4. Flipped card: lifts toward user (ready to work with forms)
   
   WHY translateY not scale for flipped:
   Scale changes element geometry which breaks Select2 overlay positioning.
   TranslateY lifts visually without changing dimensions.
   
   ═══════════════════════════════════════════════════════════════════════════ */

/* Base: side cards are smaller and recessed */
.strip-card {
    transform: scale(0.92);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}

/* Hover side card: perks up */
.strip-card:hover {
    transform: scale(0.95);
    box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
}

/* Centered card: full size hero */
.strip-card.centered {
    transform: scale(1.0);
    box-shadow: 0 12px 30px rgba(0, 0, 0, 0.35);
}

/* Centered + hover: same as centered (already the hero) */
.strip-card.centered:hover {
    transform: scale(1.0);
    box-shadow: 0 12px 30px rgba(0, 0, 0, 0.35);
}

/* Flipped: same as centered - no translateY needed anymore.
   The size difference (0.92 → 1.0) already creates the "lifted" feel.
   Keeping scale(1.0) preserves geometry for Select2 overlays. */
.strip-card.flipped {
    transform: scale(1.0);
    box-shadow: 0 12px 30px rgba(0, 0, 0, 0.35);
}

/* ═══════════════════════════════════════════════════════════════════════════
   CARD IMAGE
═══════════════════════════════════════════════════════════════════════════ */

.strip-card-image {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    border-radius: inherit;
}

.strip-card-placeholder {
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    background: linear-gradient(135deg, var(--card-base) 0%, var(--card-base) 50%, color-mix(in srgb, var(--primary) 5%, var(--card-base)) 100%);
    color: rgba(255, 255, 255, 0.4);
    border-radius: inherit;
}

.strip-card-placeholder-icon { font-size: 4rem; margin-bottom: 1rem; }
.strip-card-placeholder-text { font-size: 1rem; text-transform: uppercase; letter-spacing: 0.1em; }

/* ═══════════════════════════════════════════════════════════════════════════
   CARD OVERLAY
═══════════════════════════════════════════════════════════════════════════ */

.strip-card-overlay {
    position: absolute;
    bottom: 4px;
    top: auto;
    left: 4px;
    right: 4px;
    padding: 0.5rem 0.75rem;
    background: linear-gradient(transparent, rgba(0, 0, 0, 0.25));
    color: rgba(255, 255, 255, 0.92);
    opacity: 0;
    transition: opacity 0.3s ease-out;
    pointer-events: none;
    border-radius: 0 0 10px 10px;
    text-align: center;
}

/* Mobile: show by default, hide when chat or elements active */
@media (max-width: 768px) {
    .strip-card-overlay {
        opacity: 1;
    }
    .strip-card:has(.card-front-chat-overlay) .strip-card-overlay {
        opacity: 0;
    }
}

.strip-card:hover .strip-card-overlay { opacity: 1; }

/* Hide name overlay when the chat overlay is mounted (always-on for the
   centered card on the scene page — gives the chat input room). */
.carousel-standalone[data-entity-type="scene"] .strip-card.centered:hover .strip-card-overlay {
    opacity: 0;
}

/* Gallery (character carousel): the centered card's overlay is always
   visible. The point of the gallery is to read this metadata to pick
   a character; hiding it behind a hover defeats the purpose. Side
   cards still hover-to-show as before. */
.carousel-standalone[data-entity-type="character"] .strip-card.centered .strip-card-overlay {
    opacity: 1;
}
/* ...and ONLY the centered card. On the gallery the metadata overlay never
   shows on side cards — not on desktop hover, not on every card on mobile.
   Higher specificity (0,5,0) than both the global .strip-card:hover reveal and
   the mobile .strip-card-overlay{opacity:1}, so it wins in every viewport. */
.carousel-standalone[data-entity-type="character"] .strip-card:not(.centered) .strip-card-overlay {
    opacity: 0;
}

/* Global toggle — scene control "Overlay" button strips every floating control
   off the card fronts so you see a clean image. Covers:
     - name/title overlay
     - card-front generate orb (not the big GENERATE on placeholder cards)
     - chat input + conversation scrollback (the persistent
       `.card-front-chat-overlay` container — user can still chat from
       the scene_update form). The in-row buttons (elements toggle, TTS,
       aspect, lock) ride that display:none for free.
     - the top-left reset orb. */
.carousel-standalone.overlays-off .strip-card .strip-card-overlay,
.carousel-standalone.overlays-off .strip-card .card-front-chat-overlay,
.carousel-standalone.overlays-off .strip-card.centered .card-front-reset {
    display: none;
}

.strip-card-title {
    font-size: 0.875rem;
    font-weight: 600;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    color: color-mix(in srgb, var(--primary) 45%, white);
    text-shadow: 0 0 6px color-mix(in srgb, var(--secondary) 50%, rgba(0, 0, 0, 0.6));
}

/* Chub V2 metadata on imported gallery cards. Sits under the title in
   the bottom overlay strip. Empty when card has no creator_notes/tags
   (Aurora-native characters) — template skips the wrapper so there's
   no dead space. */
.strip-card-meta {
    margin-top: 0.35rem;
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    text-shadow: 0 0 4px color-mix(in srgb, var(--secondary) 50%, rgba(0, 0, 0, 0.65));
}
.strip-card-desc {
    font-size: 0.75rem;
    line-height: 1.25;
    color: white;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
.strip-card-tags {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.2rem;
}
.strip-card-tag {
    font-size: 0.65rem;
    line-height: 1;
    padding: 0.18rem 0.4rem;
    border-radius: 999px;
    background: color-mix(in srgb, var(--primary) 30%, transparent);
    border: 1px solid color-mix(in srgb, currentColor 25%, transparent);
    color: white;
    white-space: nowrap;
}

/* ── Editor gallery: EDITABLE tags on the card front (gallery-tags.js) ──
   The read-only chips become removable builder-tag bubbles + a select2 add-
   input, stacked below the pile and above the owner byline. The overlay is
   pointer-events:none by default; only the CENTERED card's edit region takes
   pointer events, so the picker + × are live there and inert elsewhere. */
.strip-card-meta--edit {
    margin-top: 0.35rem;
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    align-items: stretch;
    text-shadow: none;
}
.strip-card.centered .strip-card-meta--edit,
.strip-card.centered .strip-card-meta--edit * {
    pointer-events: auto;
}
.strip-card-tags--edit {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.2rem;
}
/* Editable bubble = the read-only chip scale, with an inline × remove. */
.strip-card-tags--edit .builder-tag {
    display: inline-flex;
    align-items: center;
    gap: 0.15rem;
    font-size: 0.65rem;
    line-height: 1;
    padding: 0.18rem 0.4rem;
    border-radius: 999px;
    background: color-mix(in srgb, var(--primary) 30%, transparent);
    border: 1px solid color-mix(in srgb, currentColor 25%, transparent);
    color: white;
    white-space: nowrap;
}
.strip-card-tags--edit .tag-remove {
    border: none;
    background: none;
    padding: 0;
    font: inherit;
    font-size: 0.85em;
    line-height: 1;
    cursor: pointer;
    color: white;
    opacity: 0.65;
}
.strip-card-tags--edit .tag-remove:hover { opacity: 1; }

/* Add-tag select2 — below the pile, full width, aligned card-to-card above the
   username. Styled after the card-back mood field: dark inset box, defined
   border, small type. dropdownParent is body, so the list isn't card-clipped. */
.strip-card-meta--edit .select2-container { width: 100%; }
.strip-card-meta--edit .select2-container--default .select2-selection--single {
    height: auto;
    min-height: 1.7rem;
    background: rgba(0, 0, 0, 0.35);
    border: 1px solid rgba(255, 255, 255, 0.2);
    border-radius: 4px;
    box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.45);
}
.strip-card-meta--edit .select2-selection--single .select2-selection__rendered,
.strip-card-meta--edit .select2-selection--single .select2-selection__placeholder {
    font-size: 0.7rem;
    line-height: 1.6;
    color: white;
    padding-left: 0.3rem;
}

/* Editor notes — author-curated blurb. Sits below the chub tags
   and above the owner byline. A touch larger/brighter than
   creator-notes so it reads as "what the editor wants you to know
   about this character." Two-line clamp keeps cards from growing
   on long entries. */
.strip-card-editor-notes {
    margin-top: 0.35rem;
    font-size: 0.78rem;
    line-height: 1.3;
    color: color-mix(in srgb, var(--primary) 25%, white);
    text-shadow: 0 0 4px rgba(0, 0, 0, 0.7);
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* Owner byline — sits at the very bottom of the overlay, below the
   chub tags. Quiet text so it doesn't compete with the title/tags. */
.strip-card-owner {
    margin-top: 0.35rem;
    font-size: 0.65rem;
    line-height: 1;
    text-align: center;
    opacity: 0.65;
    text-shadow: 0 0 4px rgba(0, 0, 0, 0.65);
}

/* Gallery card-front CTA — Chat (everyone) + Edit (own gallery). Lets a new
   user skip the center-into-a-slot dance entirely: click Chat, land in the
   conversation. The overlay is pointer-events:none and only shown on the
   centered card, so re-enable pointer-events on the pills — they're live only
   where the overlay is visible. Grey chrome (toolbar/private-toggle convention)
   so it reads on any character's colours; the icon + label carry meaning. */
.strip-card-actions {
    display: flex;
    justify-content: center;
    gap: 0.4rem;
    margin-top: 0.5rem;
}
.strip-card-cta {
    pointer-events: auto;
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0.34rem 0.72rem;
    border-radius: 999px;
    cursor: pointer;
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.02em;
    line-height: 1;
    text-decoration: none;
    background: color-mix(in srgb, white 10%, var(--card-base));
    border: 1px solid rgba(255, 255, 255, 0.18);
    color: rgba(255, 255, 255, 0.9);
    box-shadow: 0 1px 6px rgba(0, 0, 0, 0.35);
    transition: background 0.15s ease-out, color 0.15s ease-out;
}
.strip-card-cta:hover {
    background: color-mix(in srgb, white 16%, var(--card-base));
    color: rgba(255, 255, 255, 0.98);
}
.strip-card-cta svg { opacity: 0.85; }

/* ═══════════════════════════════════════════════════════════════════════════
   NAV PIPS
═══════════════════════════════════════════════════════════════════════════ */

/* PAGE TITLE — LAST row of the preview grid (pips 1, carousel 2, roster 3, title 4),
   the mirror of the pip row at the bottom of the fieldset. Both are 6% tracks, so they
   balance and the card reads centred between them.
   Caps + the pip's muted neutral: it's a where-am-i marker, not a heading, and it
   carries no slot colour (same reasoning as the pips — it indexes the page, not the
   character). */
.strip-title {
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    container-type: size;   /* so the label can be a share of this row */
}
.strip-title-label {
    font-size: 40cqh;   /* a share of the title row */
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    line-height: 1;
    white-space: nowrap;
    color: rgba(255, 255, 255, 0.3);   /* matches the idle pip */
}

.strip-nav {
    order: 1;
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 8px;
    padding: 0;
    position: relative;
    container-type: size;   /* so the dot can be a share of this row */
}

.strip-pip {
    height: 34cqh;        /* a share of the pip row */
    aspect-ratio: 1;
    width: auto;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.3);
    border: none;
    cursor: pointer;
    transition: all 0.2s ease-out;
    padding: 0;
}

.strip-pip:hover {
    background: rgba(255, 255, 255, 0.6);
    transform: scale(1.2);
}

.strip-pip.active {
    /* System-level neutral, NOT the character's outfit colour. The pips index
       the gallery/strip (shared across characters), so a per-character dress
       accent bled across navigation and persisted between the two galleries.
       Matches the inactive/hover pips, which are already neutral whites. */
    background: rgba(255, 255, 255, 0.85);
    transform: scale(1.3);
}

/* ═══════════════════════════════════════════════════════════════════════════
   FLIP CARD
═══════════════════════════════════════════════════════════════════════════ */

.flip-container {
    width: 100%;
    aspect-ratio: inherit;
    position: relative;
    perspective: 1200px;
    transform-style: preserve-3d;
    border-radius: 12px;
}

.flip-container > .flip-front,
.flip-container > .flip-back {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    box-sizing: border-box;  /* Padding + border stay inside card bounds */
    backface-visibility: hidden;
    border-radius: 12px;
    transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}

/* ── Standardized face chrome — ALL 4 faces share ONE recipe so they can't drift
   (front / back / memories / chat). Eyes (slot-2) tint from the TOP-RIGHT corner
   (225deg) fading to the lighter --card-surface grey, with an eyes border + glow.
   The chat fieldset stays transparent so it shows this same face gradient; the
   assistant bubbles (eyes) then sit in the opposite corner against the plain
   surface — the balance the chat fieldset had. Replaces the four hair + hardcoded
   #22222a copies. ── */
.flip-container > .flip-front,
.flip-container > .flip-back,
.flip-container > .flip-memories,
.flip-container > .flip-chat {
    background: linear-gradient(225deg,
        color-mix(in srgb, var(--secondary) 22%, var(--card-surface)) 0%,
        color-mix(in srgb, var(--secondary) 10%, var(--card-surface)) 40%,
        var(--card-surface) 60%,
        var(--card-surface) 100%);
    border: 2px solid color-mix(in srgb, var(--secondary) 40%, rgba(255, 255, 255, 0.15));
    box-shadow:
        0 4px 20px rgba(0, 0, 0, 0.4),
        0 0 15px color-mix(in srgb, var(--secondary) 20%, transparent),
        inset 0 2px 6px rgba(0, 0, 0, 0.3),
        inset 0 -1px 0 rgba(255, 255, 255, 0.1),
        inset 0 1px 0 rgba(255, 255, 255, 0.06);
}

.flip-container > .flip-front {
    transform: rotateY(0deg);
    overflow: hidden;
    padding: 4px;
}

.flip-container > .flip-front .strip-card-image,
.flip-container > .flip-front .strip-card-placeholder {
    border-radius: 10px;
    box-shadow: 
        inset 0 2px 6px rgba(0, 0, 0, 0.3),
        inset 0 -1px 0 rgba(255, 255, 255, 0.1);
}

.flip-container > .flip-back {
    transform: rotateY(180deg);
    display: flex;
    flex-direction: column;
    overflow: hidden;
    padding: 4px;
}

.flip-container > .flip-back::before { display: none ; }

.strip-card.flipped .flip-container > .flip-front { transform: rotateY(-180deg); }
.strip-card.flipped .flip-container > .flip-back { transform: rotateY(0deg); }

/* ═══ Third face: Memories ═══════════════════════════════════════════
   A sibling face (can't nest a 3D flip inside .flip-back — its overflow
   flattens preserve-3d). Same chrome as .flip-back; parked at rotateY(180)
   behind the back. The Memories button adds .showing-memories, which rotates
   the back away (0→180) and this in (180→0) — a real flip from the back. */
.flip-container > .flip-memories {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    box-sizing: border-box;
    backface-visibility: hidden;
    border-radius: 12px;
    transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
    transform: rotateY(180deg);
    display: flex;
    flex-direction: column;
    overflow: hidden;
    padding: 4px;
}
/* .showing-memories rides alongside .flipped; declared after the .flipped rules
   so it wins for the back + memories faces when both classes are present.
   The back rotates -180 (NOT +180) so it sweeps the SAME direction as the
   front->back flip — otherwise the two halves cross and the flip reads funny. */
.strip-card.showing-memories .flip-container > .flip-front { transform: rotateY(-180deg); }
.strip-card.showing-memories .flip-container > .flip-back { transform: rotateY(-180deg); }
.strip-card.showing-memories .flip-container > .flip-memories { transform: rotateY(0deg); }

/* Homing rules live AFTER .showing-chat (below) so they win — see there. */

/* ═══ Fourth face: Chat ═══ Parked at rotateY(180) behind the others (same as
   memories); .showing-chat rotates memories away (0→-180) and this in (180→0).
   The forward flip reaches it after memories; the wrap (chat→front) uses the
   homing spin above. Slice-1 chrome; the full-thread fieldset fills it next. */
.flip-container > .flip-chat {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    box-sizing: border-box;
    backface-visibility: hidden;
    border-radius: 12px;
    transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
    transform: rotateY(180deg);
    display: flex;
    flex-direction: column;
    overflow: hidden;
    padding: 4px;
}
.strip-card.showing-chat .flip-container > .flip-front { transform: rotateY(-180deg); }
.strip-card.showing-chat .flip-container > .flip-back { transform: rotateY(-180deg); }
.strip-card.showing-chat .flip-container > .flip-memories { transform: rotateY(-180deg); }
.strip-card.showing-chat .flip-container > .flip-chat { transform: rotateY(0deg); }

/* Homing: the LAST step of the forward flip (memories→front on a 3-face card,
   chat→front on a 4-face card) continues the SAME direction as the other flips
   — the front spins another -180 (right→left) to the resting front — instead of
   un-winding left→right (the "morph"). MUST be declared after every .showing-*
   rule (same specificity) so it wins while both classes are on the card; being
   above .showing-chat is exactly what made the chat→front flip morph. The JS
   snaps home under .flip-no-anim once the spin lands (rotateY(-360)==0 visually). */
.strip-card.flip-homing .flip-container > .flip-front { transform: rotateY(-360deg); }
.strip-card.flip-homing .flip-container > .flip-back { transform: rotateY(-180deg); }
.strip-card.flip-homing .flip-container > .flip-memories { transform: rotateY(-180deg); }
.strip-card.flip-homing .flip-container > .flip-chat { transform: rotateY(-180deg); }
.strip-card.flip-no-anim .flip-container > .flip-front,
.strip-card.flip-no-anim .flip-container > .flip-back,
.strip-card.flip-no-anim .flip-container > .flip-memories,
.strip-card.flip-no-anim .flip-container > .flip-chat { transition: none; }
.flip-chat-placeholder {
    margin: auto;
    font-size: 1.2rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.4);
}
/* The chat fieldset lives INSIDE the chat flip-face. CRUCIAL: the scene page's
   `.entity-2col .entity-fields .fieldset--chat` rules (the 100svh height, the
   conversation sizing) ALSO match this nested card copy and OUT-SPECIFY a plain
   `.flip-chat` selector — that's why it clipped with no scrollbar (fieldset got
   the VIEWPORT height, my overflow was ignored). Prefix with the card ancestor
   chain so these win: drop the fieldset's own eyes border/tint, FILL the face,
   SCROLL the conversation, and pin the input + footers visible. */
.strip-card .flip-container .flip-chat .fieldset--chat {
    border: none;
    background: none;
    margin: 0;
    height: auto;
    min-height: 0;
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
}
.strip-card .flip-container .flip-chat .fieldset--chat .chat-conversation-wrap {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
}
.strip-card .flip-container .flip-chat .fieldset--chat .chat-conversation {
    flex: 1 1 auto;
    height: auto;
    min-height: 0;
    overflow-y: auto;   /* SCROLL the messages instead of growing past the input */
}
.strip-card .flip-container .flip-chat .fieldset--chat .chat-form-input,
.strip-card .flip-container .flip-chat .fieldset--chat .chat-followups,
.strip-card .flip-container .flip-chat .fieldset--chat .chat-voice-row--footer {
    flex: 0 0 auto;   /* pin visible; the conversation above scrolls */
}
/* Chat text keyed to the card-back / memory vmax master, nudged one notch up from
   the panes' clamp(11px, 1.5vmax, 13px) so the conversation reads a touch larger. */
.strip-card .flip-container .flip-chat .fieldset--chat .chat-msg {
    font-size: clamp(11px, 1.6vmax, 14px);
}
/* Scale the card-face avatar down so it covers less of the conversation and frees
   the header row — the h2's clear-padding shrinks with it, exposing more of the
   character switcher (select2). */
.strip-card .flip-container .flip-chat .fieldset--chat .chat-avatar-thumb {
    width: 3.6rem;   /* was 5rem → ~5.4rem tall at the 2/3 aspect */
}
.strip-card .flip-container .flip-chat .fieldset--chat > h2 {
    padding-left: 4.8rem;   /* was 6.4rem — less avatar to clear */
}
/* Blocked-state CTA under the now-smaller avatar: shrink the Upgrade button toward
   the avatar's width, and let the "Daily limit… 00:00 UTC" flag fill + CENTER the
   freed space so UTC stays on the top line instead of wrapping. */
.strip-card .flip-container .flip-chat .fieldset--chat .tier-info-upgrade {
    padding: 0.3rem 0.5rem;   /* was 0.35rem 0.95rem */
    font-size: 0.68rem;       /* was 0.75rem */
    margin-left: calc(-6.125rem + 21px);   /* nudge the broken-out button right (was +11px) */
}
/* Pull the whole blocked drawer up ~10px — the L-header left too much gap above the
   black box. (Base margin-top is 0.15rem.) */
.strip-card .flip-container .flip-chat .fieldset--chat .tier-info-drawer.open {
    margin-top: calc(0.15rem - 10px);
}
/* Center the flag on the BLACK BOX, not the button-skewed flex space: as a flex
   item its centering was dragged left by the button's negative-margin breakout.
   Absolute-fill the CTA row so text-align centers on the box regardless. */
.strip-card .flip-container .flip-chat .fieldset--chat .tier-info-cta {
    position: relative;
}
.strip-card .flip-container .flip-chat .fieldset--chat .tier-info-flag {
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    transform: translateY(-50%);
    margin: 0;
    text-align: center;
}
/* Expand the black tier box left into the void the smaller avatar left behind
   (its clear-margin was tuned to the old 5rem avatar), and give it a 2px right gap
   (it touched the right edge with no padding). */
.strip-card .flip-container .flip-chat .fieldset--chat .tier-info-block {
    margin-left: calc(5.75rem - 17px);   /* was -14px — 3px further left */
    margin-right: 2px;
}
/* Line the card-face avatar's left edge up with the conversation's left border on
   mobile (it sat ~7px to the right of it). */
@media (max-width: 700px) {
    .strip-card .flip-container .flip-chat .fieldset--chat .chat-avatar-well {
        left: calc(0.6rem - 7px);
    }
    /* The L-header bar (the raised box behind the switcher + deck buttons) sits too
       LOW on mobile, so the select overshoots its top edge. Raise it ~6px. (Desktop
       reads a touch high but the select still clears it, so it's left alone.) */
    .strip-card .flip-container .flip-chat .fieldset--chat:has(.chat-avatar-well)::before {
        top: calc(0.6rem - 6px);
    }
}

/* Memories face — mirrors the cast fieldset: swatch/name header, a thumb +
   nodes row, then the detail panel filling the rest. The whole face is a flex
   column; the two content sections (nodes beside the thumb, panel below) grow
   to the card's border padding as beveled containers. */
.flip-memories-header {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    flex: 0 0 auto;
    padding: 0.3rem 0.45rem 0.35rem;
}
.flip-memories-name {
    font-size: 1.2em;   /* em off the master scale (below) so it tracks the card */
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--ui-text-strong);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* Thumb + nodes row. The nodes container is a beveled inset that fills the
   space beside the (square) thumb. */
.flip-memories-row {
    display: flex;
    gap: 0.4rem;
    flex: 0 0 auto;
    align-items: stretch;
    padding: 0 0.4rem;
    min-height: 0;
}
.flip-memories-thumb {
    /* Portrait (2/3) avatar matching the card back's flipping portrait — same
       vmax avatar scale the gallery card-back uses, so it isn't oversized. The
       thumb defines the row height; the nodes inset stretches to it. */
    flex: 0 0 auto;
    width: clamp(52px, 8.5vmax, 74px);
    align-self: flex-start;
}
.flip-memories-row .card-back-memory-nodes {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;   /* centre the (single) user node vertically beside the portrait */
    overflow-y: auto;
    /* Node bubbles sit FLAT on the card-face surface like the builder-tags — the
       recessed dark inset box (bg rgba(0,0,0,0.18) + inset shadows + radius) is
       removed so they read as tags ON the top surface, not sunk into a container. */
    padding: 0.2rem;
}
/* Delicate scrollbar (matches the memory panel) — was the OS default once the
   recessed box's own overflow lost its dark backing. */
.flip-memories-row .card-back-memory-nodes { scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--secondary) 25%, rgba(255, 255, 255, 0.1)) transparent; }
.flip-memories-row .card-back-memory-nodes::-webkit-scrollbar { width: 6px; }
.flip-memories-row .card-back-memory-nodes::-webkit-scrollbar-track { background: transparent; }
.flip-memories-row .card-back-memory-nodes::-webkit-scrollbar-thumb {
    background: color-mix(in srgb, var(--secondary) 25%, rgba(255, 255, 255, 0.1)); border-radius: 3px;
}
/* Add-a-memory input reads as a plain typing line (like the observation lines
   above it) — no field box at all. The panel is a .field, so fieldsets-base's
   `.field input[type="text"]` (0,2,1 — the attribute selector) was painting it
   as a bordered/tinted field; this 3-class scope + the attribute beats it. */
.flip-memories .cast-scene-memory-panel .cb-obs-add-input[type="text"] {
    width: 100%;
    box-sizing: border-box;
    height: auto;
    margin-top: 0.2em;
    font-family: inherit;
    font-size: 0.85em;
    line-height: 1.4;
    color: rgba(255, 255, 255, 0.85);
    background: transparent;
    border: none;
    border-radius: 0;
    box-shadow: none;
    /* Left pad in em (scales with the master) lines the placeholder up under
       the observation text — the obs list is padding-left 1.1em, ~1 char
       further in. The title tooltip is affordance enough — no underline. */
    padding: 0.1em 0.1em 0.1em 1.9em;
}
.flip-memories .cast-scene-memory-panel .cb-obs-add-input[type="text"]:hover,
.flip-memories .cast-scene-memory-panel .cb-obs-add-input[type="text"]:focus {
    outline: none;
    background: transparent;
    border: none;
    box-shadow: none;
}
.flip-memories .cast-scene-memory-panel .cb-obs-add-input::placeholder {
    color: rgba(255, 255, 255, 0.35);
    font-style: italic;
}
/* Detail panel below the row fills the remaining height. The injected
   .cast-scene-memory-panel (.field--bevel) is capped + content-sized, so it
   collapsed around a one-line description; stretch it to fill the card down to
   the border padding. */
.flip-memories > .card-back-memory-panel {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    margin: 0.4rem 0.4rem 0.1rem;
}
.flip-memories .card-back-memory-panel .cast-scene-memory-panel {
    flex: 1 1 auto;
    min-height: 0;
    max-height: none;   /* drop the 220px cap so the bevel fills the card */
    margin-top: 0;
    overflow-y: auto;
}

/* Memories button removed — the flip cycle reaches the memories face now. */

/* ── Memories face scale — it's essentially a gallery card back, so scale the
   pills + detail text the same way: a vmax master on the direct children,
   everything deeper in em off it (mirrors .card-back-info--character). Beats the
   cast fieldset's fixed-rem sizes via the extra class in the selector. ── */
.flip-memories > * {
    font-size: clamp(11px, 1.5vmax, 13px);
}
.flip-memories .cast-scene-memory-bubbles {
    flex-wrap: wrap;
    gap: 0.3em;
}
.flip-memories .cast-scene-memory-bubbles .memory-node-bubble {
    font-size: 0.85em;
    padding: 0.22em 0.5em;
    gap: 0.3em;
    max-width: 100%;
    min-width: 0;
    white-space: nowrap;
}
.flip-memories .cast-scene-memory-bubbles .memory-node-bubble .tag-name {
    overflow: hidden;
    text-overflow: ellipsis;
}
.flip-memories .memory-node-detail__name { font-size: 1em; }
.flip-memories .memory-node-detail__type { font-size: 0.7em; }
.flip-memories .memory-node-detail__obs { padding-left: 1.1em; gap: 0.3em; }
.flip-memories .memory-node-detail__obs li { font-size: 0.9em; gap: 0.45em; }
.flip-memories .memory-node-detail__none { font-size: 0.88em; }

/* Subtle RESTING tint per provenance (--tag-color: system=hair, user=neutral,
   fly=eyes — set by the cast-fieldset rules) so the pills are distinguishable
   without hover (mobile has none). Hover/active deepen it (higher specificity
   here so they still win over the resting tint). */
.flip-memories .cast-scene-memory-bubbles .memory-node-bubble {
    background: color-mix(in srgb, var(--tag-color) 15%, transparent);
    border-color: color-mix(in srgb, var(--tag-color) 32%, rgba(255, 255, 255, 0.1));
}
.flip-memories .cast-scene-memory-bubbles .memory-node-bubble:hover,
.flip-memories .cast-scene-memory-bubbles .memory-node-bubble:focus-visible {
    background: color-mix(in srgb, var(--tag-color) 32%, rgba(255, 255, 255, 0.06));
}
.flip-memories .cast-scene-memory-bubbles .memory-node-bubble.active {
    background: color-mix(in srgb, var(--tag-color) 45%, rgba(255, 255, 255, 0.08));
    border-color: color-mix(in srgb, var(--tag-color) 55%, transparent);
}

/* Swatches match the card-back scale (vmax, not the bigger base size). */
.flip-memories .color-swatches { gap: clamp(2px, 0.35vmax, 3px); flex-wrap: wrap; }
.flip-memories .color-swatch {
    flex: 0 0 auto;
    width: clamp(7px, 1.7vmax, 14px);
    height: auto;
    aspect-ratio: 1;
    border-width: 1px;
    border-radius: 18%;
}

/* Subscribe offer injected onto a quota-exhausted scene card's back: the
   auth-surface brings its own frame, so drop the card-back padding and let it
   fill edge-to-edge. */
.flip-container > .flip-back.flip-back--subscribe {
    padding: 0;
}
.flip-back--subscribe > .upgrade-surface {
    flex: 1 1 auto;
    border-radius: 0;
}


/* ═══════════════════════════════════════════════════════════════════════════
   UPGRADE FACE — front face content for scenes whose image generation was
   blocked by the daily quota. The model's snapshot still lives on the back
   face; the front swaps <img> for an intro-card-style tease that reads as
   native to the carousel instead of a heavy black overlay. Reuses
   .generate-card (grid backdrop + chrome) so it sits in the same visual
   family as the introduction / prompt cards. Inner layout: usage subject
   ("3/3"), the "Daily Limit Reached" headline, single Upgrade pill below.
   ═══════════════════════════════════════════════════════════════════════════ */

.card-front-upgrade {
    position: absolute;
    inset: 0;
}

.card-front-upgrade__inner {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 0.7rem;
    padding: 1.4rem 1.2rem;
    text-align: center;
}

.card-front-upgrade__subject {
    font-size: 0.78rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--primary);
    opacity: 0.85;
}

.card-front-upgrade__title {
    font-size: 1.05rem;
    font-weight: 600;
    letter-spacing: 0.01em;
    line-height: 1.3;
    color: inherit;
}

.card-front-upgrade__cta {
    margin-top: 0.5rem;
    padding: 0.55rem 1.4rem;
    border: 1px solid color-mix(in srgb, var(--primary) 60%, transparent);
    border-radius: 10px;
    background: color-mix(in srgb, var(--primary) 75%, transparent);
    color: inherit;
    font-weight: 600;
    text-decoration: none;
    transition: transform 80ms ease, background 120ms ease, border-color 120ms ease;
}

.card-front-upgrade__cta:hover {
    background: var(--primary);
    transform: translateY(-1px);
    color: inherit;
    text-decoration: none;
}

.card-front-upgrade__cta:active {
    transform: translateY(0);
}

/* ═══════════════════════════════════════════════════════════════════════════
   CREATOR CARD — the gallery's 'New character' door. Reuses .generate-card
   (grid backdrop + slot-tinted gradient) so it reads as native to the carousel;
   the whole card is a link to /new/. A giant + (the slot-card + writ large) over
   a small label. Colors come from the slot swatches via color-mix, like the rest
   of the generate-card family.
   ═══════════════════════════════════════════════════════════════════════════ */
.creator-card {
    text-decoration: none;
    color: inherit;
    cursor: pointer;
}

.creator-card__inner {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 0.6rem;
    padding: 1.2rem;
    text-align: center;
}

.creator-card__plus {
    font-size: 5.5rem;
    font-weight: 300;
    line-height: 0.8;
    color: color-mix(in srgb, var(--primary) 75%, white);
    text-shadow: 0 0 24px color-mix(in srgb, var(--primary) 45%, transparent);
    transition: transform 120ms ease, color 120ms ease, text-shadow 120ms ease;
}

.creator-card__label {
    font-size: 0.82rem;
    font-weight: 600;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: color-mix(in srgb, var(--primary) 70%, white);
    opacity: 0.85;
}

.creator-card:hover .creator-card__plus {
    transform: translateY(-2px) scale(1.04);
    color: color-mix(in srgb, var(--primary) 90%, white);
    text-shadow: 0 0 34px color-mix(in srgb, var(--primary) 60%, transparent);
}


/* ═══════════════════════════════════════════════════════════════════════════
   CARD-BACK INFO PANEL — gallery only. Renders on .flip-back when the
   strip is in character mode. Server-side per-card, themed by the
   card's own slot vars (already on the .strip-card wrapper). Reuses
   the .char-identity-row / .char-row-with-thumb / .card-tab-strip /
   .card-pane structure from the bottom fieldset so the typography
   matches the rest of the form chrome.
═══════════════════════════════════════════════════════════════════════════ */
.card-back-info {
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    padding: 0.6rem;
    box-sizing: border-box;
    container-type: inline-size;  /* swatches (and the scene thumb) size off the card width */
    overflow: hidden;
    color: inherit;
    font-size: 0.85rem;
    /* --accent feeds the shared select2-theme.css scope; setting it
       to slot-1 (the character's hair color) paints each card-back
       select2's left border + hover tint with that character's hair,
       so the chrome reads "this is X's panel" at a glance. Same
       pattern fieldsets-env / fieldsets-base use to slot-tint
       their own dropdowns. */
    --accent: var(--primary);
}

.card-back-info .char-identity-row {
    display: flex;
    justify-content: space-around;
    gap: 0.5rem;
    padding: 0.3rem 0;
}

.card-back-info .char-identity-row .identity-field {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.15rem;
    flex: 1;
    min-width: 0;
}

/* Chat read-only identity — inline with the CHARACTER h2 title (no extra line).
   Subtle next to the title: smaller, dimmer, normal-weight, not uppercased. */
.cast-h2-identity {
    font-size: 0.7rem;
    font-weight: 400;
    letter-spacing: 0.02em;
    text-transform: none;
    opacity: 0.6;
}

.card-back-info .char-identity-row label {
    font-size: 0.65rem;
    opacity: 0.6;
    text-transform: uppercase;
    letter-spacing: 0.06em;
}

.card-back-info .char-identity-row .identity-readonly {
    font-size: 0.85rem;
    font-weight: 600;
    /* Was var(--primary) — dark-haired characters vanished against the
       work-surface grey. --ui-text-strong is the same token .card-tab-
       btn.active uses for the same reason (readable on ANY slot). */
    color: var(--ui-text-strong);
}

/* Gallery-only overrides for the thumb area. Scoped to --character so
   scene-mode card-backs fall through to the base .char-row-with-thumb
   grid in fieldsets-layout.css (auto thumb + 1fr fields, two-row
   areas) — same scaling the bottom character fieldset uses. The
   gallery only puts two children in this row (.char-thumb + .card-
   back-join-area), so we don't fight the base's named template-areas
   — we just override the column widths to give the join panel more
   room than the thumb. */
/* The gallery card-back must FILL its face for the memory panel to hold a
   stable lower portion. height:100% wasn't resolving down the aspect-ratio flip
   chain (.flip-container has aspect-ratio:inherit, no explicit height), so
   card-back-info sat at content-height — panel collapsed + a bottom gap. Pin it
   to the back face: .flip-back is position:absolute, so inset:0 fills it cleanly
   with no percentage chain. Gallery-only; the scene card-back is untouched. */
.card-back-info--character {
    position: absolute;
    inset: 0;
    gap: 0.3rem;            /* tighten header/row/panel spacing (base is 0.5rem) */
    /* Content sizes off vmax — the device's LONG side — NOT the card. Card-relative
       units (cqw/cqh/cqmin) change when the card resizes, and the card resizes when
       you rotate the phone — so the content kept rescaling on flip, and a big
       landscape card got tiny content ("giant with a tiny head"). vmax is identical
       in portrait and landscape (rotating just swaps width↔height) and immune to the
       URL bar, so the content holds ONE size across the flip. clamp() bounds it on a
       big desktop screen. Tune the vmax coefficients / caps here + below. */
    --cb-avatar-w: clamp(52px, 8.5vmax, 74px);   /* avatar short side; bumped a bit; cap ≈ mobile size so desktop matches */
}
/* ── MASTER SCALE — one knob for the whole card-back ─────────────────────────
   font on the CHILDREN (not the container); everything deeper is em off this, so
   header/name, tags and descriptions scale as one unit. vmax-based (see the note
   above) so it's steady across the portrait↔landscape flip. Tune the 1.5 for overall
   text size, the 7.5 above for the avatar; the caps ≈ the mobile size so desktop
   MATCHES mobile (raise a cap to let desktop grow, lower it to shrink it). */
.card-back-info--character > *,
.card-back-info--scene > * {
    font-size: clamp(11px, 1.5vmax, 13px);
}
.card-back-info--character .card-back-header .card-back-character-name {
    font-size: 1.2em;   /* em off the master so the name tracks the card too
                           (was a fixed 0.9rem that never scaled) */
}

.card-back-info--character .char-row-with-thumb {
    /* Avatar column AUTO-sizes to the thumb (which is the portrait thumb rotated
       per aspect — see below), so a wide landscape thumb gets the room it needs;
       nodes take the rest. Row height = the portrait thumb height; a landscape thumb
       is shorter and sits at the top. */
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 0.6em;
    align-items: start;
    flex: 0 0 auto;
    height: calc(var(--cb-avatar-w) * 1.5);
}
/* Square cards: the avatar is var(--cb-avatar-w) tall (square), so size the row to
   THAT, not the portrait 1.5× — otherwise ~var×0.5 of dead space sits below the
   square avatar before the details. (Landscape uses the 2-col grid below.) */
.strip-card[data-aspect-ratio="square"] .card-back-info--character .char-row-with-thumb {
    height: var(--cb-avatar-w);
}

.card-back-info--character .char-thumb.card-back-thumb {
    /* The thumb is the SAME little card in every aspect — a "portrait on its side":
       its SHORTER side is always --cb-avatar-w, the longer side follows the aspect.
       Portrait/square constrain the WIDTH (tall/square thumb); landscape constrains
       the HEIGHT (wide thumb) — see the override below. var(--card-aspect) + the img's
       object-fit: cover then fill the box without a portrait crop. ~slot-card sized. */
    aspect-ratio: var(--card-aspect);
    width: var(--cb-avatar-w);
    height: auto;
    align-self: start;
    border-radius: 8px;
    overflow: hidden;
    background: color-mix(in srgb, var(--secondary) 20%, transparent);
}
/* Landscape: the SHORT side is the HEIGHT — the portrait thumb laid on its side.
   height = the portrait's width (--cb-avatar-w); width grows to the aspect. */
.strip-card[data-aspect-ratio="landscape"] .card-back-info--character .char-thumb.card-back-thumb {
    width: auto;
    height: var(--cb-avatar-w);
}

/* ── Landscape cards: 2-column card-back ─────────────────────────────────────
   The wide landscape thumb leaves a big area, so use it: identity (the wide avatar
   OVER its nodes) is a narrow left column; the memory details fill a tall justified
   column on the right. Keyed on the card ASPECT, so it's the same in mobile-portrait
   and mobile-landscape. char-row stays a real box (just becomes a flex column) and
   the panel stays a real grid child — NO display:contents (that collapsed it before),
   and the avatar's align-self:start keeps it from stretching in the column. */
.strip-card[data-aspect-ratio="landscape"] .card-back-info--character {
    display: grid;
    /* Left column = the AVATAR's width (short side --cb-avatar-w × the 3/2 landscape
       aspect), NOT auto — so the avatar governs the column, not a long node name.
       Nodes wrap / ellipsis to fit it. Track it via --cb-avatar-w so it scales with
       the avatar dial. 10px to the details (your padding). */
    grid-template-columns: calc(var(--cb-avatar-w) * 1.5) 1fr;
    grid-template-rows: auto 1fr;       /* swatch/name across the top, then the body */
    column-gap: 10px;
}
.strip-card[data-aspect-ratio="landscape"] .card-back-info--character .card-back-header {
    grid-column: 1 / -1;   /* swatch/name across the top */
}
.strip-card[data-aspect-ratio="landscape"] .card-back-info--character .char-row-with-thumb {
    grid-column: 1;
    grid-row: 2;
    display: flex;
    flex-direction: column;   /* avatar OVER nodes (not beside) */
    gap: 0.4em;
    height: auto;             /* drop the fixed avatar-row height; the grid row sizes it */
    min-height: 0;
}
.strip-card[data-aspect-ratio="landscape"] .card-back-info--character .card-back-memory-nodes {
    flex: 1;          /* fill below the avatar (scrolls — overflow-y:auto is set in the base) */
    min-height: 0;
}
.strip-card[data-aspect-ratio="landscape"] .card-back-info--character .card-back-memory-panel {
    grid-column: 2;
    grid-row: 2;
    text-align: justify;   /* the newspaper column */
}

.card-back-info--character .char-thumb.card-back-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

/* ── Gallery card-back memory (read-only) ───────────────────────────────
   Reuses the global .memory-node-bubble / .memory-node-detail chrome from
   cast-fieldset.css. This only lays the loaded fragment out — nodes in the
   avatar row's 2nd column, the detail panel below the row — and drives the
   read-only reveal (the cast fieldset's reveal is scoped to .fieldset--cast-
   scene, so it doesn't reach here). Both containers start empty; the card-back
   memory loader fills them on flip. */
.card-back-info--character .card-back-memory-nodes {
    min-width: 0;
    min-height: 0;
    align-self: stretch;
    overflow-y: auto;      /* node bubbles scroll within the fixed upper row */
}
.card-back-info--character .card-back-memory-nodes .cast-scene-memory-bubbles {
    flex-wrap: wrap;
    gap: 0.3em;
}
/* Node tags in em off the master: 1em IS the master size, padding/gap relative,
   so tags and their text scale with the whole card. nowrap keeps a long name one
   line; the tag-name ellipsises if it ever exceeds the column. */
.card-back-info--character .memory-node-bubble {
    font-size: 0.85em;      /* smaller than the master so ~3 rows of tags fit
                               clean beside the avatar without cutoff. Tune. */
    padding: 0.22em 0.5em;
    gap: 0.3em;
    max-width: 100%;
    min-width: 0;
    white-space: nowrap;
}
.card-back-info--character .memory-node-bubble .tag-name {
    overflow: hidden;
    text-overflow: ellipsis;
}
.card-back-info--character .card-back-memory-panel {
    flex: 1;               /* fill the lower portion of the card-back */
    min-height: 0;
    display: flex;         /* host the injected bevel field as a fill-the-area child */
    flex-direction: column;
    /* No extra top margin: the ~10px gap to the avatar row comes from
       card-back-info's own flex gap, matching the side insets. */
}
/* The loader appends .cast-scene-memory-panel (a .field--bevel) INTO the panel.
   Two global rules (cast-fieldset.css) fight the fill: it sizes to content (a
   small node collapsed it to ~2 lines) AND it's capped at max-height:220px (the
   ~40px bottom gap on a tall card) with its own margin-top. Override all three:
   flex-fill the panel, drop the cap, zero the margin; observations scroll. */
.card-back-info--character .card-back-memory-panel .cast-scene-memory-panel {
    flex: 1;
    min-height: 0;
    max-height: none;
    margin: 0;
    padding: 0.3em 0.85em 0.7em;   /* trimmed TOP (closes the gap above the details); em scales with master */
    overflow-y: auto;
}
/* one detail visible at a time (the loader / first-node default sets .active) */
.card-back-info--character .card-back-memory-panel .memory-node-detail { display: none; }
.card-back-info--character .card-back-memory-panel .memory-node-detail.active { display: block; }

/* ── Owner card-back memory EDITOR (Phase A: editable Conversation fields) ──
   The card back is the character's memory editor for its owner. A Conversation
   group of card-field bubbles sits above the read-only memory nodes; clicking a
   bubble reveals its blur-save textarea in the shared panel. em-scaled like the
   rest of the card. */
.card-back-info--character .card-back-memory-nodes .node-group {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: 0.3em;
    width: 100%;
    padding: 0.3em 0.4em 0.4em;
    margin-bottom: 0.3em;
    border-radius: 0.6em;
    background: rgba(255, 255, 255, 0.03);
    border: 1px solid rgba(255, 255, 255, 0.08);
}
.card-back-info--character .card-back-memory-nodes .node-group-label {
    flex-basis: 100%;
    text-align: center;
    font-size: 0.6em;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.42);
}
/* Card-field bubbles reuse the memory-node bubble look (hair tint, clickable). */
.card-back-info--character .card-back-memory-nodes .detail-bubble {
    --tag-color: var(--primary);
    font-size: 0.85em;
    padding: 0.22em 0.5em;
    cursor: pointer;
    transition: background 0.15s ease, border-color 0.15s ease;
}
.card-back-info--character .card-back-memory-nodes .detail-bubble:hover,
.card-back-info--character .card-back-memory-nodes .detail-bubble:focus-visible {
    background: color-mix(in srgb, var(--tag-color) 32%, rgba(255, 255, 255, 0.06));
    outline: none;
}
.card-back-info--character .card-back-memory-nodes .detail-bubble.active {
    background: color-mix(in srgb, var(--tag-color) 42%, rgba(255, 255, 255, 0.08));
    border-color: color-mix(in srgb, var(--tag-color) 55%, transparent);
}
/* Memory pip on the card back — flips the field's *_enabled flag. Ported from
   the cast fieldset's .tag-switch; slot-1 tint (the bubble's --tag-color). */
:is(.card-back-info--character, .flip-memories) .card-back-memory-nodes .detail-toggle {
    position: relative;
    width: 1.7em;
    height: 1em;
    margin-left: 0.35em;
    border-radius: 0.5em;
    background: color-mix(in srgb, var(--tag-color) 20%, rgba(255, 255, 255, 0.1));
    border: 1px solid color-mix(in srgb, var(--tag-color) 35%, rgba(255, 255, 255, 0.18));
    cursor: pointer;
    flex-shrink: 0;
    transition: background 0.2s, border-color 0.2s;
}
:is(.card-back-info--character, .flip-memories) .card-back-memory-nodes .detail-toggle .tag-knob {
    position: absolute;
    top: 0.12em;
    left: 0.12em;
    width: 0.6em;
    height: 0.6em;
    border-radius: 50%;
    background: color-mix(in srgb, var(--tag-color) 55%, rgba(255, 255, 255, 0.4));
    transition: left 0.2s, background 0.2s;
}
:is(.card-back-info--character, .flip-memories) .card-back-memory-nodes .detail-toggle.on {
    background: color-mix(in srgb, var(--tag-color) 35%, rgba(255, 255, 255, 0.1));
    border-color: color-mix(in srgb, var(--tag-color) 55%, rgba(255, 255, 255, 0.25));
}
:is(.card-back-info--character, .flip-memories) .card-back-memory-nodes .detail-toggle.on .tag-knob {
    left: 0.98em;
    background: var(--tag-color, rgba(255, 255, 255, 0.7));
}
/* Card-field details share the panel's one-at-a-time show/hide. Active fills the
   panel so the textarea has the whole area and scrolls internally. */
.card-back-info--character .card-back-memory-panel .cb-cardfield-detail { display: none; }
/* Active card-field fills the (definite-height) panel so its textarea runs to
   the bottom of the card, not just a couple of lines. */
.card-back-info--character .card-back-memory-panel .cb-cardfield-detail.active {
    display: flex;
    flex-direction: column;
    height: 100%;
    min-height: 0;
}
.card-back-info--character .cb-cardfield-input {
    flex: 1;
    min-height: 3em;
    width: 100%;
    box-sizing: border-box;
    margin-top: 0.4em;
    resize: none;
    /* Match the read-only detail text (.memory-node-detail__obs li) — plain
       font-family:inherit + 0.9em so the em resolves against the card master,
       not a textarea UA default that renders fixed-large on small cards. */
    font-family: inherit;
    font-size: 0.9em;
    line-height: 1.4;
    color: rgba(255, 255, 255, 0.85);
    background: rgba(0, 0, 0, 0.28);
    border: 1px solid rgba(255, 255, 255, 0.12);
    border-radius: 0.3em;
    padding: 0.4em 0.5em;
    overflow-y: auto;
}
.card-back-info--character .cb-cardfield-input:focus {
    outline: none;
    border-color: color-mix(in srgb, var(--primary) 40%, rgba(255, 255, 255, 0.2));
}
.card-back-info--character .cb-cardfield-input::placeholder {
    color: rgba(255, 255, 255, 0.35);
    font-style: italic;
}
/* Editor Memories flip-face: the editable card-field editor now lives here too,
   but the rules above are scoped to .card-back-info--character (the gallery card
   back). Mirror the card-field detail toggle + input fill for the flip face so
   its sub-sections show ONE at a time (the active one filling the panel) instead
   of every group stacking open with its own scrollbar. */
.flip-memories .card-back-memory-panel .cb-cardfield-detail { display: none; }
.flip-memories .card-back-memory-panel .cb-cardfield-detail.active {
    display: flex;
    flex-direction: column;
    height: 100%;
    min-height: 0;
}
.flip-memories .cb-cardfield-input {
    flex: 1;
    min-height: 3em;
    width: 100%;
    box-sizing: border-box;
}
/* ...and the node-group sub-container + its label (the Memory / Conversation
   groups with the subtle background), likewise gallery-scoped above — so each
   section reads as its own grouped container with the label on its own line,
   not the label + all its nodes on one wrapped line. */
.flip-memories .card-back-memory-nodes .node-group {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: 0.3em;
    width: 100%;
    padding: 0.3em 0.4em 0.4em;
    margin-bottom: 0.3em;
    border-radius: 0.6em;
    background: rgba(255, 255, 255, 0.03);
    border: 1px solid rgba(255, 255, 255, 0.08);
}
.flip-memories .card-back-memory-nodes .node-group-label {
    flex-basis: 100%;
    text-align: center;
    font-size: 0.6em;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.42);
}
/* ...and the card-field bubbles' system tint (slot-1) so Description / Persona /
   Identity / the Conversation fields read as SYSTEM nodes, distinct from the
   user (neutral) and on-the-fly (slot-2) memory nodes — whose kind colors come
   from the global .memory-node--{system,user,fly} rules and already reach here.
   Only this detail-bubble tint was gallery-scoped. */
.flip-memories .card-back-memory-nodes .detail-bubble {
    --tag-color: var(--primary);
    font-size: 0.85em;
    padding: 0.22em 0.5em;
    cursor: pointer;
    transition: background 0.15s ease, border-color 0.15s ease;
}
.flip-memories .card-back-memory-nodes .detail-bubble:hover,
.flip-memories .card-back-memory-nodes .detail-bubble:focus-visible {
    background: color-mix(in srgb, var(--tag-color) 32%, rgba(255, 255, 255, 0.06));
    outline: none;
}
.flip-memories .card-back-memory-nodes .detail-bubble.active {
    background: color-mix(in srgb, var(--tag-color) 42%, rgba(255, 255, 255, 0.08));
    border-color: color-mix(in srgb, var(--tag-color) 55%, transparent);
}
/* Card-back Identity pills (C2) — ported from the cast fieldset's
   .detail-field[data-field="identity"], em-scaled to the card. race/gender/age
   as collapsing select2 pills; the boxed select2 chrome is stripped so each pill
   shows just its value over the slot-1 bubble fill. Identity is 3 short pills, so
   its detail doesn't fill the panel like the textareas do. */
:is(.card-back-info--character, .flip-memories) .cb-identity-detail.active { height: auto; }
:is(.card-back-info--character, .flip-memories) .cb-identity-detail .identity-pills-row {
    display: flex;
    flex-wrap: wrap;
    /* Top-align: age was reading a few px low when centred (same as the cast
       fieldset), and the pills are a uniform height anyway. */
    align-items: flex-start;
    justify-content: center;
    gap: 0.4em 0.6em;
    padding-top: 0.3em;
}
:is(.card-back-info--character, .flip-memories) .cb-identity-detail .identity-field {
    display: flex;
    align-items: center;
    flex: 0 1 auto;
    min-width: 0;
}
:is(.card-back-info--character, .flip-memories) .cb-identity-detail .identity-pill {
    gap: 0.4em;
    max-width: 100%;
    min-width: 3.5em;
    padding-top: 0.1em;
    padding-bottom: 0.1em;
    align-items: center;
    background: color-mix(in srgb, var(--primary) 25%, transparent);
    border-color: color-mix(in srgb, var(--primary) 50%, rgba(255, 255, 255, 0.2));
}
:is(.card-back-info--character, .flip-memories) .cb-identity-detail .identity-pill-label {
    font-size: 0.6em;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    opacity: 0.55;
    flex: 0 0 auto;
    white-space: nowrap;
    cursor: pointer;
    line-height: 1;
    align-self: center;
}
:is(.card-back-info--character, .flip-memories) .cb-identity-detail .identity-pill .select2-container { flex: 0 1 auto; min-width: 0; }
:is(.card-back-info--character, .flip-memories) .cb-identity-detail .identity-pill .select2-container--default .select2-selection--single {
    background: transparent;
    border: none;
    border-radius: 0;
    box-shadow: none;
    /* Taller than the cast fieldset's 1.5rem: the select2 search overlay has a
       fixed height that won't shrink, so give the pill room to clear it. */
    height: 1.9em;
    min-height: 0;
    padding: 0;
    display: flex;
    align-items: center;
}
:is(.card-back-info--character, .flip-memories) .cb-identity-detail .identity-pill .select2-container--default .select2-selection--single:hover {
    background: transparent;
    border: none;
}
:is(.card-back-info--character, .flip-memories) .cb-identity-detail .identity-pill .select2-container--default .select2-selection__rendered {
    padding: 0;
    line-height: 1;
    font-size: 0.85em;
    overflow: hidden;
    text-overflow: ellipsis;
}
:is(.card-back-info--character, .flip-memories) .cb-identity-detail .identity-pill .select2-selection__arrow { display: none; }
/* Hand-install-a-memory input (C4b) — a subtle "type in a line" field under a
   node's observations: no box, just a faint underline (no slot-colour accent,
   which read as a red border on a red-haired character). */
/* Add-a-memory input reads as a plain typing line (no field box) — same as the
   Memories face. The panel is a .field, so fieldsets-base's `.field
   input[type=text]` (0,2,1 + its hover/focus accent) was painting it as a
   bordered/tinted field; the 3-class + attribute scope here beats it. Indented
   to line up under the observation text. */
.card-back-info--character .cast-scene-memory-panel .cb-obs-add-input[type="text"] {
    width: 100%;
    box-sizing: border-box;
    height: auto;
    margin-top: 0.2em;
    font-family: inherit;
    font-size: 0.85em;
    line-height: 1.4;
    color: rgba(255, 255, 255, 0.85);
    background: transparent;
    border: none;
    border-radius: 0;
    box-shadow: none;
    padding: 0.1em 0.1em 0.1em 1.9em;
}
.card-back-info--character .cast-scene-memory-panel .cb-obs-add-input[type="text"]:hover,
.card-back-info--character .cast-scene-memory-panel .cb-obs-add-input[type="text"]:focus {
    outline: none;
    background: transparent;
    border: none;
    box-shadow: none;
}
.card-back-info--character .cast-scene-memory-panel .cb-obs-add-input::placeholder {
    color: rgba(255, 255, 255, 0.35);
    font-style: italic;
}
/* Description / node-detail text in em off the master (2-class scope beats
   cast-fieldset's 1-class fixed-rem rules) so the descriptions scale in lockstep
   with the tags and the rest of the card. */
.card-back-info--character .memory-node-detail__head { gap: 0.5em; margin-bottom: 0.4em; }
.card-back-info--character .memory-node-detail__name { font-size: 1em; }
.card-back-info--character .memory-node-detail__type { font-size: 0.7em; }
.card-back-info--character .memory-node-detail__obs { padding-left: 1.1em; gap: 0.3em; }
.card-back-info--character .memory-node-detail__obs li { font-size: 0.9em; gap: 0.45em; }
.card-back-info--character .memory-node-detail__none { font-size: 0.88em; }

/* Scene/chat (+ editor clone) card-back: scale the thumb with the CARD, not the
   field content. The base .char-thumb is height:100% — sized by the mood/
   activity/description content height — so it stayed FIXED as the card shrank
   and ate the fields. Make the card-back a query container and size the thumb
   as a fraction of ITS width; --thumb-aspect (set per scene aspect) derives the
   height. Now the thumb tracks the card. (Gallery --character keeps its own
   1fr path above; the base grid / bottom fieldset are untouched.) Tune the
   38cqw to trade thumb size against field room. */
.card-back-info--scene {
    container-type: inline-size;
    /* Scene thumb short side on vmax (device long side) — flip-stable, like the
       gallery avatar; replaces 38cqw, which rescaled the thumb on every rotation
       (the same flip-shrink we fought on the gallery). SAME dial as the gallery
       avatar — this card-back fits as much content (or more), so match its size. */
    --cb-avatar-w: clamp(52px, 8.5vmax, 74px);
    /* One gap dial for the scene card-back: ~5px on small/mobile cards (where we need
       the room to fit description) up to the page's 10px target on desktop. vmax so it's
       flip-stable. Overrides the base .card-back-info gap + padding (both single-class,
       so --scene wins on source order). */
    --cb-gap: clamp(5px, 0.6vmax, 10px);
    gap: var(--cb-gap);
    padding: var(--cb-gap);
}
.card-back-info--scene .char-thumb {
    width: var(--cb-avatar-w);
    height: auto;
    align-self: start;
}
/* Landscape: constrain the HEIGHT to the dial (the short side), like the gallery —
   the width grows to the aspect (a wide thumb). Without this the width stays the
   dial and the aspect-ratio collapses the height to var×0.67, so landscape rendered
   smaller than portrait/square. Now all three share the same --cb-avatar-w short side. */
.strip-card[data-aspect-ratio="landscape"] .card-back-info--scene .char-thumb {
    width: auto;
    height: var(--cb-avatar-w);
}

.card-back-info .card-back-join-area {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 0.5rem;
    text-align: center;
}

/* .join-question removed 2026-05-27 — the wrapping description
   above the button blew up the fixed-aspect card on mobile. Button
   label IS the affordance now (Chat / Room Full). */

.card-back-info .join-btn {
    appearance: none;
    border: 1px solid rgba(255, 255, 255, 0.2);
    background: color-mix(in srgb, var(--primary) 75%, transparent);
    color: inherit;
    border-radius: 8px;
    padding: 0.6rem 1.2rem;
    font: inherit;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    cursor: pointer;
    width: 100%;
    transition: transform 80ms ease, background 120ms ease,
                box-shadow 120ms ease;
}

.card-back-info .join-btn:hover {
    background: var(--primary);
    box-shadow: 0 3px 12px color-mix(in srgb, var(--primary) 30%, transparent);
}

.card-back-info .join-btn:active {
    transform: translateY(1px);
}

/* Roster-full: keep the Join button in its slot so the layout
   doesn't shift, just grey it out with the standard busy-lockout
   recipe (opacity:0.35 + pointer-events:none + cursor) used
   elsewhere on the page. The .join-question--full label sits
   where "Add X to your roster?" did. */
.card-back-info .join-question--full {
    font-size: 0.8rem;
    line-height: 1.3;
    color: rgba(220, 80, 80, 0.9);
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    font-variant-numeric: tabular-nums;
}
.card-back-info .join-btn--disabled,
.card-back-info .join-btn[disabled] {
    opacity: 0.35;
    pointer-events: none;
    cursor: not-allowed;
}

/* Gallery roster — 3 mini-thumbs below the Chat / Room Full
   button, one per clone in the user's cast. Reuses the cast-slot
   avatar bevel recipe at a smaller scale so the gallery shows
   the same character art the scene-page cast row uses. Width-
   capped so the thumbs don't fill the full join-area column —
   "a bit smaller" per the user's feedback. */
.card-back-info .gallery-roster {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 0.3rem;
    max-width: 70%;
    margin: 0.2rem auto 0;
}
.card-back-info .gallery-roster-thumb {
    position: relative;
    aspect-ratio: 1 / 1;
    border-radius: 5px;
    overflow: hidden;
    cursor: pointer;
    background: linear-gradient(
        135deg,
        color-mix(in srgb, var(--primary) 18%, #22222a) 0%,
        #22222a 50%
    );
    border: 2px solid color-mix(in srgb, var(--primary) 40%, rgba(255, 255, 255, 0.15));
    box-shadow:
        0 2px 6px rgba(0, 0, 0, 0.3),
        0 0 6px color-mix(in srgb, var(--primary) 18%, transparent),
        inset 0 2px 4px rgba(0, 0, 0, 0.3),
        inset 0 -1px 0 rgba(255, 255, 255, 0.1),
        inset 0 1px 0 rgba(255, 255, 255, 0.06);
    transition: box-shadow 0.15s ease, border-color 0.15s ease;
}
.card-back-info .gallery-roster-thumb:hover {
    border-color: color-mix(in srgb, var(--primary) 65%, rgba(255, 255, 255, 0.25));
    box-shadow:
        0 2px 10px rgba(0, 0, 0, 0.4),
        0 0 14px color-mix(in srgb, var(--primary) 35%, transparent),
        inset 0 2px 4px rgba(0, 0, 0, 0.3),
        inset 0 -1px 0 rgba(255, 255, 255, 0.1),
        inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.card-back-info .gallery-roster-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
.card-back-info .gallery-roster-initial {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.2rem;
    font-weight: 700;
    color: rgba(255, 255, 255, 0.7);
}

/* ── Gallery roster strip ───────────────────────────────────────────────
   The gallery's character/conversation manager, docked inside the toolbar's
   .control-panel so it inherits the bar's position: a horizontal strip in the
   portrait bottom bar, a vertical strip in the landscape side rail. Borrows
   the .scrolling-strip scroll feel (flex + overflow + snap) but is its OWN
   class — no carousel JS, no visibility:hidden reveal gate. Thumbnail-height;
   eats a little preview height (tune later). */
.roster-strip {
    display: flex;
    gap: 10px;   /* fixed — was var(--layout-gap), which is now 0 (would collapse the slots together) */
    align-items: center;
    justify-content: safe center;   /* centered when it fits; `safe` falls back to
                                       start-align once it overflows so the end
                                       cards aren't clipped (plain center clips). */
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x proximity;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    /* padding = the selected slot's overhang headroom (it grows both ways from centre),
       so the cards + toolbar footprint don't move when a slot is selected. */
    padding: 12px 0.2rem 7px;
    margin: 0;                 /* margin-bottom: 12px removed — legacy from when the roster
                                  floated above the toolbar and had to "grow the preview to
                                  meet the footer". In the grid the title row owns that
                                  space, so the 12px just read as an unneeded line below the
                                  slots. */
    position: relative;
    z-index: 5;
    background: none;          /* no chrome of its own — sits on the preview surface */
}
.roster-strip::-webkit-scrollbar { display: none; }
/* EMPTY roster → remove the row. Its padding + margin (12 + 7 + 12 = 31px) rendered even
   with no slots, so a gap sat between the card and the title on every logged-out surface.
   The grid's `auto` track then collapses to nothing on its own. */
body:has(.roster-strip):not(:has(.roster-strip-item)) .roster-strip { display: none; }

.roster-strip-item {
    position: relative;
    flex: 0 0 auto;
    scroll-snap-align: start;
}
/* The selected slot sits a few px bigger than the rest — in place, no overhang.
   z-index keeps the slightly-larger card cleanly above its neighbours. */
.roster-strip-item:has(.roster-strip-thumb--selected) {
    z-index: 2;
}
.roster-strip-thumb {
    display: block;
    height: var(--roster-slot-h, 13svh);   /* base slot size; --roster-slot-h is the
                             landscape/desktop override. Fraction of the card, so ~same
                             on phone + desktop (was fixed 66px → too small on desktop). */
    width: auto;
    aspect-ratio: 2 / 3;  /* little portrait, like the cards */
    border-radius: 5px;
    overflow: hidden;
    cursor: pointer;
    background: linear-gradient(135deg,
        color-mix(in srgb, var(--primary) 18%, #22222a) 0%, #22222a 50%);
    border: 2px solid color-mix(in srgb, var(--primary) 40%, rgba(255, 255, 255, 0.15));
    transition: border-color 0.15s ease, height 0.15s ease, margin 0.15s ease;
}
/* The centered card is a few px bigger, grown EVENLY top + bottom: the extra
   height splits into equal negative top + bottom margins so it overhangs the row
   symmetrically (margin box stays base → row height + toolbar don't move), and the
   strip's top/bottom headroom shows both sides. Sharp: real height, no transform. */
.roster-strip-item:has(.roster-strip-thumb--selected) .roster-strip-thumb {
    height: calc(var(--roster-slot-h, 13svh) + 12px);
    margin-top: -6px;
    margin-bottom: -6px;
}
/* × rides up with the 6px-taller top. */
.roster-strip-item:has(.roster-strip-thumb--selected) .roster-strip-x {
    top: -11px;
}
/* No glow / colour change on the selected slot — the main carousel's centered
   card has none either, so the SIZE (the :has zoom below) is the only cue. The
   selected slot keeps its normal hair border, just bigger. */
.roster-strip-thumb:hover {
    border-color: color-mix(in srgb, var(--primary) 65%, rgba(255, 255, 255, 0.25));
}
.roster-strip-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Empty slot — a dashed frame with a + at the same portrait scale; click sets it
   to the centered character. It's a <button>, so reset and re-center the glyph. */
.roster-strip-thumb--empty {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border-style: dashed;
    /* Neutral grey at rest — an empty slot has no character, so no per-character
       accent here; the accent reveals on hover (the character you'd load in). */
    border-color: rgba(255, 255, 255, 0.18);
    background: rgba(255, 255, 255, 0.04);
    color: rgba(255, 255, 255, 0.35);
    font-size: 1.6rem;
    line-height: 1;
}
.roster-strip-thumb--empty:hover {
    /* Hover paints the dashed frame + load glyph with the centered character's
       EYES (--slot-2, the site accent — matches the card you'd be loading in). */
    border-color: color-mix(in srgb, var(--secondary) 60%, rgba(255, 255, 255, 0.25));
    color: color-mix(in srgb, var(--secondary) 75%, rgba(255, 255, 255, 0.6));
}
/* Empty slot shows its slot NUMBER at rest; the load glyph (download-into-tray)
   replaces it on hover. Both are flex-centred in the button, so toggling
   display swaps them in place; both inherit the slot's currentColor (grey at
   rest → hair on hover). */
.roster-strip-slot-num {
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}
.roster-strip-thumb--empty .roster-strip-load-icon {
    display: none;
    width: 46%;
    height: auto;
}
.roster-strip-thumb--empty:hover .roster-strip-slot-num { display: none; }
.roster-strip-thumb--empty:hover .roster-strip-load-icon { display: block; }
/* Populated slot: its number, watermarked over the image on hover — neutral
   grey (all cards reveal on hover, so a per-card hair tint isn't needed).
   Light + fades in. */
.roster-strip-thumb { position: relative; }   /* anchor the overlay */
.roster-strip-num-overlay {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.6rem;   /* match the empty-slot number */
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    color: rgba(255, 255, 255, 0.9);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.15s ease;
    text-shadow: 0 1px 5px rgba(0, 0, 0, 0.7);
}
.roster-strip-thumb:hover .roster-strip-num-overlay { opacity: 0.85; }
/* (The login stage's dimmed 'pending' fake-slot preview is retired — the login
   card goes full height via the empty-strip reclaim instead.) */
.roster-strip-initial {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.1rem;
    color: rgba(255, 255, 255, 0.7);
}
/* A glyph in a roster slot in place of an avatar — the New '+' (pending preview) or
   an Edit icon (a placed-but-undefined New draft). Centered, tinted like the slot. */
.roster-strip-glyph {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.6rem;
    line-height: 1;
    /* Glyph text = HAIR (--slot-glyph, fed by the dict) on the slot's eyes
       accent — mirrors the creator card's hair '+' on its eyes-tinted face.
       Falls back to the slot accent when no glyph colour is supplied. */
    color: color-mix(in srgb, var(--slot-glyph, var(--primary)) 60%, white);
}
.roster-strip-glyph svg { width: 46%; height: 46%; }
/* '+ New character' pending preview — a big + over a small wrapping caption. */
.roster-strip-glyph--new {
    flex-direction: column;
    gap: 0.15em;
    padding: 0 0.2em;
    text-align: center;
}
/* Unsaved draft that already has a generated image: the '+ New character' card
   overlays the image with a dark scrim so the label stays legible — it still
   reads as the in-progress new character, not a saved slot. */
.roster-strip-glyph--over { background: rgba(0, 0, 0, 0.5); }
.roster-strip-glyph-plus { font-size: 1.7rem; font-weight: 300; }
.roster-strip-glyph-cap {
    font-size: 0.5rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    line-height: 1.05;
    opacity: 0.85;
}
.roster-strip-x {
    position: absolute;
    /* Overhangs the card corner. The strip keeps top padding so overflow-y:hidden
       doesn't clip it, and a matching negative margin-top (below) cancels that
       padding from the gap so the top still reads as the 10px rhythm. */
    top: -5px;
    right: -5px;
    width: 16px;
    height: 16px;
    line-height: 13px;
    text-align: center;
    padding: 0;
    border: none;
    border-radius: 50%;
    cursor: pointer;
    background: rgba(0, 0, 0, 0.72);
    color: #fff;
    font-size: 13px;
}
.roster-strip-x:hover { background: var(--danger); }   /* semantic remove-red, not a slot color */
.roster-strip-empty {
    font-size: 0.7rem;
    color: rgba(255, 255, 255, 0.4);
    padding: 0 0.4rem;
    white-space: nowrap;
    align-self: center;
}
/* Editor slots now own their OWN strip (the character carousel), separate from
   the conversation strip (the gallery) — so they no longer need a detach gap to
   set them apart from chat slots. The --editor modifier survives only as the JS
   hook (the P1 thumb handler skips it); it lays out like any base slot. */

/* Landscape: the roster now lives IN-FLOW below the card inside the preview fieldset
   (like portrait), NOT fixed to the right rail. The gallery strip reserves room below the
   card (see its height rule) so the slots stack under it — no overlap. */
/* min-width guard: without it this block OVERLAPPED the portrait `max-width:700px`
   rules — a 650x500 window matched BOTH, so the rail layout and the portrait
   fixed-toolbar layout applied at once and collided (the footer landing in the
   header). The three regimes are now a clean partition: <=700 portrait,
   >=701 + short + landscape rails, >=701 + tall desktop. */
@media (orientation: landscape) and (max-height: 600px) and (min-width: 701px) {
    .roster-strip {
        position: relative;      /* was fixed to the right rail */
        flex-direction: row;     /* horizontal row below the card, not a vertical column */
        justify-content: safe center;
        overflow-x: auto;
        overflow-y: hidden;
        scroll-snap-type: x proximity;
        margin-bottom: 0;   /* drop the portrait pip-gap; landscape fieldset is fixed-height,
                               so this bottom space just goes to the card instead */
    }
    /* Match the mobile-portrait slot size: portrait's 13svh keys off the LONG viewport
       dimension, which in landscape is vmax — so 13vmax is the same physical size (~2x the
       short-axis svh the row would otherwise use). */
    :root { --roster-slot-h: calc(13vmax * 2 / 3); }
    .roster-strip-item--add { order: -1; }
}

/* Card-back header — name dropdown + color swatches sit on the same
   row, mirroring the bottom fieldset's h2 + char-name-row that we
   just dismantled. Dropdown gets the bulk of the space; swatches
   tuck to the right. */
.card-back-info .card-back-header {
    display: flex;
    flex-direction: row;          /* col 1: identity (name+swatches), col 2: buttons top-right */
    align-items: flex-start;
    gap: 0.4rem;
    padding: 0.1rem 0;
    background: none;
    border-bottom: none;
    border-radius: 0;
}
.card-back-info .card-back-identity {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;       /* name on line 1, swatches own line 2 */
    align-items: stretch;
    gap: 0.25rem;
}
/* Buttons in their own column — a horizontal row pinned to the top-right (align-items:
   flex-start on the header) spanning the name+swatch rows, so they don't push the name
   down. Reuse chat's .chat-deck-btn skin, so no new button styles here. */
.card-back-info .card-back-header-actions {
    flex: 0 0 auto;
    display: flex;
    flex-direction: row;
    gap: 4px;
}
/* Grey beveled action buttons — the matte-grey raised-bevel treatment from the toolbar
   buttons they replace (no slot color, no glow). State rides brightness + bevel: raised
   at rest, pressed-in on hover/active. ~3/4 the chat-deck-btn size, icon centered. */
.card-back-info .cb-action {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 27px;
    height: 24px;
    border-radius: 5px;
    border: 1px solid rgba(255, 255, 255, 0.15);
    background: color-mix(in srgb, white 5%, var(--card-base));
    box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.06),
        inset 0 -2px 3px rgba(0, 0, 0, 0.2);
    color: rgba(255, 255, 255, 0.55);
    text-decoration: none;
    transition: all 0.12s ease;
}
.card-back-info .cb-action:hover {
    background: color-mix(in srgb, white 9%, var(--card-base));
    color: var(--ui-text-strong);
    border-color: rgba(255, 255, 255, 0.18);
    box-shadow:
        inset 0 2px 4px rgba(0, 0, 0, 0.35),
        inset 0 -1px 0 rgba(255, 255, 255, 0.08);
}
.card-back-info .cb-action:active {
    transform: translateY(1px);
    box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.4);
}
.card-back-info .cb-action svg {
    width: 13px;
    height: 13px;
    display: block;
}
/* Delete — grey at rest like its siblings, semantic remove-red on hover (the same
   var(--danger) the roster × uses) to flag the destructive action. No slot color. */
.card-back-info .cb-action--danger:hover {
    background: color-mix(in srgb, var(--danger) 30%, var(--card-base));
    border-color: color-mix(in srgb, var(--danger) 55%, transparent);
    color: #fff;
}
/* Download is inert for guests — sign in to export. Greyed like a disabled control. */
.card-back-info .cb-action--guest-off {
    opacity: 0.4;
    pointer-events: none;
}
.card-back-info .card-back-character-nav {
    flex: 0 0 auto;   /* own line now — header is a column */
    width: 100%;
    min-width: 0;
}

/* Static character name on scene cards — replaces the card-back
   character dropdown switcher (which moved up to the top-control-
   surface roster strip). Reads as a heading next to the swatches,
   keeps the row layout intact. */
.card-back-info .card-back-character-name {
    flex: 0 0 auto;   /* own line now — header is a column */
    min-width: 0;
    font-size: 0.9rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--ui-text-strong);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* Editable name on the card-back header (editor). It wears .card-back-character-name
   too, so the font/size/colour come from the rule above; here we just strip the
   input chrome so it reads exactly like the label — no border/underline, just an
   editable spot. */
.card-back-info .card-back-name-input {
    background: transparent;
    border: none;
    outline: none;
    padding: 0;
    margin: 0;
    width: 100%;
}
.card-back-info .card-back-name-input::placeholder {
    color: rgba(255, 255, 255, 0.3);
    font-style: italic;
    font-weight: 500;
}

/* Swatch line: swatches at the left, the public/private toggle pushed to the
   right (editor only). Shares .cast-editor-private-toggle's colours; this is a
   compact card-sized variant. */
.card-back-info .card-back-swatch-row {
    display: flex;
    align-items: center;
    gap: 0.4rem;
}
/* Scoped under .card-back-info so this compact variant always beats the shared
   .cast-editor-private-toggle base rule regardless of stylesheet load order. */
.card-back-info .card-back-private-toggle {
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    flex: 0 0 auto;
    padding: 0.1rem 0.45rem;
    border-radius: 999px;
    cursor: pointer;
    font-size: 0.6rem;
    font-weight: 700;
    letter-spacing: 0.03em;
    line-height: 1;
    /* Neutral grey chrome (toolbar convention) — readable on ANY character.
       Slot colours made it vanish on e.g. a brown-hair/brown-eyes character;
       the lock icon + Public/Private label carry the state, not a hue. */
    background: color-mix(in srgb, white 6%, var(--card-base));
    border: 1px solid rgba(255, 255, 255, 0.14);
    color: rgba(255, 255, 255, 0.58);
}
.card-back-info .card-back-private-toggle.is-private {
    /* Locked reads as a brighter neutral, not a colour shift. */
    background: color-mix(in srgb, white 11%, var(--card-base));
    border-color: rgba(255, 255, 255, 0.30);
    color: rgba(255, 255, 255, 0.85);
}

/* Front (image-side) copy of the public/private toggle — pinned top-right so the
   editor finds it without flipping to the scene back. Shares the neutral grey
   .cast-editor-private-toggle base; only the position differs. */
.strip-card .card-front-private-toggle {
    position: absolute;
    top: 8px;
    right: 8px;
    z-index: 4;
}

/* Scene-mode thumb-row breathing room. Base .char-row-with-thumb
   in fieldsets-layout.css has gap: 0.5rem — tight at the carousel
   card-back size where the thumb and the field column kiss.
   Bump just the column gap so the thumb and the mood / activity /
   description stack don't touch. */
.card-back-info--scene .char-row-with-thumb {
    column-gap: var(--cb-gap);
    row-gap: var(--cb-gap);
    /* Always use the .is-wide grid pattern from fieldsets-layout.css —
       thumb + mood/activity stack share row 1, description takes the
       full width below. Default base puts thumb spanning both rows
       and description squeezed beside it; on the carousel-card size
       a longer description shoved the row taller and dragged the
       thumb with it, blowing up the layout. Pinning description to
       its own row gives it the breathing room it needs (4-line clamp
       below) without resizing the thumb. The thumb gets a touch
       smaller (bounded by the stack's height) but the trade is
       worth it. */
    grid-template-areas:
        "thumb stack"
        "description description";
}
/* Mood lifted to its own centered row above the avatar (out of the stack,
   title dropped). Content-width so it centers instead of stretching edge-to-edge. */
.card-back-info--scene .card-back-mood-row {
    justify-content: center;   /* fixed centered row above the scroll area (no longer a grid cell) */
}
/* Everything below mood scrolls: the avatar row + the (de-beveled) appearance tags.
   flex:1 fills the card below the fixed header+mood; on tiny cards scroll the avatar
   away to reach the tags, on tall cards the tags sit as decoration below description.
   The card-back-info bottom padding keeps the 10px lower border. */
.card-back-info--scene .card-back-scroll {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: var(--cb-gap);
    scrollbar-width: thin;
    scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
}
.card-back-info--scene .card-back-mood-row > :not(label):not(.crystallize-spark) {
    flex: 0 1 auto;   /* was flex:1 1 0 (chat-input.css filled the row) — size to content + center */
    min-width: 0;
}

/* Scene-mode mood / activity / description fields. Labels match the
   chub-tab typography (.card-tab-btn) — small uppercase, same weight
   and tracking — so the field heading reads as part of the same family
   as the tab strip below. Values sit in a dark bevel container that
   echoes the .card-pane recipe (.field--bevel). Mood is live (vocab
   input → select2 via vocabulary-select2.js); activity + description
   are placeholders until their wiring lands. */
.card-back-info--scene .card-back-form-field {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    min-width: 0;
}
.card-back-info--scene .card-back-form-field > label {
    font-size: 0.72em;   /* em off the --scene master (was 0.6rem) so it scales with the card */
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: rgba(255, 255, 255, 0.55);
}
/* Paned form fields (activity / description) — lift the chub
   .card-pane recipe (solid slot-1 15% + 3-stop inset bevel) onto the
   field wrapper itself so the title row + body share one bevel. The
   .field-header inside holds the label and gets the speaker button
   auto-inserted by field-speaker.js. Body has a fixed min-height so
   short content still reserves the full row count — the row 1 height
   (thumb + mood + activity stack) stays stable across scenes, which
   keeps the thumb at a stable larger size on idle/short scenes
   instead of collapsing. */
.card-back-info--scene .card-back-form-field--paned {
    padding: 0.4rem 0.55rem;
    background: color-mix(in srgb, var(--primary) 15%, var(--card-base));
    border: 1px solid rgba(255, 255, 255, 0.06);
    border-radius: 6px;
    box-shadow:
        inset 0 3px 6px rgba(0, 0, 0, 0.5),
        inset 0 1px 0 rgba(0, 0, 0, 0.3),
        0 1px 0 rgba(255, 255, 255, 0.04);
    color: var(--ui-text-strong);
    gap: 0.15rem;
}
.card-back-info--scene .card-back-form-field--paned .field-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    font-size: 0.72em;   /* em off the --scene master (was 0.6rem) */
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: rgba(255, 255, 255, 0.55);
    line-height: 1.2;
}
.card-back-info--scene .card-back-form-field--paned .field-header > label {
    margin: 0;
    /* Pull the label left into the pane's 0.55rem horizontal padding
       so the title sits at ~the same x as the mood label (which gets
       nudged right by the same amount below). All three field titles
       line up vertically across mood + activity + description. */
    margin-left: -0.25rem;
}

/* Mood label nudges right to meet the paned titles in the middle —
   mood lives outside the select2's bevel, so its label naturally sits
   flush with the wrapper edge. Padding (not margin) so it doesn't
   re-collapse the flex 0 0 100% width chat-input.css sets. */
.card-back-info--scene .field.field--mood > label {
    padding-left: 0.8rem;
    font-size: 0.72em;   /* match the activity/description titles (was inheriting the ~1em master) */
}
/* Mood select2 value: match the body + tags. The master left it at ~1em, bigger
   than the other values. (select2-theme sets line-height but not font-size, so a
   plain scoped rule wins over inheritance.) */
.card-back-info--scene .field.field--mood .select2-selection__rendered {
    font-size: 0.95em;   /* match the body content size */
}
/* Body text: small, line-height tight. min-height reserves the box at
   the same vertical room every scene; max-height caps it so longer
   content scrolls inside the pane instead of pushing the layout
   around. Thin overlay scrollbar matches the Appearance pane + the
   card-front conversation — subtle white thumb on transparent track,
   surfaces when scrolling. */
.card-back-info--scene .card-back-form-body {
    font-size: 0.95em;   /* content value — bumped to gallery parity (titles 0.72em, tag pills 0.85em) */
    line-height: 1.35;
    overflow-y: auto;
    scrollbar-width: thin;
    scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
}
.card-back-info--scene .card-back-form-body::-webkit-scrollbar {
    width: 4px;
}
.card-back-info--scene .card-back-form-body::-webkit-scrollbar-track {
    background: transparent;
}
.card-back-info--scene .card-back-form-body::-webkit-scrollbar-thumb {
    background: rgba(255, 255, 255, 0.2);
    border-radius: 2px;
}
/* Activity now sits beside the avatar alone (mood lifted out), so let it FILL
   the avatar's height instead of capping at 2 lines: the stack is a flex column,
   the pane grows, the body fills + scrolls. min-height keeps a floor for short/idle
   scenes so the avatar row still has a stable minimum. */
.card-back-info--scene .char-mini-stack {
    display: flex;
    flex-direction: column;
    min-height: 0;
}
.card-back-info--scene .char-mini-stack .card-back-form-field--paned {
    flex: 1;
    min-height: 0;
}
.card-back-info--scene .card-back-form-body[data-card-back-field="activity"] {
    flex: 1;
    min-height: calc(1em * 1.35 * 2);
    max-height: none;
}
/* Description full-width row below: 3 content lines (+ title row = 4
   total inside the pane). */
.card-back-info--scene .card-back-form-body[data-card-back-field="environment_description"] {
    min-height: calc(1em * 1.35 * 3);
    max-height: calc(1em * 1.35 * 3);
}
/* Card-back Appearance tab bubbles: 3-per-row form factor on the
   small carousel surface. Scoped to .card-back-builder-tags so the
   page-level #builder-* containers keep their full-size look.
   ──
   Three shrinks vs the default .builder-tag:
     - .builder-tag itself: tighter padding + smaller font + smaller gap
     - .tag-icon-circle: 18×18 (was 24×24)
     - .tag-switch: shrunk in step
   Three select2 strips inside the bubble:
     - .select2-selection--single: transparent background, no border,
       no border-left accent, no fixed height (was 28px) → the select
       reads as inline text, not a dark field-in-a-bubble nest
     - .select2-selection__rendered: zero padding so the name sits flush
     - .select2-selection__arrow: hidden — bubbles are toggle widgets;
       the click target is the whole bubble */
.card-back-builder-tags.builder-tags {
    gap: 0.25rem;
    padding-top: 0;
}

/* Appearance pane title row — label left, elements crystallizer
   right. flex + space-between for the two-end layout. The crystal
   uses its base .crystallize-spark idle / hover styling from
   chat-input.css; we just place it here. */
.card-back-info .appearance-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 0.35rem;
}
/* Match the typography of fieldsets-char.css's .card-pane-render >
   label rule — same uppercase + tracking + weight + colour as the
   Description / Persona / etc. pane labels. (Can't inherit that
   rule directly because it uses `> label` direct-child and our
   label is wrapped inside .appearance-header.) */
.card-back-info .appearance-header > label {
    margin: 0;
    display: block;
    font-size: 0.72em;   /* match the other card-back titles (was 0.6rem) */
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--ui-text-soft);
}
.card-back-info .crystallize-spark--card-back {
    font-size: 0.85rem;
    /* Anchor the hover-scale to the right edge — base rule
       transform: scale(1.18) grows from centre, which pushed past
       the pane's right edge and triggered a horizontal scrollbar.
       Right-origin keeps the crystal's right edge pinned and grows
       leftward into the pane instead. */
    transform-origin: right center;
}

/* Frame chip + bubble container share one flex-wrap row. The
   inner .card-back-builder-tags uses display: contents so its dynamic
   bubble children appear as direct flex children of the row, wrapping
   in step with the frame chip. Without display: contents the bubbles
   would all be one flex item (the dynamic container) and the frame
   chip would sit alone above them. */
.card-back-info--scene .card-back-builder-tags-row,
.card-back-info--character .card-back-builder-tags-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 0.25rem;
    /* Breathing room before the pane-render scroll boundary — without
       it the last row's bottom border (1px) was clipped, reading as
       a half-cut overlay edge. */
    padding-bottom: 0.4rem;
}
.card-back-info .card-back-builder-tags-row > .card-back-builder-tags {
    display: contents;
}

/* All-heights-locked-at-1.5rem approach: icons grow up to match the
   dropdown's search overlay so when the overlay slides over the
   bubble, the search field fits cleanly inside it. icons +
   selection + overlay margin + search container + search field all
   pinned to 1.5rem with  so no specificity tie can drift
   them apart. */
/* Match the bubble height for both dropdown positions. select2-theme.
   css has two rules tied to --gs-select-height (default 28px):
     - select2-dropdown--below: margin-top: -28px → pulls overlay UP
     - select2-dropdown--above: transform: translateY(28px) → pushes
       it DOWN from the above-anchor position
   The var-override approach on .builder-tag-dropdown wasn't reaching
   the dropdown reliably across browsers (cascade quirks), so we
   override the consumed properties directly instead. The --above
   rule is the one that bit us on mobile, where the dropdown opens
   above the bubble (less room below near the bottom of the
   viewport). */
.builder-tag-dropdown.select2-dropdown--below {
    margin-top: -1.5rem;
}
.builder-tag-dropdown.select2-dropdown--above {
    transform: translateY(1.5rem);
}
.builder-tag-dropdown .select2-search--dropdown,
.builder-tag-dropdown .select2-search--dropdown .select2-search__field {
    height: 1.5rem;
}
/* Mobile centering — line-height = height forces text to sit on the
   exact vertical centre of the input regardless of the platform's
   default input rendering. iOS Safari was positioning the text low
   inside the field while desktop honoured the parent's
   align-items: center. Pinning padding-top/bottom to 0 strips any
   user-agent vertical padding that creeps in past appearance: none. */
.builder-tag-dropdown .select2-search--dropdown .select2-search__field {
    line-height: 1.5rem ;
    padding-top: 0 ;
    padding-bottom: 0 ;
}
.card-back-builder-tags-row .builder-tag {
    /* Inset around the icon — vertical + horizontal padding so the
       icon circle isn't crowding the bubble's outer border. Icon
       wears its default grey border (cleaner than the slot-tinted
       one which overlapped the bubble's edge messily); the grey
       icon edge sits in a small pool of bubble background, then
       the bubble's slot-tinted outer edge. */
    padding: 0.2rem 0.35rem;
    gap: 0.25rem;
    font-size: 0.85em;   /* em off the master — match the body + select value (was 0.7rem) */
    border-radius: 999px;
    /* No overflow: hidden — it was clipping the swatch popup that
       openSwatchPopup (element-builder.js:370) appends inside the
       icon element. With the padding inset keeping the icon back
       from the bubble's rounded cap, there's nothing for overflow
       to clip anyway. */
}
.card-back-builder-tags .tag-icon-circle {
    /* 2px smaller than the bubble's content height so the bubble's
       padding can breathe around it. Default grey border restored —
       the bubble's slot-tinted outer edge contrasts with the icon's
       neutral edge instead of fighting it at the overlap. */
    width: calc(1.5rem - 2px);
    height: calc(1.5rem - 2px);
    border-width: 1px;
}
.card-back-builder-tags .tag-icon-emoji {
    font-size: 0.7rem;
}
.card-back-builder-tags .tag-switch {
    width: 24px;
    height: 14px;
    border-radius: 7px;
}
.card-back-builder-tags .tag-knob {
    width: 8px;
    height: 8px;
    top: 2px;
    left: 2px;
}
.card-back-builder-tags .tag-switch.on .tag-knob {
    left: 13px;
}
/* Selection: strip the dark field AND pin to 1.5rem to match the
   icons + dropdown overlay. All four heights move together so the
   overlay sits exactly on the bubble area when the dropdown opens. */
.card-back-info .card-back-builder-tags-row .select2-container--default .select2-selection--single,
.card-back-info .card-back-builder-tags-row .select2-container--admin-autocomplete .select2-selection--single {
    background: transparent ;
    border: none ;
    border-left: none ;
    border-radius: 0 ;
    box-shadow: none ;
    height: 1.5rem ;
    min-height: 0 ;
    padding: 0 ;
    display: flex;
    align-items: center;
}
.card-back-info .card-back-builder-tags .select2-container--default .select2-selection--single:hover,
.card-back-info .card-back-builder-tags .select2-container--admin-autocomplete .select2-selection--single:hover {
    background: transparent;
    border: none;
}
/* Selector has .select2-container--default included so it beats
   select2-theme.css's :is(.fieldset, .card-back-info)
   .select2-container--default .select2-selection__rendered rule
   (which sets line-height: 28px and inflates the chip). 4 classes
   beats 3 regardless of file load order. */
.card-back-info .card-back-builder-tags-row .select2-container--default .select2-selection__rendered,
.card-back-info .card-back-builder-tags-row .select2-container--admin-autocomplete .select2-selection__rendered {
    padding: 0;
    line-height: 1.5rem;
    font-size: 0.85em;   /* em off the master — match the tag bubbles + body (was 0.7rem) */
}
.card-back-info .card-back-builder-tags-row .select2-selection__arrow {
    display: none;
}

/* Frame chip uses .frame-icon (SVG container) instead of the dynamic
   bubbles' .tag-icon-circle (emoji). Mirror the icon-circle look so
   the frame chip sits flush with the rest of the row: same size,
   same circle, same border + background. SVG inside scales to ~60%
   so it has the same visual weight as the bubble emojis. */
.card-back-info .card-back-builder-tags-row .frame-icon {
    width: calc(1.5rem - 2px);
    height: calc(1.5rem - 2px);
    border-radius: 50%;
    border: 1px solid rgba(160, 160, 160, 0.5);
    background: rgba(255, 255, 255, 0.1);
}
.card-back-info .card-back-builder-tags-row .frame-icon svg {
    width: 60%;
    height: 60%;
}

/* Live mood input before vocabulary-select2.js converts it — bare text
   input wears the same bevel so it doesn't pop visually when the JS
   runs late. After conversion select2's own chrome takes over (from
   select2-theme.css, already scoped to .card-back-info). */
.card-back-info--scene .card-back-form-field > input[data-vocabulary] {
    padding: 0.3rem 0.5rem;
    font: inherit;
    font-size: 0.75rem;
    background: rgba(0, 0, 0, 0.25);
    border: 1px solid rgba(255, 255, 255, 0.06);
    border-radius: 4px;
    box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.4);
    color: var(--ui-text-strong);
    width: 100%;
    box-sizing: border-box;
}

/* Gallery card-back select2: drop the slot-tinted 3px left accent
   from select2-theme.css. The accent inherits --accent: var(--primary)
   on the card-back, so on dark-haired characters that left edge goes
   invisible and the box reads as missing a border. Restore the same
   simple grey on all four sides so the select always has a defined
   fallback frame, matching the .join-btn treatment. */
.card-back-info .select2-container--default .select2-selection--single,
.card-back-info .select2-container--admin-autocomplete .select2-selection--single {
    border-left: 1px solid rgba(255, 255, 255, 0.2);
}
/* Hover: select2-theme.css's base sets border-color (all 4 sides) to
   60% --accent + border-left-color to FULL --accent (the old 3px
   accent's hover state). Since we flattened the left to a 1px line at
   rest, the full-strength left-color looked off versus the matching
   60% on the other 3 sides. Pin border-left-color to the same 60% mix
   so the field accent paints symmetrically on hover. */
.card-back-info .select2-container--default .select2-selection--single:hover,
.card-back-info .select2-container--admin-autocomplete .select2-selection--single:hover {
    border-left-color: color-mix(in srgb, var(--accent) 60%, transparent);
}

/* color-swatches + color-swatch base styles inherited from the global
   fieldsets-base.css (.color-swatches: flex+4px gap; .color-swatch: 20x20
   with 2px border + 4px radius). The card back DIVERGES from the editor's
   fixed 20x20 here: on a small landscape-portrait card a full set of
   builder-tag colors would breach the width, so the row is allowed to give
   and each swatch scales to fit — 20px when there's room, shrinking equally
   (staying square) when crowded. Big cards with few colors look unchanged. */
.card-back-info .color-swatches {
    order: -1;          /* swatches own the top line; name/select drops below */
    min-width: 0;
    gap: clamp(2px, 1cqw, 3px);
    flex-wrap: wrap;    /* overflow wraps to a 2nd row instead of breaching */
}
.card-back-info .color-swatch {
    flex: 0 0 auto;
    width: clamp(7px, 5cqw, 20px);  /* keyed to the CARD width, not the count:
                                       same-size cards => same swatches. Factor
                                       kept low enough that narrow (mobile) cards
                                       scale BELOW the 20px cap instead of pinning
                                       to it, so the fraction matches wide desktop
                                       cards. Lower the 5 to shrink further. */
    height: auto;
    aspect-ratio: 1;    /* stay square at any width */
    border-width: 1px;  /* thinner so the colour interior survives when small */
    border-radius: 18%; /* proportional radius — stays square, never a circle */
}
/* Swatches off vmax too (matching the avatar + master scale), clamped — for BOTH
   card-backs. The general rule above is cqw (card-width-keyed), so without this the
   scene swatches rescaled portrait-vs-landscape on the mobile-landscape flip. */
.card-back-info--character .color-swatches,
.card-back-info--scene .color-swatches { gap: clamp(2px, 0.35vmax, 3px); }
.card-back-info--character .color-swatch,
.card-back-info--scene .color-swatch { width: clamp(7px, 1.7vmax, 14px); }
/* (No per-orientation override needed now — vmax is the same in both, so the
   portrait↔landscape flip no longer rescales the content.) */

/* .card-tab-strip / .card-tab-btn / .card-pane / .card-pane-render
   styles inherited from fieldsets-char.css (the editor's tab + pane
   chrome). One source of truth — restyling the editor restyles the
   card back. Notable inherited rules:
     .card-tab-btn.active uses color: var(--ui-text-strong) which is
     the global light grey (rgba(255,255,255,0.85)) — readable on
     ANY slot color, so dark-haired characters don't lose their tab
     text against the dark active gradient. */
.card-back-info .card-pane {
    flex: 1;
    min-height: 0;
    overflow: hidden;
    /* Override fieldsets-char.css's slot-2 → slot-1 gradient: on the
       gallery the gradient lives on the card surface now (flip-front /
       flip-back), and the vLargeText pane reads cleaner as a single
       quieter shade. Solid value mirrors the colour the flip surface
       used to wear, so the pane sits a touch darker than the card
       around it for definition. Scene-page editor (.fieldset--char
       .card-pane) keeps its gradient — this override is scoped. */
    background: color-mix(in srgb, var(--primary) 15%, var(--card-base));
}
/* Scene card-back: the appearance tags are decoration on the card background now,
   not a bevelled field. Strip the .card-pane bevel + the 100px card-pane-render
   min-height (a giant empty box on tall desktop cards); it sizes to its tags inside
   the scroll area. */
.card-back-info--scene .card-pane {
    flex: 0 0 auto;
    background: none;
    border: none;
    box-shadow: none;
    border-radius: 0;
    padding: 0;
    overflow: visible;
}
.card-back-info--scene .card-pane-render {
    min-height: 0;
}

/* Tighten the tab area on the carousel card-back — every pixel
   counts at this size. Two compressions vs the bottom-fieldset
   defaults (fieldsets-char.css):
     - Strip margin-top: 0.6rem → 0 — kills the gap between the
       description box and the tab strip.
     - Strip margin-bottom: -1px — pulls the whole strip down over
       the pane's top border so non-active tabs flush against the
       pane the same way the active tab already does via its own
       -1px lift. */
.card-back-info .card-tab-strip {
    margin-top: 0;
    margin-bottom: -1px;
}

/* Active tab merges into the pane bevel below it, so its background
   has to match — kill the gradient inherited from fieldsets-char.css
   and use the same solid as .card-back-info .card-pane. */
.card-back-info .card-tab-btn.active {
    background: color-mix(in srgb, var(--primary) 15%, var(--card-base));
}

.card-back-info .card-pane-render {
    overflow-y: auto;
    max-height: 100%;
}

/* ═══════════════════════════════════════════════════════════════════════════
   SLIDE-IN — one animation for page load and WS injection
   Card slides from off-screen right to its natural position.
   'both' fill mode hides the card during the delay (holds 0% keyframe).
   Remove the class after animation ends so depth system takes over.
═══════════════════════════════════════════════════════════════════════════ */

@keyframes slideIn {
    from { transform: translateX(120%) scale(0.92); opacity: 0; }
    to   { transform: translateX(0) scale(0.92); opacity: 1; }
}

.strip-card--slide-in {
    animation: slideIn 0.5s ease-out 0.5s both;
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE LAYOUT MODEL — one shape, three regimes
   ═══════════════════════════════════════════════════════════════════════════
   The preview fieldset is a GRID of four rows, and its four children are real
   siblings in the template (character_form_fields.html), each stating its own
   grid-row:

     fieldset  = 100vh − <the regime's ONE chrome number>     ← anchored, fixed
       row 1   6%          .strip-nav      (pips)
       row 2   minmax(0,1fr)  .carousel-standalone  (fills — the card lives here)
       row 3   auto        .roster-strip   (gallery only; collapses when absent)
       row 4   6%          .strip-title    (page title)

   Rows 1 and 4 are equal shares, so the card reads centred between them at every
   size. Nothing RESERVES anything: each box gets a track and its contents size off
   that track (the pips are 34cqh of row 1, the title 40cqh of row 4). The old model
   reserved each row with a variable that had to be kept manually equal to the real
   element — every drift between a pair was a bug.

   Each regime supplies only the chrome number and the viewport unit; the grid,
   the row assignments and the fill rules are otherwise identical in all three. */

/* NO container-type here. It carried `contain: layout style inline-size`, and a
   contained subtree can keep a STALE internal layout when the container is sized after
   the fact — which is exactly what the failure looked like: a correctly sized 774x561
   carousel with a 0x0 child inside it, on a page where nothing above it was wrong. It
   survived the child being a percentage box AND a stretched grid item, because neither
   was ever the problem. Nothing needs a container here any more: the card's cqw readers
   (.card-back-info, .card-back-info--scene) declare their own, and the card-width formula
   that read 100cqw off this element is gone with the reservation model. */

/* THE CARD FILLS ITS BOX BY STRETCHING, NOT BY PERCENTAGE.
   The chain used to be: fieldset height → row → carousel 100% → container 100% →
   strip 100% → card 94%. Five percentage resolutions deep, and a percentage resolves to
   NOTHING when its parent's size isn't definite yet. A stretched grid item just takes
   the track's size — no resolution step to lose. So the carousel and the container each
   become a one-row grid and their child stretches into it. Only the card keeps a
   percentage, and it's a single hop off a definite box. */
.carousel-standalone { display: grid; grid-template-rows: minmax(0, 1fr); min-height: 0; }
.film-strip-container { display: grid; grid-template-rows: minmax(0, 1fr); min-height: 0; }

/* THE card-size knob: % of the carousel grid row the card fills; the remainder is the
   top/bottom clearance off the frame tabs. 94 -> 97 halves that gap (~30px -> ~15px).
   Height-bound, so every % taller widens the card in step (aspect ratio) — bigger card,
   equal on both axes. Watch it doesn't push the card into the frame tabs horizontally or
   trip max-width; if it does, that's the compensation, not the chrome dials.
   94 -> 97 -> 98.5 (settled; 99 was edge-to-edge, 98.5 frames nicer). % not px, so the real
   gain scales with the row height; if precise px clearance is wanted, switch to calc(100% - Npx). */
.entity-2col .strip-card { height: 98.5%; width: auto; }

/* ═══════════════════════════════════════════════════════════════════════════
   RESPONSIVE — each regime supplies its chrome number; the grid is shared
═══════════════════════════════════════════════════════════════════════════ */

/* Mobile portrait: the card FITS the screen instead of clipping. The card is
   the smaller of what the width allows (full phone width) and what the height
   allows (the visible viewport minus the header + control bar, converted to a
   width via --card-ar). So header + carousel + control bar land in one
   screenful — no scroll just to reach the controls — and a short phone shrinks
   the card (narrower, centred) rather than cutting it off. height:auto +
   aspect-ratio keep it undistorted. 100svh (the SMALL, static viewport) so the
   size never recalculates as the URL bar slides — 100dvh did, which made the
   cards jump while scrolling. Trade: it always reserves the URL-bar space, so
   it won't expand when the bar hides, but it also won't twitch.
   --mobile-chrome is the one tunable offset. */
@media (max-width: 700px) {
    /* Same four-row grid as desktop — the ONLY difference is which number sizes the box
       (--mobile-chrome, and dvh so the card grows as the URL bar hides). One shape for
       every regime; nothing here reserves or forwards a height. */
    .entity-2col .entity-preview .fieldset {
        height: calc(100dvh - var(--mobile-chrome, 188px));
        min-height: 0;
        display: grid;
        grid-template-rows: 6% minmax(0, 1fr) auto 6%;
        grid-template-columns: minmax(0, 1fr);
    }
    .entity-2col .entity-preview .fieldset > .strip-nav  { grid-row: 1; }
    .entity-2col .entity-preview .fieldset > .carousel-standalone { grid-row: 2; min-height: 0; }
    .entity-2col .entity-preview .fieldset > .roster-strip { grid-row: 3; }
    .entity-2col .entity-preview .fieldset > .strip-title  { grid-row: 4; }

    .entity-2col .scrolling-strip { min-height: 0; padding-top: 0; padding-bottom: 0; }

    .carousel-standalone { --strip-padding: 4px; }
}

/* Desktop / tablet: the carousel FILLS the page via a DEFINITE strip height
   (viewport − --carousel-chrome) for ALL layouts. The card's height:100% then
   resolves against a real number. The non-gallery pages USED to flex-fill, but
   that height-chain was fragile — it collapsed the cards to bare pips when the
   chrome heights shifted (the exact failure the gallery hit, which is why the
   gallery went definite first; now they share the robust path). --carousel-
   chrome is a per-page px offset set in fieldsets-layout.css; tune it if there's
   dead space (lower) or the control row is cut off (raise). Placed BEFORE the
   mobile-landscape block so its rules still win on landscape phones (which also
   match 701px). */
@media (min-width: 701px) and (min-height: 601px) {
    /* ══ THE PREVIEW IS A GRID OF FOUR SIBLING ROWS ══
       pips / card / roster / title. They are siblings in the markup now, so the browser
       divides the box and the contents stretch — there is no height forwarded through a
       wrapper, no reservation kept manually equal to a real element, and no calc chain.
       Every bug this file has had was one of those drifting.
       --preview-h is the ONE per-surface number: the fieldset's height. */
    .entity-2col .entity-preview .fieldset {
        height: calc(100svh - var(--carousel-chrome, 260px));
        min-height: 0;
        display: grid;
        grid-template-rows: 6% minmax(0, 1fr) auto 6%;
        grid-template-columns: minmax(0, 1fr);
    }
    /* Rows are STATED, not inferred from child order — the roster is absent on chat and
       pre-login, and counting children would slide the title up a row. */
    .entity-2col .entity-preview .fieldset > .strip-nav  { grid-row: 1; }
    .entity-2col .entity-preview .fieldset > .carousel-standalone { grid-row: 2; min-height: 0; }
    .entity-2col .entity-preview .fieldset > .roster-strip { grid-row: 3; }
    .entity-2col .entity-preview .fieldset > .strip-title  { grid-row: 4; }

    /* The carousel just passes its row's height down to the strip. */
    .entity-2col .scrolling-strip { min-height: 0; padding-top: 0; padding-bottom: 0; }
    /* The card stretches to its row, less a little clearance off the frame tabs. Its own
       height, not strip padding — padding percentages resolve against WIDTH. */

    /* The CHAT FIELDSET fills the window and bottom-aligns with the preview
       column (carousel + toolbar). The height lives on the FIELDSET (not the
       column), so it's the SAME size on the user chat page and the admin editor
       — nothing shrinks it. The editor's Generation Settings simply sits BELOW
       it (the fields column grows past the window and the page scrolls), so the
       chat reads identically for admin and users. Tune the 98 to bottom-align
       with the preview column; the carousel's own --carousel-chrome (260) is
       untouched. --face-tabs-reserve (fieldsets-layout.css) adds the header
       face-tabs row height here TOO, so the chat fieldset shrinks in step with
       the preview column and they stay the same height. The EDITOR carries the
       face tabs now as well, so it gets the same reserve; the 0px fallback just
       guards any page without the row. */
    .entity-2col .entity-fields .fieldset--chat {
        /* 109 → 59 (−50 down) → 69 (+10 back up to clear scrollbars), in step with the
           preview column's chrome (fieldsets-layout.css) so the two stay bottom-aligned.
           The preview took an extra −12 to close the skew it had over this column.
           NOTE this is the DESKTOP dial (min-height:601). The short/landscape regime has
           its own chat height further down — don't tune one expecting the other. */
        --carousel-chrome: calc(67px + var(--face-tabs-reserve, 0px));
        height: calc(100svh - var(--carousel-chrome, 260px));
        min-height: 0;
        display: flex;
        flex-direction: column;
    }
    .entity-2col .entity-fields .fieldset--chat .chat-conversation-wrap {
        flex: 1 1 auto;
        min-height: 0;
        display: flex;
        flex-direction: column;
    }
    .entity-2col .entity-fields .fieldset--chat .chat-conversation {
        flex: 1 1 auto;
        height: auto;      /* override the fixed 360px so it fills the fieldset */
        min-height: 0;
        resize: none;      /* the fieldset height is the knob now, not a drag handle */
    }
}

/* Mobile landscape: height is scarce, flip to height-driven.
   max-width:700px doesn't fire because landscape phones are wider. */
/* ── 3-COLUMN LANDSCAPE (chat / editor / gallery) ─────────────────────────────
   Phone turned sideways: the header becomes the LEFT rail, the toolbar (chat
   transport / gallery controls) the RIGHT rail, and the carousel fills the
   CENTER full-height. Frees the scarce landscape height for the image instead
   of letting top/bottom chrome eat it. The gallery's header tag-filter is
   hidden here (doesn't fit the narrow rail) — navigate by swiping. */
/* min-width guard: without it this block OVERLAPPED the portrait `max-width:700px`
   rules — a 650x500 window matched BOTH, so the rail layout and the portrait
   fixed-toolbar layout applied at once and collided (the footer landing in the
   header). The three regimes are now a clean partition: <=700 portrait,
   >=701 + short + landscape rails, >=701 + tall desktop. */
@media (orientation: landscape) and (max-height: 600px) and (min-width: 701px) {
    .carousel-standalone { --rail-w: 56px; }

    /* LEFT RAIL — the site header, stood up vertical (login/user top → brand
       bottom via column-reverse: the strip is brand, slot, deck in DOM order). */
    body:has(.entity-2col) .scene-control-surface--header {
        position: fixed;
        top: 0;
        left: 0;
        bottom: 0;
        width: 56px;
        z-index: 40;
        /* NO overflow clip — set up exactly like the footer/right rail, so the
           absolute frame tabs poke out freely (no show/hide toggle). The vertical
           brand/username text is clipped on its own strip instead (below). */
        /* Frame the whole rail like the footer/right rail: 2px grey border, screen
           (left) corners rounded, the carousel-facing RIGHT edge square (the tabs own
           it). --surface-bg is a 90deg hair->eyes sweep; the rail is that bar stood up,
           so re-run it bottom->top: hair at the BOTTOM (hair tab), eyes at the TOP. */
        border: 2px solid rgba(255, 255, 255, 0.15);
        border-right: none;   /* the carousel-facing edge is owned by the TABS + the
                                 between-tabs line; the rail's own right border would just
                                 double up with them, so drop it and let the tabs BE the edge. */
        border-radius: 12px 0 0 12px;
        background: linear-gradient(to top,
            color-mix(in srgb, var(--primary) 14%, var(--card-base)) 0%,
            color-mix(in srgb, var(--secondary) 14%, var(--card-base)) 100%);
    }
    body:has(.entity-2col) .scene-control-surface--header .control-panel,
    body:has(.entity-2col) .scene-control-surface--header .aurora-header__strip {
        flex-direction: column-reverse;
        height: 100%;
        gap: 6px;
    }
    body:has(.entity-2col) .scene-control-surface--header .control-panel {
        display: flex;   /* THE missing piece — same lesson as the gallery filter
                            (further down): .control-panel is block by default, so
                            the column-reverse above was a no-op and the 100%-tall
                            strip pushed any later row (the face tabs; the filter
                            before it) below the overflow-clipped rail. Flex lets
                            the strip shrink and share the rail. (The gallery's own
                            later rule still overrides this to a row there.) */
    }
    body:has(.entity-2col) .scene-control-surface--header .aurora-header__strip {
        min-height: 0;   /* flex min-height:auto would refuse to shrink beside the
                            face tabs; allow it, clipping inside the strip instead */
    }
    body:has(.entity-2col) .aurora-header__deck {
        /* column-reverse: DOM is [admin/tools/think, username, logout], so this
           reads top→bottom as logout/login, username, then the (temporary)
           admin buttons. margin:0 kills .chat-control-deck's margin-left:auto
           (the portrait right-push) that was shoving it right + clipping it. */
        flex-direction: column-reverse;
        align-items: center;   /* center the buttons inside the deck */
        align-self: center;    /* center the DECK in the rail (override the
                                  base align-self:flex-start that pinned it aside) */
        margin: 0;
    }
    /* Username reads vertically up the rail (writing-mode = a real vertical box
       that fits the rail, unlike rotate() whose box stayed horizontal/clipped). */
    body:has(.entity-2col) .aurora-user-chip {
        writing-mode: vertical-rl;
        transform: rotate(180deg);
        white-space: nowrap;
    }
    body:has(.entity-2col) .drawer-controls--nav {
        display: none;   /* page tabs live on the right rail (scs-mobile-nav) */
    }
    /* Brand reads vertically up the narrow rail. writing-mode gives it a real
       vertical box (fits the rail width) instead of rotate()'s horizontal box
       that overflowed and clipped "Acc". rotate(180) makes it read bottom→top. */
    body:has(.entity-2col) .aurora-header__brand {
        writing-mode: vertical-rl;
        transform: rotate(180deg) translateY(4px);   /* lift the brand so the cursive 'A'
                                                         flourish (at the rotated bottom)
                                                         clears the strip's overflow clip */
        padding: 10px 0;
    }
    body:has(.entity-2col) .aurora-header__brand .brand-cursive {
        font-size: 1.6rem;
        white-space: nowrap;
    }
    /* Gallery tag-filter doesn't fit the narrow rail — portrait-only. */
    body:has(.entity-2col) .aurora-header__filter {
        display: none;
    }

    /* RIGHT RAIL — the portrait toolbar, ROTATED. Keep the housing/styling; lay
       it out as two columns (transport stacked | page tabs stacked); rail width
       = the portrait toolbar's height. Buttons expand to fill their column
       (portrait's flex:1 runs across; here it runs down). Only the icons rotate. */
    .entity-2col .scene-control-surface:not(.scene-control-surface--header) {
        position: fixed;
        top: 0;
        right: 0;
        bottom: 0;
        width: 86px;
        z-index: 40;
        padding: 4px;
        display: flex;
        flex-direction: row;
        gap: 4px;
    }
    /* Column 1 — transport buttons, stacked, filling the height. Order reversed
       so it reads top→bottom: forward / record / play / stop / reverse (DOM is
       prev/stop/play/record/next). Slightly wider than the nav column to match
       portrait (transport row is a touch taller than the nav row). */
    .entity-2col .scene-control-surface:not(.scene-control-surface--header) .control-panel {
        flex: 1.5;
        height: 100%;
    }
    .entity-2col .scene-control-surface:not(.scene-control-surface--header) .drawer-controls--transport {
        flex-direction: column-reverse;
        height: 100%;
        gap: 4px;
    }
    /* Column 2 — page tabs, stacked, filling the height. (Base hides scs-mobile-
       nav; only the portrait query un-hides it, which never fires in landscape.) */
    .entity-2col .scs-mobile-nav {
        display: flex;
        /* column-reverse: DOM is Private/Public/Chat, so this reads top→bottom
           as Chat / Public / Private. */
        flex-direction: column-reverse;
        flex: 1.0;        /* full share so the page-tab icons aren't squeezed off
                             screen (transport still a touch wider, 1.4 : 1.0). */
        height: 100%;
        gap: 4px;
    }
    /* Stand each nav tab up the flipped rail: icon at the BOTTOM, label reading
       bottom→top above it — so the button reads down→up, icon through label. The
       button's DOM is [icon, label]; column-reverse drops the icon low and lifts
       the label; the label goes vertical like the brand/username (writing-mode +
       rotate(180) = a real vertical box that fits the rail, reading upward). */
    .entity-2col .scs-mobile-nav .aurora-header__nav-btn {
        flex-direction: column-reverse;
        gap: 0.3rem;
    }
    .entity-2col .scs-mobile-nav .aurora-header__nav-label {
        writing-mode: vertical-rl;
        transform: rotate(180deg);
        line-height: 1;
        white-space: nowrap;
    }
    /* The big transport actuator stands its label up the rail too — icon low, label
       reading bottom→top — same as the page tabs. Its icon already rotates via the
       .transport-btn svg rule below; this handles its stacked label. */
    .entity-2col .scene-control-surface:not(.scene-control-surface--header) .transport-btn.chats-nav--primary {
        flex-direction: column-reverse;
        gap: 0.3rem;
        flex: 2 1 0;   /* dominant in the rail (2x prev/next height); .transport-btn.chats-nav--primary
                          out-specifies the .transport-btn flex:1 rule below (same source order lost before) */
    }
    .entity-2col .scene-control-surface:not(.scene-control-surface--header) .chats-nav--primary .aurora-header__nav-label {
        writing-mode: vertical-rl;
        transform: rotate(180deg);
        line-height: 1;
        white-space: nowrap;
    }
    /* Buttons expand to fill their column (portrait flex:1, rotated). */
    .entity-2col .scene-control-surface:not(.scene-control-surface--header) .transport-btn {
        flex: 1 1 0;
        min-height: 0;
        height: auto;
        width: 100%;
        min-width: 0;   /* override the 48px floor so buttons fit the rail column
                           instead of spilling out toward the nav */
    }
    /* Turn every rail icon to match the labels so the whole strip reads like the
       portrait page laid on its side — icons were upright while the text went
       sideways, a half-and-half that read wrong. rotate(-90deg) is the labels' own
       net turn (vertical-rl + rotate(180)). Generate isn't a .transport-btn (see
       the block below), so its round button stays put — this is exactly the 8
       transport + page-tab icons. */
    .entity-2col .scene-control-surface:not(.scene-control-surface--header) .transport-btn svg {
        transform: rotate(-90deg);
    }
    /* Generate isn't a .transport-btn, so it keeps its own size here. Portrait's
       straddle trick rotated 90°: right-align it and pull it LEFT with a negative
       margin so its margin-box stays the column width (doesn't widen the rail /
       squeeze the nav) while the button bulges out toward the carousel. */
    .entity-2col .scene-control-surface:not(.scene-control-surface--header) .transport-btn--generate {
        flex: 0 0 auto;
        width: 56px;
        height: 56px;
        min-width: 0;
        align-self: flex-end;
        /* top/bottom: tread clearance from the buttons above + below; left: the
           negative straddle (kept from before). */
        margin: 4px 0 4px -28px;
        z-index: 3;
    }
    /* Portrait's button-caption footer — no room for it on the narrow rail. */
    .entity-2col .scene-control-surface:not(.scene-control-surface--header) .panel-footer {
        display: none;
    }

    /* ── Undo the portrait context-tab restructure for the rail ──────────────────
       The portrait tab makes the surface a grid, wraps both button rows in
       .scs-body, and moves the 2px frame + gradient onto .scs-body. None of that
       fits the vertical rail, so here:
       - re-assert the rail's own flex-row + restore its frame (the portrait grid
         rule stripped the surface's border/background),
       - DISSOLVE .scs-body (display:contents) so the rail's flex-row rules above
         lay out .control-panel + .scs-mobile-nav directly, exactly as before,
       - hide the horizontal tab (the sideways rail tab is a separate build). */
    .entity-2col .scene-control-surface:not(.scene-control-surface--header) {
        display: flex;
        border: 2px solid rgba(255, 255, 255, 0.15);
        /* Square the LEFT (carousel-facing) corners — the tabs own that edge, so a
           rounded rail corner would curve in front of the tab. Right (screen) corners
           stay rounded. TL TR BR BL: */
        border-radius: 0 12px 12px 0;
        /* --surface-bg is a 90deg (left→right) hair→eyes sweep. The rail is the bar
           ROTATED -90°, so re-run the sweep bottom→top: hair at the BOTTOM (under the
           hair tab), eyes at the TOP (under the eyes/mirror tab). Same two stops. */
        background: linear-gradient(to top,
            color-mix(in srgb, var(--primary) 14%, var(--card-base)) 0%,
            color-mix(in srgb, var(--secondary) 14%, var(--card-base)) 100%);
    }
    .entity-2col .scene-control-surface:not(.scene-control-surface--header) .scs-body {
        display: contents;
    }
    /* Sideways rail tab — the portrait tab ROTATED -90° (CCW), exactly what a phone
       flip does, so it "doesn't move": lands at the rail's BOTTOM-LEFT beside the
       reverse (<<) button, poking LEFT over the carousel; text reads bottom→top and
       the SVG ramp rides the top, all reused from portrait. Absolute so it never
       disturbs the rail's button columns.
       BLIND FIRST PASS — I can't render landscape; expect to tune: the rotation
       direction (-90 vs 90), transform-origin, and the bottom/left offsets that pin
       it beside the reverse button. */
    .entity-2col .scene-control-surface:not(.scene-control-surface--header) .scs-tab {
        position: absolute;
        left: 0;          /* was 2px — the 2nd 22.5 curve overlapped the rail border by 2px */
        bottom: -2px;     /* down onto the rail's bottom border */
        z-index: 3;
        transform-origin: bottom left;
        transform: rotate(-90deg);
    }
    /* MIRROR tab in the rail — the portrait top-RIGHT tab. Flipped, it lands at the
       rail's TOP-left (the left tab is bottom-left), still poking LEFT over the
       carousel. BLIND first pass — direction/origin/offsets need tuning on a real
       landscape viewport. */
    .entity-2col .scene-control-surface:not(.scene-control-surface--header) .scs-tab--right {
        bottom: auto;
        top: 78px;        /* down from the top of the screen, below the corner */
        left: -26px;      /* left ~1 tab-height — the flip pushed it to the right */
        transform-origin: top left;
        transform: rotate(-90deg);   /* flipped 180° from rotate(90) — it was upside down */
    }
    .entity-2col .scene-control-surface:not(.scene-control-surface--header) .scs-tab-edge {
        display: none;   /* the notched left-border segment — a refinement pass */
    }

    /* CENTER — carousel fills between the rails, full height; card height-driven
       so the image uses the landscape height. */
    body:has(.entity-2col) {
        /* Collapsing scroll spacer (100lvh − 100dvh) = the URL bar's visible height, so
           there's just enough scroll to retract it (nothing else sits below the fixed rails
           in landscape, so the bar would never hide otherwise — the portrait trick, minus
           the 100px bottom-footer clearance since the toolbar is a side rail here). Shrinks
           to 0 as the bar goes; the dvh carousel then stretches into the reclaimed space. */
        padding-bottom: calc(env(safe-area-inset-bottom, 0px) + (100lvh - 100dvh));
    }
    .entity-2col {
        padding-left: 56px;   /* clear the header rail */
        padding-right: 86px;  /* clear the toolbar rail */
    }
    /* Gallery shows the roster column to the LEFT of the rail, so it reserves room for
       that too (slot width = --roster-slot-h × 2/3, plus ~20px strip chrome + gap) — the
       landscape mirror of portrait's gallery-only strip reserve, sized off the slot so it
       tracks the scale. Chat/editor render no roster-strip (gallery-control-surface
       includes it only when is_gallery), so .entity-2col above gives them the rail alone
       and reclaims the column's space. */
    .entity-2col--gallery {
        padding-right: 86px;   /* was 86 + roster column + 10 — now just the rail, so the
                                  carousel runs UNDER the fixed slots (images slide beneath). */
    }
    /* (.carousel-container override removed with the rule itself — the class is in no
       markup; see fieldsets-layout.css.) */
    /* Same four-row grid as desktop and portrait. The ONE number here is the fieldset's
       height; dvh (not svh) so it grows with the fixed rails as the URL bar hides.
       The roster is FIXED slots in this regime (the carousel runs UNDER them), so it's
       out of flow and its `auto` track collapses on its own — no reserve, and no
       separate empty-roster variant, exactly like the other two regimes. */
    .entity-2col .entity-preview .fieldset {
        height: calc(100dvh - var(--landscape-chrome, 0px));
        min-height: 0;
        display: grid;
        grid-template-rows: 6% minmax(0, 1fr) auto 6%;
        grid-template-columns: minmax(0, 1fr);
    }
    .entity-2col .entity-preview .fieldset > .strip-nav  { grid-row: 1; }
    .entity-2col .entity-preview .fieldset > .carousel-standalone { grid-row: 2; min-height: 0; }
    .entity-2col .entity-preview .fieldset > .roster-strip { grid-row: 3; }
    .entity-2col .entity-preview .fieldset > .strip-title  { grid-row: 4; }

    .entity-2col .scrolling-strip { min-height: 0; padding-top: 0; padding-bottom: 0; }
    /* Chat column had NO height rule in this regime — it was content-sized, which is
       why there was nothing to dial (the desktop rule that owns the chat height is
       min-height:601 and never applies here). Give it the SAME definite box as the
       strip above so the two columns span equally and bottom-align.
       --chat-chrome is the dial: SMALLER = taller / lower. */
    .entity-2col .entity-fields .fieldset--chat {
        --chat-chrome: 0px;   /* 18 → 8 → 0 → −5 → 0. The −5 was a phantom: the box was
                                 already exactly 100dvh, but the conversation inside it
                                 was stuck at its fixed 360px, so the column read short
                                 and the dial was asked to make up a gap that wasn't in
                                 the box at all. The .chat-conversation fill rule below
                                 is the real fix; 0 = flush, no viewport overrun, and
                                 there is no unexplained offset left to chase. */
        height: calc(100dvh - var(--chat-chrome));
        min-height: 0;
        display: flex;
        flex-direction: column;
    }
    /* ...and let the CONVERSATION fill that taller box. Without these TWO rules the
       fieldset grew but the chat area didn't, so the extra height landed as dead space.
       BOTH are needed and they fix DIFFERENT gaps — the desktop block carries the pair
       and this regime only ever had the first:
         wrap  — flexes to fill the fieldset (else the gap lands under the wrap)
         conversation — overrides its fixed 360px (chat-input.css) so it fills the WRAP
                        (else the wrap stretches and the gap lands INSIDE it, between the
                        messages and the input — the "stretched the fieldset, not the
                        chat" bug). */
    .entity-2col .entity-fields .fieldset--chat .chat-conversation-wrap {
        flex: 1 1 auto;
        min-height: 0;
        display: flex;
        flex-direction: column;
    }
    .entity-2col .entity-fields .fieldset--chat .chat-conversation {
        flex: 1 1 auto;
        height: auto;      /* override the fixed 360px so it fills the wrap */
        min-height: 0;
        resize: none;      /* the fieldset height is the knob now, not a drag handle */
    }
    /* pips size off their own grid row (34cqh) — no padding sum to keep in step. */

    /* ── Gallery landscape: give the header rail a SECOND column for the tag
       filter. Other pages have no filter, so they keep the 56px single column;
       only the gallery widens and splits the control-panel into a row
       (strip | filter), mirroring the right rail's two-column layout. ── */
    body:has(.entity-2col--gallery) .scene-control-surface--header {
        width: 90px;   /* strip (~46) + filter col (38) + frame (2). 72 pinched the strip;
                          90 gives the brand/deck room back while still trimming the old 96.
                          Tabs re-docked to this edge below. */
        /* Rail is a flex ROW so a real in-flow FRAME-EDGE column can sit at the
           carousel-facing edge. The absolute line couldn't fill the tab gap; an in-flow
           grid column renders deterministically. flex (NOT grid) keeps the absolute tabs
           resolving against the rail's padding box. */
        display: flex;
        flex-direction: row;
    }
    body:has(.entity-2col--gallery) .scene-control-surface--header .control-panel {
        display: flex;         /* THE missing piece: .control-panel is block by
                                  default, so flex-direction alone was a no-op —
                                  that's why no 2nd column ever formed. */
        flex-direction: row;   /* two side-by-side columns: strip | filter */
        align-items: stretch;
        gap: 4px;
        height: 100%;
        flex: 1 1 0;           /* fill the rail, leaving the frame-edge column at the far right */
        min-width: 0;
    }
    /* ── FRAME-EDGE column — a thin 3px column at the rail's carousel-facing edge, split
       into 3 ROWS: [eyes tab | GAP line | hair tab]. Only the MIDDLE row paints (::before),
       so the line fills the gap between the tabs and stretches (1fr), while the tabs own
       the top/bottom. Row heights are the knobs for where the line starts/stops. ── */
    body:has(.entity-2col--gallery) .scene-control-surface--header .scs-tab-edge,
    body:has(.entity-2col) .scene-control-surface--header:has(.aurora-header__scene-tabs) .scs-tab-edge {
        position: static;      /* in-flow flex item, not the old absolute line */
        top: auto; bottom: auto; left: auto;
        flex: 0 0 2px;
        align-self: stretch;
        width: 2px;                    /* 2px weight, matching the rest of the borders */
        display: grid;
        grid-template-rows: 100px 1fr 100px;   /* eyes region | gap | hair region */
        background: none;
        border: none;
        transform: translateX(3px);   /* nudge toward the tabs, leaving a ~3px pad off the
                                         buttons — matches the footer's button-to-frame gap */
    }
    body:has(.entity-2col--gallery) .scene-control-surface--header .scs-tab-edge::before,
    body:has(.entity-2col) .scene-control-surface--header:has(.aurora-header__scene-tabs) .scs-tab-edge::before {
        content: "";
        grid-row: 2;           /* only the middle (gap) row */
        background: rgba(255, 255, 255, 0.15);
    }
    /* Re-dock the tabs to the narrowed (72px) 2-column rail — shift left 24px (67→43) to
       track the rail edge that moved in with the width. Scoped to the 2-column rails only;
       chat's single-column rail keeps the tabs at 67. (.scs-tab covers eyes too.) */
    body:has(.entity-2col--gallery) .scene-control-surface--header .scs-tab,
    body:has(.entity-2col) .scene-control-surface--header:has(.aurora-header__scene-tabs) .scs-tab {
        left: 61px;   /* re-docked to the 90px rail edge (6px in from the original 96/67) */
    }
    /* Column 1 — the brand/user/deck strip keeps its vertical (column-reverse)
       internal layout. flex:1 1 0 + min-width:0 so it takes exactly the space
       left after the filter column instead of its content width (default
       min-width:auto let it refuse to shrink, so the brand bled into column 2).
       overflow:hidden clips any remainder to its own column. */
    body:has(.entity-2col--gallery) .scene-control-surface--header .aurora-header__strip {
        flex: 1 1 0;
        min-width: 0;
        overflow: hidden;
    }
    /* Column 2 — INCREMENTAL. Step 1: prove the column renders by giving it an
       explicit width + a visible separator line (the brand stays in column 1).
       Step 2: show the active-filter bubbles as a vertical strip in the styled
       pocket. The picker (select2) is HIDDEN for now — we'll only revisit it
       once the bubble strip is working. */
    body:has(.entity-2col--gallery) .aurora-header__filter {
        display: flex;
        flex: 0 0 38px;        /* EXPLICIT width — match the editor's face-tab column (38px)
                                  so the filter fills the same "button row" footprint; the
                                  strip (flex:1) absorbs the difference, rail stays 96px. */
        width: 38px;
        flex-direction: column;
        align-items: stretch;
        height: 100%;
        padding: 3px 1px;      /* 3px at BOTH short ends (top + bottom) so the field insets
                                  off the rail's top/bottom borders — gradient pulls through
                                  the ends and the field's own border stops being cut off.
                                  Insets (shorter), doesn't grow the field. */
        border-left: 1px solid rgba(255, 255, 255, 0.12);   /* the separator line */
    }
    /* Picker hidden for now — focus on mirroring the active filters first. */
    body:has(.entity-2col--gallery) .aurora-header__filter .gallery-search-bar > select,
    body:has(.entity-2col--gallery) .aurora-header__filter .gallery-search-bar > .select2-container {
        display: none;
    }
    /* The bubble pocket — the beveled "styled line" the bubbles live in, stood
       up to fill the column. Override the portrait grid + fixed 26px height. */
    body:has(.entity-2col--gallery) .aurora-header__filter .gallery-search-bar {
        display: block;
        width: 100%;
        height: 100%;
        padding: 0;
    }
    body:has(.entity-2col--gallery) .aurora-header__filter .gallery-search-tags-wrap {
        height: 100%;
        width: 100%;
        flex-direction: column;
        align-items: center;
        justify-content: flex-end;   /* bottom-aligned — portrait's left-align, rotated */
        padding: 0;              /* no padding — bubbles run to the field's top/
                                    bottom border, like portrait */
        border-radius: 0;        /* square the squeezed portrait corners — the column runs
                                    edge-to-edge up against the tabs in the rail */
        overflow-x: hidden;
        overflow-y: auto;
    }
    body:has(.entity-2col--gallery) .aurora-header__filter .gs-quality-tags {
        justify-content: flex-end;
    }
    /* Step 2: bubbles stack up the column, each rotated to read sideways. */
    body:has(.entity-2col--gallery) .aurora-header__filter .gs-quality-tags {
        flex-direction: column;
        align-items: center;
        width: auto;
        height: max-content;
    }
    /* Scoped one level deeper (.gs-quality-tags .builder-tag--no-color) so it
       cleanly out-ranks generation-settings.css's same-named rule WITHOUT
       !important — severs the landscape chip from the shared portrait styling.
       Padding maps to the PORTRAIT chip rotated: 1st value = UP the rail (the
       cap-to-text gap that keeps it a pill), 2nd = ACROSS (= portrait height /
       thickness). line-height:1 flattens the 1.2 line-box; font sets the
       thickness; radius auto-caps at half the across-width so the circle holds. */
    body:has(.entity-2col--gallery) .aurora-header__filter .gs-quality-tags .builder-tag--no-color {
        writing-mode: vertical-rl;
        transform: rotate(180deg);
        padding: 0.45rem 0.03rem;
        font-size: 0.84rem;
        line-height: 1;
        border-radius: 0.7rem;
    }
    /* Match the × to the label so both share one center (it was bigger, sat in
       its own zone — the "× high" misalignment). */
    body:has(.entity-2col--gallery) .aurora-header__filter .gs-quality-tags .builder-tag--no-color .tag-remove {
        font-size: 0.84rem;
        line-height: 1;
        margin: 0;
    }
    /* Widen the center's left gutter to clear the wider 2-column gallery rail. */
    .entity-2col--gallery {
        padding-left: 90px;   /* clear the 2-column gallery rail */
    }

    /* ── Scene/editor landscape: the header goes 2-COLUMN, like the gallery's
       strip|filter split above — portrait's 2 header ROWS become landscape's 2
       header COLUMNS: column 1 = the brand/user strip, column 2 = the FACE TABS
       (image/scene/memory/chat) stood up with vertical labels like the right
       rail's page tabs. These rules live HERE because this sheet loads LAST —
       fieldsets-base.css rules lose equal-specificity cascade ties to later
       sheets (.drawer-controls{display:flex} silently beat its display:none
       "hide", which is how one face button leaked in above the login). The tabs
       column gets an EXPLICIT width (the filter-column lesson above). ── */
    body:has(.entity-2col) .scene-control-surface--header:has(.aurora-header__scene-tabs) {
        width: 90px;   /* strip (~46) + face-tab col (38) + frame (2) — matched to the gallery rail. */
        display: flex;         /* rail is a flex ROW so the in-flow frame-edge column can
                                  sit at the carousel edge, insetting the face-tab buttons */
        flex-direction: row;
    }
    body:has(.entity-2col) .scene-control-surface--header:has(.aurora-header__scene-tabs) .control-panel {
        flex-direction: row;   /* two side-by-side columns: strip | face tabs */
        align-items: stretch;
        gap: 4px;
        flex: 1 1 0;           /* fill the rail, leaving the frame-edge column at the far right */
        min-width: 0;
    }
    body:has(.entity-2col) .scene-control-surface--header:has(.aurora-header__scene-tabs) .aurora-header__strip {
        flex: 1 1 0;      /* column 1 takes what's left of the rail... */
        min-width: 0;     /* ...and may actually shrink (same lesson as the gallery) */
        overflow: hidden;
    }
    body:has(.entity-2col) .aurora-header__scene-tabs {
        display: flex;
        /* column-reverse so bottom→top reads image, scene, memory, chat (DOM order),
           matching the natural progress order — plain column put image at the top. */
        flex-direction: column-reverse;
        flex: 0 0 38px;   /* EXPLICIT column width — content width would collapse.
                             38px ≈ the right rail's create/discover width, so the
                             six rail tabs read the same size. */
        width: 38px;
        height: 100%;
        gap: 4px;
    }
    body:has(.entity-2col) .aurora-header__scene-tabs .aurora-header__nav-btn {
        flex: 1 1 auto;   /* the 4 tabs share the column height */
        width: auto;
        height: auto;
        min-height: 0;
        flex-direction: column-reverse;   /* icon low, label above it reading up */
        gap: 0.3rem;
        padding: 0.3rem 0;
    }
    body:has(.entity-2col) .aurora-header__scene-tabs .aurora-header__nav-label {
        writing-mode: vertical-rl;
        transform: rotate(180deg);
        line-height: 1;
        white-space: nowrap;
    }
    /* Icons: rotated to match the vertical label orientation, same as the right
       rail's icons. Always shown — the room problem was the LABELS, not the
       icons (see the size fix below). */
    body:has(.entity-2col) .aurora-header__scene-tabs .aurora-header__nav-btn svg {
        transform: rotate(-90deg);
        flex: 0 0 auto;
    }
    /* Rail label size: the portrait shrink (0.82rem) keys on max-width:700, which
       landscape phones (w > 700) SKIP — so all six rail labels were rendering at
       the desktop 1rem. That's what squeezed the face-tab icons into dots. Size
       the labels for the rails; the icons fit alongside them again. */
    .entity-2col .scs-mobile-nav .aurora-header__nav-label,
    body:has(.entity-2col) .aurora-header__scene-tabs .aurora-header__nav-label {
        font-size: 0.78rem;
    }
    /* Widen the center's left gutter to clear the wider 2-column scene rail. */
    body:has(.scene-control-surface--header .aurora-header__scene-tabs) .entity-2col {
        padding-left: 90px;
    }
}