/* ═══════════════════════════════════════════════════════════════════
   CHAT INPUT - Fixed bar at bottom of card front (last card only)
   COLOR SYSTEM: All colors from --slot-N vars. --card-base is the
   single dark seed (inherited from parent or defined here).
   ═══════════════════════════════════════════════════════════════════ */

.card-chat-input {
    position: absolute;
    bottom: 4px;
    left: 4px;
    right: 4px;
    height: 36px;
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 0 6px;
    background: rgba(0, 0, 0, 0.75);
    backdrop-filter: blur(6px);
    border-radius: 0 0 8px 8px;
    z-index: 10;
}

.chat-message-input {
    /* Local accent so the inline mix below picks up the slot-1 hair color */
    --accent: var(--primary);
    flex: 1;
    background: rgba(0, 0, 0, 0.2);
    border: 1px solid rgba(255, 255, 255, 0.12);
    border-left: 2px solid color-mix(in srgb, var(--primary) 40%, transparent);
    border-radius: 4px;
    color: #c0c0c0;
    padding: 4px 8px;
    font-size: 12px;
    height: 26px;
    outline: none;
    min-width: 0;
    transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

.chat-message-input::placeholder {
    color: rgba(255, 255, 255, 0.3);
}

.chat-message-input:hover {
    border-color: rgba(255, 255, 255, 0.18);
    border-left-color: color-mix(in srgb, var(--primary) 60%, transparent);
}

.chat-message-input:focus {
    background: rgba(0, 0, 0, 0.4);
    border-color: var(--primary);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 25%, transparent);
}

.chat-message-input:disabled {
    opacity: 0.5;
}

/* Send button — 32px circle sized to match the audio buttons it
   replaced. Lives INSIDE the input pill at the same absolute
   position the audio button used to sit, so the in-pill space
   never reflows when the send appears. No elevation animation —
   the button just shows/hides via display when the input has /
   doesn't have text. */
.chat-send-btn {
    flex-shrink: 0;
    width: 32px;
    height: 32px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--neutral);
    border-radius: 50%;
    color: #fff;
    cursor: pointer;
    transition: background 0.2s ease;
}

.chat-send-btn svg {
    width: 16px;
    height: 16px;
}

.chat-send-btn:hover {
    background: white;
}

.chat-send-btn.disabled {
    opacity: 0.4;
    cursor: not-allowed;
    pointer-events: none;
}

/* ═══════════════════════════════════════════════════════════════════
   CHAT CAPTION - LLM response overlay on card front
   Sits just above the chat input bar, fades in with the new card.
   ═══════════════════════════════════════════════════════════════════ */

.card-chat-caption {
    position: absolute;
    bottom: 44px;  /* above the 36px chat input + 4px bottom + 4px gap */
    left: 4px;
    right: 4px;
    padding: 6px 10px;
    background: rgba(0, 0, 0, 0.7);
    backdrop-filter: blur(6px);
    border-radius: 6px;
    z-index: 10;
    animation: caption-fade-in 0.4s ease;
}

/* When no chat input present (non-last cards), caption sits at bottom */
.strip-card:not(:last-child) .card-chat-caption {
    bottom: 4px;
    border-radius: 0 0 8px 8px;
}

.chat-caption-text {
    display: block;
    font-size: 11px;
    font-style: italic;
    color: rgba(255, 255, 255, 0.85);
    line-height: 1.3;
}

.chat-caption-changes {
    display: flex;
    flex-wrap: wrap;
    gap: 3px;
    margin-top: 3px;
}

.chat-change-tag {
    background: color-mix(in srgb, var(--primary) 30%, rgba(0, 0, 0, 0.4));
    border: 1px solid color-mix(in srgb, var(--primary) 50%, transparent);
    border-radius: 3px;
    padding: 1px 5px;
    font-size: 9px;
    color: color-mix(in srgb, var(--primary) 85%, white);
    letter-spacing: 0.02em;
}

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

/* ═══════════════════════════════════════════════════════════════════
   CHAT RESPONSE - Card back (kept for debugging/detail view)
   ═══════════════════════════════════════════════════════════════════ */

.card-chat-response {
    margin: 0 0.5rem 0.5rem 0.5rem;
    padding: 0.4rem 0.5rem;
    font-size: 11px;
    color: rgba(255, 255, 255, 0.7);
    max-height: 50px;
    overflow-y: auto;
    background: linear-gradient(135deg,
        color-mix(in srgb, var(--primary) 12%, var(--card-base)) 0%,
        color-mix(in srgb, var(--secondary) 8%, var(--card-base)) 100%);
    border-radius: 6px;
    border: 1px solid rgba(255, 255, 255, 0.06);
    border-left: 2px solid transparent;
}

.chat-response-text {
    margin-bottom: 2px;
    font-style: italic;
}

.chat-changes-diff {
    display: flex;
    flex-wrap: wrap;
    gap: 3px;
}

/* ═══════════════════════════════════════════════════════════════════
   PAGE-LEVEL CHAT FIELDSET - Full conversation view
   ═══════════════════════════════════════════════════════════════════ */

.fieldset--chat {
    --accent: var(--secondary);
    background: linear-gradient(
        225deg,
        color-mix(in srgb, var(--secondary) 22%, var(--card-base)) 0%,
        color-mix(in srgb, var(--secondary) 10%, var(--card-base)) 40%,
        var(--card-base) 60%,
        var(--card-base) 100%
    );
    border-color: color-mix(in srgb, var(--secondary) 25%, rgba(255, 255, 255, 0.08));
}

/* Chat control deck during generation — surgical, not a wash.
   The recessed deck tray + every button's border/bg stay fully lit so
   the header keeps its life while a scene renders. Settings (opens the
   prompt drawer) and Overlay (client-side card toggle) never disable —
   neither touches the generation pipeline, and they give the deck
   something alive to look at during the wait. Tools / Think / Reset go
   inert: only the icon greys (the button frame stays put) and
   pointer-events:none eats the click. The opacity below is the tuning
   knob — currently icon-only, dial up toward a fuller fade if it reads
   too live. */
.generating .fieldset--chat :is(.chat-deck-btn--toggle, .chat-deck-btn--reset) {
    pointer-events: none;
}

.generating .fieldset--chat :is(.chat-deck-btn--toggle, .chat-deck-btn--reset) :is(svg, .chat-deck-btn-icon) {
    opacity: 0.3;
}

/* Prompt drawer stays fully lit during generation — it's reference the
   user reads while a scene renders, and always-live Settings opens it.
   Only the editable fields (system prompt + staff hist) go inert so a
   value can't change mid-pipeline; labels + drawer chrome keep their
   colour. opacity is the tuning knob. */
.generating .fieldset--chat .llm-params-drawer .drawer-prompt-textarea,
.generating .fieldset--chat .llm-params-drawer .llm-field {
    pointer-events: none;
    opacity: 0.55;
}

/* Dim each child of the input row individually instead of the row
   itself, so .chat-generate-btn can stay at full opacity (parent
   opacity clamps children — they can't be MORE opaque than parent). */
.generating .fieldset--chat .chat-form-input > *:not(.chat-generate-btn) {
    opacity: 0.35;
    pointer-events: none;
}

/* Card-face twins of the form-view lock/aspect orbs. The rule above is
   scoped to .fieldset--chat .chat-form-input, so it never reached the
   carousel card's own input row — the on-card lock + aspect stayed live
   during generation AND crystallize. Same lockout here, keyed on both
   events. The card chat input + elements-toggle stay live (type-through
   and peek-bubbles during the wait), matching the form-view exemptions. */
:is(.generating, .crystallizing, .prompt-locked) .card-front-chat-input-row
    :is(.card-front-aspect, .card-front-lock) {
    opacity: 0.35;
    pointer-events: none;
}

/* Dim/disable the per-bubble action row (play audio / regenerate /
   delete) during generation / crystallize. The bubbles themselves stay
   at full opacity so the user can read the conversation — and watch a
   live token stream — clearly; only the action buttons go inert so a
   stray click can't trigger a regenerate while the pipeline is busy.
   .crystallizing is the reset-time twin of .generating (set on
   #form-content by performReset). */
:is(.generating, .crystallizing, .prompt-locked) .chat-msg-actions {
    opacity: 0.35;
    pointer-events: none;
}

/* ═══════════════════════════════════════════════════════════════════
   CHAT CONTROL DECK - Inlaid 4-button transport bar in fieldset header.
   Mirrors .transport-bar--main / .action-deck (recessed black tray
   with tactile inset buttons), themed with slot-2 (chat accent).
   ═══════════════════════════════════════════════════════════════════ */

.fieldset--chat h2 {
    /* Pin the heading to align with the deck instead of stretching it */
    align-items: center;
    gap: 0.6rem;
}

/* Editor header is ONE L-shaped surface (6 corners): a header BAR across the top
   + the recessed WELL hanging below-left, sharing the same material and the same
   top-left origin so they read as a single surface that wraps the avatar. The
   avatar is recessed (cutout) into the left arm; the switcher + deck ride the
   bar. Bar and well are both anchored at top/left 0.6rem so they align exactly —
   no clip-path, no offset guessing. */
.fieldset--chat { position: relative; }
/* Top arm of the L — the header bar. Behind the h2 content, above the fieldset
   background; same 225deg slot material as the well so the join is seamless. */
.fieldset--chat:has(.chat-avatar-well)::before {
    content: '';
    position: absolute;
    top: 0.6rem;
    left: 0.6rem;
    right: 0.6rem;
    height: 3.4rem;
    z-index: 0;
    border-radius: 9px;
    background: linear-gradient(225deg,
        color-mix(in srgb, var(--secondary) 16%, var(--card-base)) 0%,
        color-mix(in srgb, var(--primary) 16%, var(--card-base)) 100%);
    box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.06),
        0 1px 3px rgba(0, 0, 0, 0.3);
    pointer-events: none;
}
/* Tall left arm of the L — the well (unchanged look), hanging below the bar with
   the avatar recessed into it. Sits ON the bar (higher z), same origin. */
.chat-avatar-well {
    position: absolute;
    top: 0.6rem;
    left: 0.6rem;
    z-index: 6;
    padding: 0.35rem;   /* the cutout gap around the thumb */
    border-radius: 9px;
    background: linear-gradient(225deg,
        color-mix(in srgb, var(--secondary) 16%, var(--card-base)) 0%,
        color-mix(in srgb, var(--primary) 16%, var(--card-base)) 100%);
    box-shadow:
        inset 0 2px 6px rgba(0, 0, 0, 0.45),
        inset 0 1px 0 rgba(0, 0, 0, 0.3),
        0 1px 0 rgba(255, 255, 255, 0.05);
}
/* Header content rides ON the bar (above the ::before), cleared past the well. */
.fieldset--chat:has(.chat-avatar-well) > h2 {
    position: relative;
    z-index: 1;
    min-height: 3.4rem;
    align-items: center;
    padding-left: 6.4rem;   /* clear the well (thumb 5rem + padding + inset) */
    padding-right: 0.6rem;  /* keep the deck within the bar's right edge */
}
.chat-avatar-well .chat-avatar-thumb {
    width: 5rem;   /* a touch bigger than 4.5rem */
    height: auto;    /* override the base .char-thumb height:100% (grid-row fill);
                        portrait height derives from width + --thumb-aspect: 2/3 */
    /* Strip the base .char-thumb frame — the WELL is the recess now; the thumb's
       own faces carry the card border below. */
    background: none;
    border: none;
    box-shadow: none;
}
.chat-avatar-thumb .flip-front,
.chat-avatar-thumb .flip-back {
    border-radius: 6px;
    overflow: hidden;
    /* EYES border — the site accent (matches the card borders / slot strips).
       One rule covers the Django view AND the card chat face (same fieldset). */
    border: 2px solid color-mix(in srgb, var(--secondary) 40%, rgba(255, 255, 255, 0.15));
    background: linear-gradient(135deg,
        color-mix(in srgb, var(--primary) 16%, var(--card-base)) 0%, var(--card-base) 50%);
}
.chat-avatar-thumb .flip-front img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* The rich character switcher IS the heading now (replaces "Chat"). Size it as
   a header element — grow to fill the space left of the control deck, capped —
   and override select2's inline width:100% so it doesn't shove the deck out.
   Its own .cast-rich-selected spans carry explicit sizes, so the h2's 0.9rem/600
   don't leak in. */
.fieldset--chat h2 > .cast-rich-switcher--heading + .select2-container--default {
    flex: 1 1 auto;
    min-width: 0;
    max-width: 15rem;
    width: auto !important;
}

/* Closed switcher = a CLEAN BOX. The big avatar in the well now carries the
   identity, so the closed selection drops its thumb and the slot accent (the
   3px accent left-border from the theme) and reads as a plain neutral box:
   name + a neutral detail. The dropdown OPTIONS (.cast-rich-option) are
   untouched — they keep their thumbs + per-character accents + double height. */
.fieldset--chat h2 .cast-rich-selected__thumb {
    display: none;   /* redundant with the big well avatar */
}
.fieldset--chat h2 .cast-rich-selected__detail {
    color: rgba(255, 255, 255, 0.45);   /* neutral — no slot-1 accent */
}
.fieldset--chat h2 .cast-rich-switcher--heading + .select2-container--default .select2-selection--single {
    border: 1px solid rgba(255, 255, 255, 0.14);
    border-left-width: 1px;                       /* kill the 3px slot accent bar */
    border-left-color: rgba(255, 255, 255, 0.14);
    background: rgba(0, 0, 0, 0.22);
}
.fieldset--chat h2 .cast-rich-switcher--heading + .select2-container--default .select2-selection--single:hover {
    border-color: rgba(255, 255, 255, 0.22);
    border-left-color: rgba(255, 255, 255, 0.22);
    background: rgba(0, 0, 0, 0.3);
}

.chat-control-deck {
    /* Auto-size around the buttons; push to the right of the <h2> */
    flex: 0 0 auto;
    margin-left: auto;
    display: flex;
    align-items: stretch;
    gap: 2px;
    padding: 3px;
    /* Softer recess than the deep black tray — mirrors .transport-housing
       in scene-control-surface so the deck reads as inset without going
       stark against the slot-2 fieldset background. */
    background: linear-gradient(180deg,
        color-mix(in srgb, var(--secondary) 4%, color-mix(in srgb, black 18%, var(--card-base))) 0%,
        color-mix(in srgb, var(--secondary) 4%, color-mix(in srgb, black 28%, var(--card-base))) 100%);
    border-radius: 7px;
    border: 1px solid rgba(255, 255, 255, 0.06);
    box-shadow:
        inset 0 2px 4px rgba(0, 0, 0, 0.35),
        0 1px 0 rgba(255, 255, 255, 0.04);
}

.chat-deck-btn {
    width: 36px;
    height: 32px;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 1px solid rgba(255, 255, 255, 0.15);
    border-radius: 4px;
    /* card-base underlay blocks the panel gradient from bleeding through
       the translucent overlay above. */
    background:
        linear-gradient(180deg,
            rgba(255, 255, 255, 0.06) 0%,
            rgba(0, 0, 0, 0.1) 100%),
        var(--card-base);
    color: rgba(255, 255, 255, 0.5);
    cursor: pointer;
    transition: all 0.12s ease;
    padding: 0;
    flex-shrink: 0;
    margin: 0;
    position: relative;
    font-family: inherit;
    font-size: inherit;
}

.chat-deck-btn:hover {
    background:
        linear-gradient(180deg,
            rgba(255, 255, 255, 0.1) 0%,
            rgba(0, 0, 0, 0.05) 100%),
        var(--card-base);
    /* Grey icon, not slot-2 — eye colors can be near-black and would vanish.
       Border keeps the accent (hover glow removed — crisp). */
    color: var(--ui-text-strong);
    border-color: color-mix(in srgb, var(--secondary) 30%, transparent);
}

.chat-deck-btn:active {
    transform: translateY(1px);
    background:
        linear-gradient(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.3)),
        var(--card-base);
    box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.4);
}

.chat-deck-btn.active,
.chat-deck-btn:has(.chat-deck-checkbox:checked) {
    background:
        linear-gradient(180deg,
            color-mix(in srgb, var(--secondary) 22%, rgba(255, 255, 255, 0.06)) 0%,
            color-mix(in srgb, var(--secondary) 12%, rgba(0, 0, 0, 0.1)) 100%),
        var(--card-base);
    border-color: color-mix(in srgb, var(--secondary) 45%, transparent);
    color: var(--ui-text-strong);
    box-shadow:
        0 0 10px color-mix(in srgb, var(--secondary) 30%, transparent),
        inset 0 0 4px color-mix(in srgb, var(--secondary) 18%, transparent);
}

.chat-deck-btn svg {
    width: 14px;
    height: 14px;
}

.chat-deck-btn-icon {
    font-size: 0.95rem;
    line-height: 1;
    display: inline-block;
}

/* Reset button — spin the icon on hover */
.chat-deck-btn--reset .chat-deck-btn-icon {
    transition: transform 0.3s ease;
}

.chat-deck-btn--reset:hover .chat-deck-btn-icon {
    transform: rotate(-180deg);
}

/* Toggle buttons wrap a hidden checkbox so clicks fire the existing
   .llm-toggle change handler that persists state. */
.chat-deck-btn--toggle {
    /* <label> default has user-select; deck buttons shouldn't select text */
    user-select: none;
}

.chat-deck-checkbox {
    position: absolute;
    opacity: 0;
    pointer-events: none;
    width: 0;
    height: 0;
    margin: 0;
}

/* Settings gear — gentle spin when drawer is open */
.chat-deck-btn--settings.active svg {
    animation: chat-deck-gear-spin 4s linear infinite;
}

@keyframes chat-deck-gear-spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
}

/* ═══════════════════════════════════════════════════════════════════
   LLM PARAMS DRAWER - Beveled housing for the gear-revealed controls.
   Mirrors .transport-housing — a recessed bay with the deck-style
   tray inside it. Slot-2 accent ties to the chat header deck.
   ═══════════════════════════════════════════════════════════════════ */

/* Drawer is a transparent frame — the trays inside provide all the surface.
   When open it just gives the chips room to breathe between header + chat. */
.llm-params-drawer,
.llm-voice-drawer {
    max-height: 0;
    overflow: hidden;
    transition: max-height 0.3s ease, margin 0.3s ease;
    margin-top: 0;
    margin-bottom: 0;
}

.llm-params-drawer.open {
    max-height: 520px;
    margin-top: 0.6rem;
    margin-bottom: 0.6rem;
}

/* Voice drawer follows tts_enabled — open when auto-narrate is on. */
.llm-voice-drawer.open {
    max-height: 200px;
    margin-top: 0.6rem;
    margin-bottom: 0.6rem;
}

/* ─── Tier / Usage drawer — separate from the prompt drawer above.
   Visible to all authenticated users on both editor + scene pages.
   Neutral grey so the surface reads as system info; colors can layer
   in later if we want tier-tinted variants. Auto-opens when the
   policy gate is hit; the info button pulses to draw the eye. ─── */
.tier-info-drawer {
    max-height: 0;
    overflow: hidden;
    transition: max-height 0.3s ease, margin 0.3s ease;
    margin-top: 0;
    margin-bottom: 0;
    /* In the fieldset's flex column the scrolling conversation would otherwise
       steal a few px from this drawer too, shrinking it below its content so
       overflow clips the block's bottom border + the CTA line. Pin it to its
       content height; only the conversation flexes. */
    flex-shrink: 0;
}
.tier-info-drawer.open {
    max-height: 340px;      /* was 200px — the 3-row grid + CTA was clipping */
    margin-top: 0.15rem;    /* was 0.6rem — sit close under the L header (~10px total) */
    margin-bottom: 0.3rem;
    padding-bottom: 5px;    /* room inside the overflow so the block's bottom
                               border/shadow isn't clipped; also nudges chat down */
}

.tier-info-block {
    /* Vertical stack: the data grid on top, the blocked CTA line below it. */
    display: flex;
    flex-direction: column;
    padding: 0.3rem 0.55rem 0.4rem;
    border-radius: 6px;
    font-size: 0.7rem;
    color: rgba(255, 255, 255, 0.55);
    /* Pure grey interface chrome; bevel + edge highlight per the field
       convention. */
    background: var(--card-base);
    border: 1px solid rgba(255, 255, 255, 0.06);
    box-shadow:
        inset 0 2px 4px rgba(0, 0, 0, 0.35),
        0 1px 0 rgba(255, 255, 255, 0.04);
}
/* Data grid — auto-fit columns: 3-across (2 rows) when wide, else 2-across
   (3 rows). Content-sized tracks (not 1fr) so the value sits close to its title
   instead of being flung to the far edge of a stretched track; the columns
   spread evenly to fill the box. */
.tier-info-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(5rem, max-content));
    justify-content: space-between;
    align-items: center;
    gap: 0.25rem 1.1rem;
}
/* Shift the whole dark box RIGHT past the avatar column (well right edge ≈
   6.3rem) so it reads beside the avatar, not behind it. */
.fieldset--chat:has(.chat-avatar-well) .tier-info-block {
    margin-left: 5.75rem;
}
/* THE ONE KNOB: the grid's min-height aligns the box's lower border with the
   avatar's bottom. When blocked, the CTA line below adds a line past it,
   growing the box under the avatar to seat the button. */
.fieldset--chat:has(.chat-avatar-well) .tier-info-grid {
    min-height: 4.5rem;
}
/* One cell: label LEFT (fixed width so values line up), value close after it. */
.tier-info-item {
    display: flex;
    align-items: baseline;
    gap: 0.45rem;
    min-width: 0;
}
/* Blocked line INSIDE the box: upgrade button (broken out left, under the
   avatar) + message (pushed right, in the box). Empty until both show. */
.tier-info-cta {
    display: flex;
    align-items: center;
    gap: 0.6rem;
}
.fieldset--chat:has(.chat-avatar-well) .tier-info-cta .tier-info-upgrade {
    margin-left: -6.125rem;  /* break out left, under the avatar (~6px further
                                left than the box shift — centered under the avatar) */
}
.tier-info-flag {
    margin-left: auto;       /* push the message to the right of the box */
    color: rgba(255, 255, 255, 0.75);
    font-weight: 600;
    letter-spacing: 0.02em;
}
.tier-info-label {
    min-width: 2.6rem;   /* ≈ the widest title (IMAGES/EDITOR) so values align */
    text-transform: uppercase;
    letter-spacing: 0.1em;
    font-size: 0.55rem;
    opacity: 0.6;
}
.tier-info-value {
    color: rgba(255, 255, 255, 0.9);
    font-weight: 600;
}
.tier-info-block--blocked {
    /* Same background as the unblocked state so the two are
       directly comparable — alert lift via the border only. */
    border-color: rgba(255, 255, 255, 0.18);
}

/* Persistent Upgrade CTA — shows only when the block class is on
   the parent so dismissing the HAL quota-blocked card still leaves
   a funnel touchpoint on the page. Slot-1 (hair) tint so it picks
   up the character's accent and reads as the only colored element
   in the otherwise-neutral drawer. */
.tier-info-upgrade {
    display: none;
    padding: 0.35rem 0.95rem;   /* a touch bigger (was 0.25rem 0.7rem) */
    border-radius: 999px;
    background: color-mix(in srgb, var(--primary) 28%, rgba(0, 0, 0, 0.35));
    border: 1px solid color-mix(in srgb, var(--primary) 55%, transparent);
    color: rgba(255, 255, 255, 0.92);
    font-size: 0.75rem;   /* a touch bigger (was 0.7rem) */
    font-weight: 600;
    letter-spacing: 0.04em;
    text-decoration: none;
    transition: background 0.15s ease, border-color 0.15s ease;
}
.tier-info-block--blocked .tier-info-cta .tier-info-upgrade {
    display: inline-block;
}
.tier-info-upgrade:hover {
    background: color-mix(in srgb, var(--primary) 45%, rgba(0, 0, 0, 0.25));
    border-color: color-mix(in srgb, var(--primary) 75%, transparent);
    color: #fff;
    text-decoration: none;
}

/* Info-button alert: gentle grey pulse when blocked. Neutral so we
   can swap to a tier-tinted color later without a code change. */
@keyframes tier-info-pulse {
    0%   { box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.35); }
    70%  { box-shadow: 0 0 0 6px rgba(255, 255, 255, 0); }
    100% { box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); }
}
.tier-info-btn--alert {
    animation: tier-info-pulse 1.8s ease-out infinite;
    color: rgba(255, 255, 255, 0.9);
}

/* ─── LLM PROMPT / VOICE BLOCK - Shared beveled surface matching the
   chat conversation pane below. Same slot-2/slot-1 gradient + inset
   bevel so every drawer reads as the same material as the conversation.
   Variants below add layout for prompt (tall, resizable) vs voice
   (flat row of controls). ─── */

.llm-prompt-block,
.llm-voice-block,
.field--bevel {
    padding: 0.5rem 0.6rem 0.6rem;
    border-radius: 6px;
    background: linear-gradient(
        225deg,
        color-mix(in srgb, var(--secondary) 16%, var(--card-base)) 0%,
        color-mix(in srgb, var(--primary) 16%, var(--card-base)) 100%
    );
    border: 1px solid rgba(255, 255, 255, 0.06);
    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);
}

/* Generic .field wrapper opting into the bevel surface — label stays
   inside the pane at the top, textarea fills below as transparent
   content. Used by Activity / Description in the character fieldset
   to match the system-prompt block above. */
.field.field--bevel label {
    padding-left: 0;
    margin-bottom: 0.35rem;
}

.field.field--bevel textarea {
    background: transparent;
    border: none;
    padding: 0;
    color: rgba(255, 255, 255, 0.88);
    font-size: 0.8rem;
    box-shadow: none;
}

.field.field--bevel textarea:hover,
.field.field--bevel textarea:focus {
    background: transparent;
    border: none;
    box-shadow: none;
}

.field.field--bevel textarea::placeholder {
    color: rgba(255, 255, 255, 0.3);
}

/* Per-field default heights. Generic .field textarea floors at
   min-height:60px (~3 lines here); Activity is effectively a one-liner
   so cap its reserved space at 2 lines (scrolls past that), and hand the
   freed room to the verbose Description. Bevel textareas have no padding,
   font-size 0.8rem, line-height 1.4 → 1 line = 1.4em. ID beats the
   generic .field textarea rule (no !important). resize:vertical is
   inherited, so either can still be dragged taller. */
#id_activity {
    min-height: 2.8em;   /* 2 lines */
}
#id_environment_description {
    min-height: 8.4em;   /* ~6 lines — Description carries the prose */
}

/* Prompt block — bevel is just the surface; the textarea inside owns
   its own resize. Gripper rides inside the block's padding so it stays
   grabbable on touch, matching .chat-conversation and the bevel
   .field--bevel textareas. */

/* Header row: SYSTEM PROMPT title left, optional Hist micro-field right. */
.llm-prompt-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    margin-bottom: 0.4rem;
}

.llm-prompt-header .drawer-label {
    margin: 0;
}

/* Inline Hist control — tiny labeled input that rides on the right of
   the header instead of claiming its own row. */
.llm-prompt-hist {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
}

.llm-prompt-hist > label {
    margin: 0;
    font-size: 0.55rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.5);
    transition: color 0.12s ease;
}

.llm-prompt-hist:focus-within > label {
    color: var(--secondary);
}

.llm-prompt-hist > input {
    width: 36px;
    height: 18px;
    padding: 0 4px;
    background: rgba(0, 0, 0, 0.45);
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 3px;
    color: color-mix(in srgb, var(--secondary) 60%, white);
    font-family: inherit;
    font-size: 0.7rem;
    text-align: center;
    outline: none;
    transition: border-color 0.15s ease, box-shadow 0.15s ease, color 0.15s ease;
}

.llm-prompt-hist > input:focus {
    border-color: color-mix(in srgb, var(--secondary) 55%, transparent);
    box-shadow: inset 0 0 6px color-mix(in srgb, var(--secondary) 20%, transparent);
}

/* Textarea sits inside the bevel as transparent content — the block
   IS the surface, so the field doesn't carry its own fill or border.
   Native resize: vertical from the base .drawer-prompt-textarea rule
   stays in effect so the gripper rides inside the block's padding. */
.llm-prompt-block .drawer-prompt-textarea {
    background: transparent;
    border: none;
    padding: 0;
    /* Match .builder-tag — same size/weight as the element bubbles
       below so the drawer reads as the same content surface. */
    font-size: 0.8rem;
    color: rgba(255, 255, 255, 0.88);
    box-shadow: none;
}

.llm-prompt-block .drawer-prompt-textarea:hover,
.llm-prompt-block .drawer-prompt-textarea:focus {
    background: transparent;
    border: none;
    box-shadow: none;
}

.llm-prompt-block .drawer-prompt-textarea::placeholder {
    color: rgba(255, 255, 255, 0.3);
}

/* ─── FIELD SPEAKER - tiny TTS button injected into .field--speakable
   boxes by field-speaker.js. Two layouts:
     1. Floating: absolute top-right of the bevel (Activity, Description,
        card-pane tabs).
     2. Header child: rides on the right of an existing flex header
        (System Prompt, Generation Settings prompt) — natural flex flow
        places it after Hist / seed counter.
   Icon mirrors .chat-msg-tts so all speaker affordances on the page
   read as the same kind of button. */
.field--speakable-floating {
    position: relative;
}

.field-speaker {
    background: transparent;
    border: none;
    color: rgba(255, 255, 255, 0.4);
    cursor: pointer;
    padding: 2px;
    border-radius: 4px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    line-height: 0;
    transition: color 0.15s ease, background 0.15s ease;
}

.field-speaker svg {
    width: 14px;
    height: 14px;
}

.field-speaker:hover {
    color: color-mix(in srgb, var(--secondary) 70%, white);
    background: rgba(255, 255, 255, 0.06);
}

.field-speaker.is-playing {
    color: var(--secondary);
}

/* Toggle speakers (Activity / Description) — persistent "armed" state:
   the box is woven into the show / chat narration. Brighter than the
   idle muted speaker, holds through hover. */
.field-speaker--on,
.field-speaker--on:hover {
    color: color-mix(in srgb, var(--secondary) 80%, white);
}

.field--speakable-floating > .field-speaker {
    position: absolute;
    top: 0.3rem;
    right: 0.4rem;
    z-index: 2;
}

/* Inside a flex header, the speaker hugs the title and pushes the
   existing right-side controls (Hist input, seed counter, crystallize
   spark) back to the right edge — overrides the header's default
   space-between spread. */
.llm-prompt-header > .field-speaker,
.gs-prompt-header > .field-speaker {
    margin-right: auto;
}

/* Mood row — label, then the mood control takes the lion's share, with
   the scene crystal pinned right so it sits in the same right-hand
   column as the Activity / Description floating speakers (right: 0.4rem).
   Activity / Description no longer use a header row: field-speaker.js
   floats their speaker top-right (.field--speakable-floating). */
/* .field is flex-direction:column by default (fieldsets-base). Keep the
   MOOD label on its own line above (left-aligned like every other field)
   via flex-wrap + a full-width label, then let the mood control + scene
   crystal share the row below it. Doubled class out-specifies plain
   .field regardless of stylesheet load order. */
.field.field--mood {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.4rem;
}
.field.field--mood > label {
    margin: 0;
    flex: 0 0 100%;
}
/* flex-basis:0 (not auto) so select2's inline width:100% doesn't claim
   the whole row and bump the crystal to its own line — the control
   sizes purely from the space left beside the fixed crystal. */
.field.field--mood > :not(label):not(.crystallize-spark) {
    flex: 1 1 0;
    min-width: 0;
}
.field.field--mood > .crystallize-spark {
    flex: 0 0 auto;
    margin-right: 0.4rem;
}

/* ─── LLM VOICE BLOCK - Voice select + speed slider side-by-side
   inside the same bevel as the prompt block. ─── */

.llm-voice-row {
    display: flex;
    gap: 0.6rem;
    align-items: stretch;
}

.llm-voice-field {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
}

.llm-voice-field .drawer-label {
    margin: 0;
}

/* Speed — thin centred inline row at the top of the voice drawer */
.llm-voice-speed {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    margin-bottom: 0.55rem;
}

.llm-voice-speed .drawer-label {
    margin: 0;
}

.llm-voice-speed .speed-slider {
    flex: 0 1 9rem;
    min-width: 0;
}

.llm-voice-speed .speed-value {
    font-size: 0.7rem;
    font-weight: 600;
    color: var(--secondary);
    white-space: nowrap;
    min-width: 2.5em;
    text-align: left;
}

/* Blend dropdown is greyed until the primary voice can blend */
.llm-voice-field.is-disabled {
    opacity: 0.4;
    pointer-events: none;
}

/* Per-section voice rows: Character / Narration / Activity / Description */
.llm-voice-section {
    flex: 0 0 5rem;
    align-self: center;
    font-size: 0.7rem;
    font-weight: 600;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    color: var(--secondary);
}

.llm-voice-row + .llm-voice-row {
    margin-top: 0.5rem;
}

/* Drawer prompt tabs — mini tab bar for Prompt / System */
.drawer-prompt-tabs {
    display: flex;
    gap: 0;
    margin-top: 0.3rem;
    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}

.drawer-prompt-tab {
    flex: 1;
    padding: 0.25rem 0.5rem;
    border: none;
    background: transparent;
    color: rgba(255, 255, 255, 0.35);
    font-size: 0.6rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    cursor: pointer;
    border-bottom: 2px solid transparent;
    transition: all 0.15s ease;
}

.drawer-prompt-tab:hover {
    color: rgba(255, 255, 255, 0.55);
}

.drawer-prompt-tab.active {
    color: var(--slot-12);
    border-bottom-color: var(--slot-12);
}

/* Drawer prompt panes — only active one visible */
.drawer-prompt-pane {
    display: none;
}

.drawer-prompt-pane.active {
    display: block;
}

/* Drawer label — slot-2 accent ties to the chat header deck */
.drawer-label {
    display: block;
    font-size: 0.55rem;
    font-weight: 700;
    letter-spacing: 0.12em;
    color: color-mix(in srgb, var(--secondary) 70%, white);
    text-transform: uppercase;
    margin-bottom: 0.35rem;
    margin-top: 0;
    padding-left: 0.2rem;
}

/* Shared textarea style for system prompt drawers — slot-2 accent */
.drawer-prompt-textarea {
    width: 100%;
    min-height: 100px;
    padding: 0.4rem 0.5rem;
    font-size: 0.75rem;
    line-height: 1.4;
    font-family: inherit;
    background: rgba(0, 0, 0, 0.4);
    border: 1px solid rgba(255, 255, 255, 0.12);
    border-left: 2px solid color-mix(in srgb, var(--secondary) 40%, transparent);
    border-radius: 4px;
    color: rgba(255, 255, 255, 0.8);
    outline: none;
    resize: vertical;
    box-sizing: border-box;
    transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

.drawer-prompt-textarea:hover {
    border-color: rgba(255, 255, 255, 0.18);
    border-left-color: color-mix(in srgb, var(--secondary) 60%, transparent);
}

.drawer-prompt-textarea:focus {
    border-color: var(--secondary);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--secondary) 25%, transparent);
}

.drawer-prompt-textarea[readonly] {
    color: rgba(255, 255, 255, 0.55);
}

.drawer-prompt-textarea::placeholder {
    color: rgba(255, 255, 255, 0.25);
}

.drawer-prompt-textarea.save-ok {
    border-color: rgba(100, 200, 100, 0.6);
    box-shadow: 0 0 4px rgba(100, 200, 100, 0.3);
    transition: border-color 0.3s, box-shadow 0.3s;
}

.drawer-prompt-textarea.save-fail {
    border-color: rgba(220, 80, 80, 0.6);
    box-shadow: 0 0 4px rgba(220, 80, 80, 0.3);
    transition: border-color 0.3s, box-shadow 0.3s;
}

.chat-conversation {
    /* Transparent content inside the bevel — wrap owns the surface +
       padding. Own height + resize keeps the gripper inside the wrap
       padding (touch-friendly), matching .field--bevel + .llm-prompt-block.
       A clip-path on .chat-elements (below) exposes this gripper through
       the bubble overlay so it stays grabbable in both modes. */
    height: 360px;
    min-height: 80px;
    overflow: auto;
    resize: vertical;
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    /* Delicate eyes-tinted scrollbar (matches the memory panel) — was OS grey. */
    scrollbar-width: thin;
    scrollbar-color: color-mix(in srgb, var(--secondary) 25%, rgba(255, 255, 255, 0.1)) transparent;
}
.chat-conversation::-webkit-scrollbar { width: 6px; }
.chat-conversation::-webkit-scrollbar-track { background: transparent; }
.chat-conversation::-webkit-scrollbar-thumb {
    background: color-mix(in srgb, var(--secondary) 25%, rgba(255, 255, 255, 0.1));
    border-radius: 3px;
}
.chat-conversation::-webkit-scrollbar-thumb:hover {
    background: color-mix(in srgb, var(--secondary) 45%, rgba(255, 255, 255, 0.15));
}

/* Push messages to the bottom — new turns appear at the bottom and the
   conversation scrolls upward as it grows. The :empty rule below
   replaces this spacer with the placeholder when there are no turns. */
.chat-conversation::before {
    content: '';
    flex: 1;
    min-height: 0;
}

.chat-conversation:empty::before {
    content: 'Start a conversation...';
    color: rgba(255, 255, 255, 0.25);
    font-style: italic;
    font-size: 0.8rem;
    align-self: center;
    margin-top: 2rem;
    flex: 0;
}

/* ─── Chat bubble tokens ──────────────────────────────────────────────
   The bubble shape (padding/radius/font/alignment) and the narration
   mix formula are shared across every chat surface. The palette differs
   by surface — form chat uses hair/eyes, card front uses hair/eyes
   (slightly darker for image-overlay legibility), card back uses
   tops/bottoms (body-mapping concept).

   Each surface sets only the tokens it overrides; defaults below
   produce the form-chat look. Tweak a value here once → form chat,
   card front, and card back all update. */
.chat-msg {
    padding: 0.5rem 0.85rem;
    /* Round enough that a single-line "hello" reads as a capsule, while
       multi-line content still looks like a pleasingly rounded rect. */
    border-radius: 18px;
    font-size: 1rem;
    line-height: 1.5;
    max-width: 85%;
    word-wrap: break-word;
}

/* The bubble that HOUSES the text is neutral grey — the character's slot
   colour lives in the surface behind it, not the fill (a brown-on-brown
   character turned the old slot-tinted bubble unreadable). Only a faint slot
   hint survives on the border edge, so user (hair) and assistant (eyes) still
   read apart without colouring the text's backing. Text stays bright for
   contrast on the grey. */
.chat-msg--user {
    align-self: flex-end;
    background: var(
        --chat-user-bg,
        color-mix(in srgb, white 4%, rgba(0, 0, 0, 0.6))
    );
    --accent: var(--chat-user-accent, var(--primary));
    color: var(--chat-user-color, rgba(255, 255, 255, 0.9));
    border: 1px solid var(
        --chat-user-border,
        color-mix(in srgb, var(--primary) 20%, rgba(255, 255, 255, 0.16))
    );
}

.chat-msg--assistant {
    align-self: flex-start;
    background: var(
        --chat-asst-bg,
        color-mix(in srgb, white 9%, rgba(0, 0, 0, 0.6))
    );
    --accent: var(--chat-asst-accent, var(--secondary));
    color: var(--chat-asst-color, rgba(255, 255, 255, 0.9));
    border: 1px solid var(
        --chat-asst-border,
        color-mix(in srgb, var(--secondary) 20%, rgba(255, 255, 255, 0.16))
    );
}

/* Speech-vs-narration collision fix: when the assistant accent is the
   same slot as the default narration tint, both end up eyes-toned and
   blur together. Form chat and card front paint assistant with eyes
   (slot-2), so flip narration to hair (slot-1) on those surfaces only.
   Card back's assistant uses tops (slot-3), already different from the
   eyes default, so it inherits the default and stays consistent. */
.fieldset--chat .chat-msg--assistant {
    --chat-narration-tint: var(--primary);
}

.chat-msg--changes {
    margin-top: 0.3rem;
    display: flex;
    flex-wrap: wrap;
    gap: 3px;
}

/* Markdown subset — see chat-generate.js renderChatMarkdown.
   Three character-voice tiers in the rendered output:
     em      — *narration* (action / aside): italic, slot-tinted grey.
               --chat-narration-tint defaults to slot-2 (eyes); the
               assistant bubble overrides to slot-1 (hair) so it
               doesn't collide with the eyes-painted bubble.
     strong  — **emphasis**: same color, just heavier weight. A colored
               bold competes with the narration tint, so we don't paint.
     speech  — "dialogue": picks up --accent (the bubble's own slot)
               at 65%, so spoken lines pop in the bubble's color while
               narration uses the OPPOSITE slot. User bubble: hair
               speech / eyes narration. Assistant: eyes speech / hair
               narration. Mix percentage tunable via --chat-speech-pct. */
.chat-msg em {
    font-style: italic;
    color: color-mix(
        in srgb,
        var(--chat-narration-tint, var(--secondary)) var(--chat-narration-pct, 30%),
        currentColor
    );
}
.chat-msg .chat-speech {
    color: color-mix(
        in srgb,
        var(--chat-speech-tint, var(--accent)) var(--chat-speech-pct, 65%),
        currentColor
    );
}
.chat-msg strong {
    font-weight: 600;
}

/* Per-bubble action row — sibling AFTER each chat-msg in the
   conversation flex column, NOT inside the bubble. Bubble stays
   compact; row is subtle by default, brightens on hover. */
/* Speaker label — rendered as a sibling BEFORE each .chat-msg so the
   reader sees who's talking. Assistant side uses the character's avatar
   (character_avatar filter); user side uses a slot-1-tinted initial
   circle (no avatar-upload model in Aurora). Mirrors .chat-msg's own
   align-self so the row sits above the bubble it labels. */
.chat-msg-speaker {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    margin: 0.5rem 0.3rem 0.1rem;
    font-size: 0.72rem;
    letter-spacing: 0.04em;
}
.chat-msg-speaker--assistant {
    align-self: flex-start;
    color: color-mix(in srgb, var(--secondary) 60%, rgba(255, 255, 255, 0.7));
}
.chat-msg-speaker--user {
    align-self: flex-end;
    color: color-mix(in srgb, var(--primary) 60%, rgba(255, 255, 255, 0.7));
}
.chat-msg-speaker-avatar {
    width: 22px;
    height: 22px;
    border-radius: 50%;
    object-fit: cover;
    border: 1px solid color-mix(in srgb, var(--secondary) 40%, transparent);
    background: rgba(0, 0, 0, 0.3);
    flex-shrink: 0;
}
.chat-msg-speaker-name {
    line-height: 1;
}

.chat-msg-actions {
    display: flex;
    gap: 0.3rem;
    margin: 0.15rem 0.3rem 0.4rem;
    opacity: 0.85;
    transition: opacity 0.12s;
}
.chat-msg-actions:hover { opacity: 1; }
/* A playing message keeps its row at full opacity so the lit speaker
   reads even when the mouse is elsewhere. */
.chat-msg-actions:has(.chat-msg-action.is-playing) { opacity: 1; }
.chat-msg-actions--user { align-self: flex-end; }
.chat-msg-actions--assistant { align-self: flex-start; }
.chat-msg-action {
    width: 1.4em;
    height: 1.4em;
    padding: 0;
    border-radius: 50%;
    background: color-mix(in srgb, var(--secondary) 16%, transparent);
    color: color-mix(in srgb, var(--secondary) 55%, rgba(255, 255, 255, 0.9));
    border: 1px solid color-mix(in srgb, var(--secondary) 45%, transparent);
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: background 0.15s, color 0.15s, border-color 0.15s, box-shadow 0.15s;
}
.chat-msg-action:hover {
    background: color-mix(in srgb, var(--secondary) 28%, transparent);
    color: color-mix(in srgb, var(--secondary) 75%, white);
    border-color: color-mix(in srgb, var(--secondary) 65%, transparent);
}
.chat-msg-action[disabled] {
    opacity: 0.55;
    cursor: not-allowed;
}
.chat-msg-action svg {
    width: 0.8em;
    height: 0.8em;
}
.chat-msg-action.is-playing,
.chat-msg-action.is-playing:hover {
    color: var(--secondary);
    background: color-mix(in srgb, var(--secondary) 38%, transparent);
    border-color: color-mix(in srgb, var(--secondary) 90%, transparent);
    box-shadow: 0 0 8px color-mix(in srgb, var(--secondary) 45%, transparent);
}

/* Live "playing" pulse — an animated glow so a speaking bubble is
   unmistakable at a glance, distinct from the static hover state.
   Unscoped so it applies on every chat surface (front/back/base); the
   scoped colour rules above don't set `animation`, so nothing overrides
   it. Only the TTS speaker gets .is-playing, so scope to it. */
@keyframes chat-msg-tts-pulse {
    0%, 100% { box-shadow: 0 0 5px color-mix(in srgb, var(--secondary) 40%, transparent); }
    50%      { box-shadow: 0 0 12px color-mix(in srgb, var(--secondary) 78%, transparent); }
}
.chat-msg-tts.is-playing {
    animation: chat-msg-tts-pulse 1.3s ease-in-out infinite;
}

/* Delete button — same default as speaker (slot-2 eyes); red-tinted
   hover as the destructive cue. */
.chat-msg-delete:hover {
    background: color-mix(in srgb, var(--danger) 30%, transparent);
    color: color-mix(in srgb, var(--danger) 70%, white);
    border-color: color-mix(in srgb, var(--danger) 60%, transparent);
}

/* Regenerate button — same default + hover as speaker. Spin while
   the request is in flight. */
@keyframes chat-msg-regen-spin {
    to { transform: rotate(360deg); }
}
.chat-msg-regen.is-regenerating svg {
    animation: chat-msg-regen-spin 1s linear infinite;
}

.chat-form-input {
    --accent: var(--secondary);
    display: flex;
    gap: 6px;
    align-items: center;
}

.chat-conversation-wrap {
    /* Bevel surface (slot-2 / slot-1 gradient + inset shadow). Houses
       the scrollable conversation in normal flow and the element-bubble
       overlay (absolute, inset:0). Padding keeps the conversation's
       resize gripper inside the bevel (touch-friendly) — same pattern
       as .llm-prompt-block + .field--bevel. */
    position: relative;
    padding: 0.5rem;
    margin-bottom: 0.75rem;
    /* Solid HAIR fill (was a 225° eyes→hair gradient) — matches the memory panel +
       the scene card's carved recessed bevels. The inset shadows below keep the
       "carved" read; the recessed-surface convention is hair, raised top surfaces eyes. */
    background: color-mix(in srgb, var(--primary) 16%, var(--card-base));
    border-radius: 6px;
    border: 1px solid rgba(255, 255, 255, 0.06);
    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);
}

/* Bare-crystal crystallize affordance — just the glyph, no button
   chrome. Shared by the overlay spark and the Activity/Description
   field sparks (next increment) so they read as one gesture. */
.crystallize-spark {
    border: none;
    background: none;
    padding: 0;
    margin: 0;
    line-height: 1;
    font-size: 0.95rem;
    cursor: pointer;
    opacity: 0.6;
    filter: saturate(0.85);
    transition: opacity 0.15s ease, transform 0.15s ease, filter 0.15s ease;
}
.crystallize-spark:hover {
    opacity: 1;
    filter: saturate(1.25) drop-shadow(0 0 4px rgba(255, 255, 255, 0.35));
    transform: scale(1.18);
}
.crystallize-spark:disabled {
    opacity: 0.3;
    cursor: default;
    transform: none;
    filter: saturate(0.4);
}

/* Follow-up nudges — on-demand 💡 trigger in the chat row + a chip row
   above the input. Click a chip → fills the input (you edit/send);
   ignore → take over. Bare-emoji trigger, matching the spark aesthetic. */
.chat-followups-btn {
    border: none;
    background: none;
    padding: 0;
    margin: 0;
    line-height: 1;
    font-size: 1rem;
    cursor: pointer;
    opacity: 0.6;
    transition: opacity 0.15s ease, transform 0.15s ease;
}
.chat-followups-btn:hover {
    opacity: 1;
    transform: scale(1.12);
}
.chat-followups-btn:disabled {
    opacity: 0.35;
    cursor: default;
    transform: none;
}
.chat-followups {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    padding: 0.4rem 0.2rem;
}
.chat-followups[hidden] {
    display: none;
}
.chat-followup-chip {
    text-align: left;
    color: inherit;
    cursor: pointer;
    font-size: 0.8rem;
    padding: 0.3rem 0.7rem;
    border-radius: 999px;
    border: 1px solid color-mix(in srgb, var(--primary) 30%, transparent);
    background: color-mix(in srgb, var(--secondary) 14%, var(--card-base));
    transition: background 0.15s ease;
}
.chat-followup-chip:hover {
    background: color-mix(in srgb, var(--secondary) 26%, var(--card-base));
}

/* Generate button shares the row's central slot with the chat pill —
   they're mutually exclusive. Pill owns the slot during chatting (overlay
   off, not generating). Generate takes over while the elements overlay
   is open (so the button's reachable from the bubble view) AND while a
   generation is in flight (the gauge spins in place, blocking new input
   until done). Same flex: 1 + 36px height as the pill so the swap is
   shape-stable. */
.chat-form-input .chat-generate-btn {
    display: none;
    flex: 1;
    height: 32px;
    padding: 0 0.7rem;
    border-radius: 999px;
    min-width: 0;
    min-height: 0;
    gap: 0.3rem;
}

/* Tank-tread ring's inset:-3px halos the button on the card front, but
   in the chat row it overshoots into the 6px flex gap and visually
   kisses the lock orb. Pull it inside the button bounds here. */
.chat-form-input .chat-generate-btn::before {
    inset: 0;
}

.chat-form-input .chat-generate-btn .generate-icon {
    font-size: 0.95rem;
}

/* Generate visible when overlay is on OR generating. */
.fieldset--chat:has(.chat-elements.visible) .chat-form-input .chat-generate-btn,
.chat-form-input .chat-generate-btn.generating {
    display: flex;
}

/* Pill + send hidden when overlay is on OR generating — generate owns
   the slot in those states. */
.fieldset--chat:has(.chat-elements.visible) .chat-form-input .chat-form-pill,
.fieldset--chat:has(.chat-elements.visible) .chat-form-input .chat-page-send,
.chat-form-input:has(.chat-generate-btn.generating) .chat-form-pill,
.chat-form-input:has(.chat-generate-btn.generating) .chat-page-send {
    display: none;
}


/* Inline aspect + face-lock orbs in the form-chat row — same chat-deck-btn
   skin (subtle border + dark gradient) as TTS, sized 30×30 to match. */
.chat-form-input .deck-orb {
    width: 32px;
    height: 32px;
    flex-shrink: 0;
    border-radius: 50%;
    background:
        linear-gradient(180deg,
            rgba(255, 255, 255, 0.06) 0%,
            rgba(0, 0, 0, 0.1) 100%),
        var(--card-base);
    border: 1px solid rgba(255, 255, 255, 0.15);
    box-shadow: none;
}

.chat-form-input .deck-orb svg {
    width: 18px;
    height: 18px;
}

.chat-form-input .deck-orb .orb-icon {
    color: rgba(255, 255, 255, 0.5);
    filter: none;
    display: flex;
    align-items: center;
    justify-content: center;
}

/* Suppress the .deck-orb spinning ring inside the form-chat row. */
.chat-form-input .deck-orb::before {
    display: none;
}

/* Aspect — flatten the data-ratio outfit-accent gradient variants. */
.chat-form-input .deck-orb--aspect,
.chat-form-input .deck-orb--aspect[data-ratio="portrait"],
.chat-form-input .deck-orb--aspect[data-ratio="landscape"],
.chat-form-input .deck-orb--aspect[data-ratio="square"] {
    background:
        linear-gradient(180deg,
            rgba(255, 255, 255, 0.06) 0%,
            rgba(0, 0, 0, 0.1) 100%),
        var(--card-base);
    border: 1px solid rgba(255, 255, 255, 0.15);
    box-shadow: none;
}

/* Aspect rect swap — show only the rect matching the current ratio. */
.chat-form-input .deck-orb--aspect .ratio-portrait,
.chat-form-input .deck-orb--aspect .ratio-landscape,
.chat-form-input .deck-orb--aspect .ratio-square {
    display: none;
}
.chat-form-input .deck-orb--aspect[data-ratio="portrait"] .ratio-portrait,
.chat-form-input .deck-orb--aspect[data-ratio="landscape"] .ratio-landscape,
.chat-form-input .deck-orb--aspect[data-ratio="square"] .ratio-square {
    display: inline;
}

/* Lock shackle swap — closed shackle visible only when .locked. */
.chat-form-input .deck-orb--lock .lock-shackle-closed {
    display: none;
}
.chat-form-input .deck-orb--lock.locked .lock-shackle-closed {
    display: inline;
}
.chat-form-input .deck-orb--lock.locked .lock-shackle-open {
    display: none;
}

/* Locked state — slot-2 tint + glow so the lock state is obvious at
   a glance even before reading the shackle. */
.chat-form-input .deck-orb--lock.locked {
    background:
        linear-gradient(180deg,
            color-mix(in srgb, var(--secondary) 22%, rgba(255, 255, 255, 0.06)) 0%,
            color-mix(in srgb, var(--secondary) 12%, rgba(0, 0, 0, 0.1)) 100%),
        var(--card-base);
    border-color: color-mix(in srgb, var(--secondary) 45%, transparent);
    box-shadow: 0 0 10px color-mix(in srgb, var(--secondary) 30%, transparent);
}

.chat-form-input .deck-orb--lock.locked .orb-icon {
    color: var(--ui-text-strong, rgba(255, 255, 255, 0.85));
}

/* Pill — visual capsule containing the chat icon (left), input field,
   and inline TTS (right). Bevel + recessed shadow + focus glow live
   here; the input itself is transparent inside. */
.chat-form-pill {
    flex: 1;
    position: relative;
    display: flex;
    align-items: center;
    background: rgba(0, 0, 0, 0.35);
    border: 1px solid rgba(255, 255, 255, 0.06);
    box-shadow:
        inset 0 2px 4px rgba(0, 0, 0, 0.35),
        0 1px 0 rgba(255, 255, 255, 0.04);
    border-radius: 999px;
    min-height: 36px;
    min-width: 0;
    transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

.chat-form-pill:focus-within {
    background: rgba(0, 0, 0, 0.45);
    border-color: color-mix(in srgb, var(--secondary) 35%, transparent);
    box-shadow:
        inset 0 2px 4px rgba(0, 0, 0, 0.4),
        0 0 0 2px color-mix(in srgb, var(--secondary) 22%, transparent);
}

.chat-form-input-icon {
    position: absolute;
    left: 0.65rem;
    top: 50%;
    transform: translateY(-50%);
    width: 16px;
    height: 16px;
    color: rgba(255, 255, 255, 0.5);
    pointer-events: none;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
}

.chat-form-input-icon svg {
    width: 100%;
    height: 100%;
    pointer-events: none;
}

/* Input is transparent inside the pill — the pill carries the bevel.
   Hover/focus rules also reset the inherited .chat-message-input:focus
   box-shadow rectangle so it doesn't leak through the rounded shape. */
.chat-form-input .chat-page-input,
.chat-form-input .chat-page-input:hover,
.chat-form-input .chat-page-input:focus {
    flex: 1;
    background: transparent;
    border: none;
    box-shadow: none;
    color: #c0c0c0;
    /* Left padding for the chat icon, right padding for the inline TTS. */
    padding: 0.4rem 2.6rem 0.4rem 2.2rem;
    font-size: 0.85rem;
    min-height: 36px;
    outline: none;
    min-width: 0;
}

/* Inline TTS pinned to the right of the pill — sized smaller than the
   send orb so it tucks neatly inside the 36px pill. Centered via
   margin-top: -16px (half its height) instead of translateY(-50%) so
   .chat-deck-btn:active's translateY(1px) press transform doesn't
   clobber the centering and drop the button half its height. */
.chat-form-tts {
    position: absolute;
    right: 2px;
    top: 50%;
    margin-top: -16px;
    width: 32px;
    height: 32px;
    border-radius: 50%;
    flex-shrink: 0;
    z-index: 2;
}

.chat-form-tts svg {
    width: 16px;
    height: 16px;
}

.chat-form-input .chat-page-send {
    width: 32px;
    height: 32px;
    flex-shrink: 0;
    border-radius: 50%;
    background: linear-gradient(180deg,
        color-mix(in srgb, var(--secondary) 30%, var(--card-base)) 0%,
        color-mix(in srgb, var(--secondary) 20%, var(--card-base)) 100%);
    border: 1px solid color-mix(in srgb, var(--secondary) 40%, transparent);
}

.chat-form-input .chat-page-send:hover {
    background: linear-gradient(180deg,
        color-mix(in srgb, var(--secondary) 45%, var(--card-base)) 0%,
        color-mix(in srgb, var(--secondary) 35%, var(--card-base)) 100%);
    color: white;
}

.chat-form-input .chat-page-send svg {
    width: 14px;
    height: 14px;
}

/* In-pill positioning: send button sits absolute at the right edge
   of .chat-form-pill, in the same slot the inline audio button used
   to occupy. Hidden until the user types one character (sibling
   combinator on :placeholder-shown). */
.chat-form-pill {
    position: relative;
}
.chat-form-pill .chat-page-send {
    position: absolute;
    right: 2px;
    top: 50%;
    transform: translateY(-50%);
    z-index: 2;
}
.chat-form-pill:has(.chat-page-input:placeholder-shown) .chat-page-send {
    display: none;
}

/* ═══════════════════════════════════════════════════════════════════
   SPINNER STATE
   ═══════════════════════════════════════════════════════════════════ */

.chat-send-btn.loading {
    pointer-events: none;
}

.chat-send-btn.loading svg {
    display: none;
}

.chat-send-btn.loading::after {
    content: '';
    width: 12px;
    height: 12px;
    border: 2px solid rgba(255, 255, 255, 0.3);
    border-top-color: #fff;
    border-radius: 50%;
    animation: chat-spin 0.6s linear infinite;
}

@keyframes chat-spin {
    to { transform: rotate(360deg); }
}

/* ═══════════════════════════════════════════════════════════════════
   CHAT LOCKED - Model is responding, block all inputs immediately.
   Applied to carousel — new cards inherit this via CSS, no gap.
   ═══════════════════════════════════════════════════════════════════ */

.chat-locked .chat-message-input {
    pointer-events: none;
    opacity: 0.5;
}

.chat-locked .chat-send-btn {
    pointer-events: none;
    opacity: 0.4;
}

/* ─── Voice volume popover ───────────────────────────────────────────
   Pops from a bubble speaker when audio is toggled ON (scene-tts.js
   showVolumePopover): a simple live volume slider that auto-hides after
   3s. Body-appended + fixed-positioned, so it reads the global tokens
   and clears any scroll/overflow container. */
.tts-volume-popover {
    /* One eyes accent drives the whole popover — surface tint, border,
       glow, and the slider — so it reads as a single object instead of
       black-box + white-track + colored-thumb. Set inline from the active
       --slot-2/eyes (scene-tts.js); this neutral is the default when no slot
       color is available (a custom-prop default, NOT a var() fallback). */
    --vol-eyes: rgba(255, 255, 255, 0.6);
    position: fixed;
    z-index: 1000;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 7px 6px;
    border-radius: 9px;
    /* Dark surface tinted toward eyes, not stark black. */
    background: color-mix(in srgb, var(--vol-eyes) 14%, var(--card-base));
    border: 1px solid color-mix(in srgb, var(--vol-eyes) 60%, transparent);
    box-shadow:
        0 6px 18px rgba(0, 0, 0, 0.45),
        0 0 10px color-mix(in srgb, var(--vol-eyes) 22%, transparent);
    animation: tts-vol-in 0.12s ease-out;
}
@keyframes tts-vol-in {
    from { opacity: 0; transform: translateY(4px); }
    to   { opacity: 1; transform: translateY(0); }
}
/* Fully custom eyes slider — no native track (that's the bright groove we
   were killing). Vertical: min at bottom, max at top (direction:rtl flips
   the default top-is-min). The track is a centered eyes channel drawn on the
   control itself; the native runnable-track is cleared to transparent. */
.tts-volume-slider {
    -webkit-appearance: none;
    appearance: none;
    writing-mode: vertical-lr;
    direction: rtl;
    width: 14px;
    height: 82px;
    cursor: pointer;
    background: linear-gradient(to right,
        transparent calc(50% - 2.5px),
        color-mix(in srgb, var(--vol-eyes) 30%, rgba(0, 0, 0, 0.5)) calc(50% - 2.5px),
        color-mix(in srgb, var(--vol-eyes) 30%, rgba(0, 0, 0, 0.5)) calc(50% + 2.5px),
        transparent calc(50% + 2.5px));
    border-radius: 3px;
}
.tts-volume-slider::-webkit-slider-runnable-track { background: transparent; }
.tts-volume-slider::-moz-range-track { background: transparent; width: 5px; }
.tts-volume-slider::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 13px;
    height: 13px;
    border-radius: 50%;
    background: var(--vol-eyes);
    box-shadow: 0 0 4px color-mix(in srgb, var(--vol-eyes) 55%, transparent);
    cursor: pointer;
}
.tts-volume-slider::-moz-range-thumb {
    width: 13px;
    height: 13px;
    border: none;
    border-radius: 50%;
    background: var(--vol-eyes);
    box-shadow: 0 0 4px color-mix(in srgb, var(--vol-eyes) 55%, transparent);
    cursor: pointer;
}
