/* ---------- Reset & Base ---------- */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

/* Opt in to cross-document View Transitions for every same-origin nav. The
   root slide keyframes live further down (search ::view-transition-old).
   Browsers without support (Firefox today) fall back to a plain hard nav. */
@view-transition { navigation: auto; }

:root {
    --bg: #0f1117;
    --bg-card: #1a1d27;
    --bg-input: #252830;
    --bg-hover: #2a2d38;
    --border: #333640;
    --text: #e4e4e7;
    --text-dim: #9ca3af;
    --accent: #83956f;
    --accent-hover: #9daf89;
    --danger: #ef4444;
    --danger-hover: #f87171;
    --success: #22c55e;
    --warning: #f59e0b;
    --radius: 8px;
    --radius-lg: 12px;
}

body {
    background: var(--bg);
    color: var(--text);
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    font-size: 14px;
    line-height: 1.5;
    min-height: 100vh;
    /* Lock out body-level horizontal scrolling. Page-level horizontal scroll
       isn't useful anywhere and would compete with the project page's own
       .unified-layout overflow-x: auto (legitimate drawer-overflow scroll).
       overflow-x: clip is preferred over hidden — it doesn't establish a
       containing block for fixed/sticky elements and doesn't break VT. */
    overflow-x: clip;
    /* Disable the browser's trackpad-swipe-back gesture. The brand link is
       the only intentional path back to the home page; preventing the swipe
       avoids accidental history navigation that would also fire a VT slide. */
    overscroll-behavior-x: none;
}
html {
    /* Some browsers (Safari) require the property on the root element rather
       than body for the swipe-back disable to take effect. */
    overscroll-behavior-x: none;
}

a { color: var(--accent); text-decoration: none; }
a:hover { color: var(--accent-hover); }

/* ---------- Nav ---------- */
.top-nav {
    display: flex;
    align-items: center;
    justify-content: space-between;
    /* Left padding tuned so the left edge of the brush-CREATE logo in the
       brand sits on the same x as the horizontal centre of the ✦ agent
       glyph below. */
    padding: 12px 24px 12px 22px;
    background: var(--bg-card);
    border-bottom: 1px solid var(--border);
}
.nav-brand-group {
    display: flex;
    align-items: center;
    gap: 8px;
    /* flex-shrink: 0 keeps the brand cluster at its natural content width.
       Earlier `min-width: 0` allowed it to shrink past content (so the
       breadcrumb could truncate), but it ALSO let the .top-nav's flex
       distribution swallow the margin-right gap on narrow viewports —
       the cluster's "right edge" was moving left under shrinkage. With
       no shrinking, the margin-right is honoured and at very narrow
       widths the nav row overflows horizontally instead. */
    flex-shrink: 0;
    /* Guaranteed minimum gap between this cluster's right edge and the
       nav-links cluster (containing Tools / username / Admin / Logout). */
    margin-right: 12px;
}
.nav-brand {
    font-size: 16px;
    font-weight: 700;
    color: var(--accent);
    white-space: nowrap;
    display: inline-flex;
    align-items: center;
    gap: 7px;
    text-decoration: none;
}
.nav-brand:hover { color: var(--accent-hover); }
/* Static variant on the home page — the brand is rendered as a span (not a
   link) to avoid same-page nav triggering an in-place VT slide. Disable
   the hover affordances so it doesn't read as clickable. */
.nav-brand-static { cursor: default; }
.nav-brand-static:hover { color: var(--accent); }
.nav-brand-static:hover .nav-brand-logo { background-color: var(--accent); }
.nav-brand-logo {
    /* Rendered as a CSS-masked block: the PNG's alpha channel is the mask,
       and the visible fill is background-color. That means the logo
       inherits the same accent / accent-hover shift as the " by New
       Forest" text on hover — no approximate filter needed.
       Source 388x120 (~3.23:1). 26x84 px box. Negative vertical margin
       absorbs the brush bleed above/below the CREATE cap height so the
       nav height isn't stretched. */
    display: block;
    width: 84px;
    height: 26px;
    margin: -5px 0;
    background-color: var(--accent);
    -webkit-mask: url("create_logo.png") no-repeat center / contain;
    mask: url("create_logo.png") no-repeat center / contain;
    transition: background-color 0.12s ease;
}
.nav-brand:hover .nav-brand-logo { background-color: var(--accent-hover); }
.nav-brand-rest {
    position: relative;
    top: -1px;
    /* Defensive: belt-and-braces against the brand text wrapping when the
       parent flex container shrinks aggressively. .nav-brand already sets
       white-space: nowrap but a child span carries its own context. */
    white-space: nowrap;
}
.nav-sep { color: var(--text-dim); font-size: 16px; font-weight: 700; position: relative; top: -1px; }
.nav-crumb {
    font-size: 16px; font-weight: 700; color: var(--text-dim);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 320px;
    position: relative; top: -1px;
}
a.nav-crumb:hover { color: var(--text); }
.nav-crumb-active { color: var(--accent); }
.nav-links {
    display: flex;
    gap: 16px;
    align-items: center;
    /* flex-shrink: 0 keeps the right-side cluster intact at narrow
       viewports. Combined with .nav-brand-group's flex-shrink: 0, this
       means at very narrow widths the .top-nav row overflows
       horizontally rather than letting either cluster compress and
       collide with the other. */
    flex-shrink: 0;
}
/* Right-side navigation links (Admin, Logout). 13px so they sit at the
   same visual weight as the username + match the action button (TOOLS)
   in their respective tier — TOOLS is a UI-state toggle (bordered/
   uppercase/11px button), Admin + Logout are navigations (plain links). */
.nav-links a {
    color: var(--text-dim);
    font-size: 13px;
    padding: 4px 12px;
    border-radius: var(--radius);
    transition: all 0.15s;
}
.nav-links a:hover { color: var(--text); background: var(--bg-hover); }
.nav-links a.active { color: var(--text); background: var(--bg-input); }
.nav-user {
    color: var(--text);
    font-size: 13px;
    padding: 4px 10px;
    background: var(--bg-input);
    border-radius: var(--radius);
    font-weight: 500;
    /* Display names like "New Forest" (two words) would wrap to a second
       line on narrow viewports without this. The pill stays a single
       intact unit; the nav row scrolls / overflows instead. */
    white-space: nowrap;
    flex-shrink: 0;
}

/* Per-drawer hide. Set by either the user (rail − button or Drawers
   dropdown) or the agent (set_drawer_visibility tool); AGENT itself is
   never hideable. The matching left-adjacent .panel-resizer is hidden
   too so the surviving layout closes flush without a 4px gap. State
   persists in localStorage under vc.drawers.<project_id>[<name>].hidden
   and is applied by initDrawerPanels() in app.js. */
.drawer-panel[data-agent-hidden="true"] { display: none !important; }
.panel-resizer[data-agent-hidden="true"] { display: none !important; }

/* Empty-layout easter egg — when every drawer is hidden, the layout
   collapses to nothing. body.layout-all-hidden flips the overlay in:
   a centred 640px composer with "What's up?" placeholder, mirroring the
   home page's create tile. Send un-hides + expands AGENT and forwards
   the message into the agent chat (so the agent can bring drawers back
   via tools). Logic in app.js initEmptyLayoutComposer. */
.layout-empty-overlay {
    position: fixed;
    top: 56px;          /* below .top-nav */
    left: 0; right: 0; bottom: 0;
    background: var(--bg, #0f0f0f);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0 24px;
    pointer-events: none;
    opacity: 0;
    visibility: hidden;
    z-index: 50;
    transition: opacity 0.22s ease-out, visibility 0s linear 0.22s;
}
body.layout-all-hidden .layout-empty-overlay {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transition: opacity 0.22s ease-out, visibility 0s linear 0s;
}
.empty-composer-tile {
    position: relative;
    width: 640px;
    max-width: 100%;
    min-height: 108px;
    background: var(--bg-input);
    border: 1px solid var(--accent);
    border-radius: var(--radius);
    padding: 10px 12px 44px 12px;
    display: flex;
    flex-direction: column;
}
.empty-composer-editor {
    flex: 1 1 auto;
    color: var(--text);
    font-size: 13px;
    line-height: 1.5;
    min-height: 54px;
    outline: none;
    overflow-y: auto;
    white-space: pre-wrap;
    word-break: break-word;
    cursor: text;
}
.empty-composer-editor.is-empty::before {
    content: attr(data-placeholder);
    color: var(--text-dim);
    pointer-events: none;
}
.empty-composer-upload {
    position: absolute;
    left: 8px; bottom: 8px;
    width: 30px; height: 30px;
    background: transparent;
    color: var(--text-dim);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 0;
    cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.empty-composer-upload::before {
    content: "+";
    display: block;
    font: 400 18px/1 -apple-system, "system-ui", "Segoe UI", Roboto, sans-serif;
    transform: translateY(-1px);
}
.empty-composer-upload:hover {
    color: var(--accent);
    border-color: var(--accent);
    background: rgba(160, 182, 140, 0.08);
}
.empty-composer-send {
    position: absolute;
    right: 8px; bottom: 8px;
    width: 30px; height: 30px;
    background: transparent;
    color: var(--text-dim);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 0;
    cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    font: 400 18px/1 -apple-system, "system-ui", "Segoe UI", Roboto, sans-serif;
    transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.empty-composer-send:hover {
    color: var(--accent);
    border-color: var(--accent);
}
.empty-composer-send.is-ready {
    background: var(--accent);
    color: white;
    border: 1px solid transparent;
}
.empty-composer-send.is-ready:hover {
    background: var(--accent-hover);
    color: white;
}
/* Hide layout chrome while overlay is up — the page should feel like a
   reset, not a half-rendered project. */
body.layout-all-hidden .unified-layout { visibility: hidden; }

/* Drawers dropdown — opens from the top-nav button. Per-row +/- toggles
   show/hide each drawer in place. Logic in app.js initDrawersDropdown. */
.nav-drawers-wrap {
    position: relative;
    display: inline-block;
}
.nav-drawers-toggle {
    font-family: inherit;
    font-size: 11px;
    letter-spacing: 0.5px;
    text-transform: uppercase;
    padding: 4px 10px;
    border-radius: var(--radius);
    border: 1px solid var(--border);
    cursor: pointer;
    transition: background 0.15s, color 0.15s, border-color 0.15s;
    background: transparent;
    color: var(--text-dim);
}
.nav-drawers-toggle:hover {
    background: var(--bg-hover);
    color: var(--text);
}
.nav-drawers-toggle[aria-expanded="true"] {
    background: rgba(141, 166, 93, 0.12);
    color: var(--accent);
    border-color: var(--accent);
}
.nav-drawers-popup {
    /* Fixed positioning lifts the popup out of the nav's stacking context so
       it can't be hidden behind the .unified-layout drawers below. JS sets
       top + right via getBoundingClientRect each open. */
    position: fixed;
    /* Width sized for the worst case: AGENT row showing a 20-char custom
       name (cap enforced by `set_agent_name`). The .drawer-title font has
       letter-spacing 3px + uppercase, so 20 chars × ~12px = ~240px just for
       the title text — plus glyph + Demo badge + toggle + paddings. */
    min-width: 320px;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: 0 6px 24px rgba(0,0,0,0.38);
    padding: 6px 0;
    z-index: 10000;
}
.nav-drawers-popup.hidden { display: none; }
.nav-drawers-row {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 10px 10px 14px;
    user-select: none;
    cursor: pointer;
    /* Faint divider between rows so the rails read as a discrete list. */
    border-top: 1px solid var(--border);
    transition: background 0.12s;
}
.nav-drawers-row:first-child { border-top: none; }
.nav-drawers-row:hover { background: var(--bg-hover); }
/* Stronger divider for the boundary between visible and hidden groups —
   border-only so toggling rows doesn't shift the popup's height. */
.nav-drawers-row.is-first-hidden {
    border-top-color: rgba(255,255,255,0.22);
}
/* Hidden rails read dim in the dropdown so visible vs hidden is obvious
   at a glance even before the +/− glyph registers. Affects glyph + title +
   any cloned badge; the toggle button keeps full contrast so the click
   target stays legible. */
.nav-drawers-row.is-hidden .drawer-glyph-header,
.nav-drawers-row.is-hidden .drawer-title,
.nav-drawers-row.is-hidden .drawer-model { opacity: 0.45; }
/* Inside a row, the .drawer-title (cloned class from the drawer header)
   should fill the remaining space, pushing the toggle to the right edge.
   Demo badge (if present) sits between title and toggle, hugging the
   toggle. */
.nav-drawers-row .drawer-title { flex: 1; }
.nav-drawers-row .drawer-glyph-header { line-height: 1; }
.nav-drawers-row .drawer-model { flex-shrink: 0; }
/* Per-row +/- toggle. Same shape language as .agent-upload-btn (the + in
   the composer) and .drawer-rail-hide-btn (the − at the bottom of a rail),
   scaled to 22×22 to fit the dropdown row. Glyph swapped by JS via
   _refreshToggle: visible drawer → −, hidden drawer → +. */
.nav-drawers-row-toggle {
    width: 22px; height: 22px;
    background: transparent;
    color: var(--text-dim);
    border: 1px solid var(--border);
    border-radius: 5px;
    padding: 0;
    cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    font: 400 14px/1 -apple-system, "system-ui", "Segoe UI", Roboto, sans-serif;
    transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.nav-drawers-row-toggle:hover {
    color: var(--accent);
    border-color: var(--accent);
    background: rgba(160, 182, 140, 0.08);
}
.nav-drawers-row-toggle:disabled {
    cursor: default;
    opacity: 0.4;
}
.nav-drawers-row-toggle:disabled:hover {
    color: var(--text-dim);
    border-color: var(--border);
    background: transparent;
}

/* ---------- Home (projects list) ----------
   Tile stack that visually mirrors the agent composer's input shell:
   same bg-input fill, same border, same radius. Click a project tile to
   open it; click the top tile to create a new project. Phase 2 will morph
   the Create tile in-place into the agent composer. */
.home-page {
    max-width: 640px;
    margin: 56px auto 80px;
    padding: 0 20px;
}
.home-stack {
    display: flex;
    flex-direction: column;
    gap: 12px;
}
.home-tile {
    /* Shared composer-shell — matches .agent-input-wrap exactly so the
       Phase 2 morph is geometry-free (same radius, border, bg). */
    display: flex;
    align-items: center;
    gap: 16px;
    padding: 18px 20px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text);
    text-decoration: none;
    font-size: 14px;
    line-height: 1.4;
    min-height: 60px;
    box-sizing: border-box;
    cursor: pointer;
    transition: border-color 0.14s ease-out, background 0.14s ease-out,
                transform 0.14s ease-out;
    /* Reset button defaults when .home-tile lands on a <button>. */
    text-align: left;
    font-family: inherit;
    width: 100%;
}
.home-tile:hover {
    border-color: var(--accent);
    background: var(--bg-hover);
}
.home-tile:active {
    transform: scale(0.996);
}
.home-tile-name {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    /* Matches .nav-crumb (16px / 700) so the project name carries the same
       weight on the home tile and in the breadcrumb once the project opens. */
    font-size: 16px;
    font-weight: 700;
    line-height: 1;
    color: var(--text);
}
.home-tile-meta {
    flex: 0 0 auto;
    color: var(--text-dim);
    font-size: 12px;
    line-height: 1;
    letter-spacing: 0.2px;
    /* Flex centring lands the line-box geometric midline on the tile midline,
       but the text's optical midline sits slightly above that — so next to
       the × (which was nudged up 1px to hit the true tile midline), the date
       reads high. Push down 1px to match. */
    position: relative;
    top: 1px;
}

/* Create tile — lives at the top. Subtle accent tone + a + glyph on the
   right where the date sits on project tiles. */
.home-tile-create {
    background: var(--bg-input);
    border-color: var(--border);
    color: var(--text);
}
.home-tile-create .home-tile-name {
    color: var(--accent);
    font-weight: 500;
}
/* Agent star on the Create tile — says "agent is here." At rest, olive
   at ~70% opacity. On hover, gentle pulse matches the AGENT drawer's
   thinking-indicator cadence: "ready to think as soon as you click." */
.home-tile-create-glyph {
    flex: 0 0 auto;
    color: var(--accent);
    font-size: 20px;
    line-height: 1;
    opacity: 0.7;
    transition: opacity 0.14s ease-out;
    /* Same -1px optical nudge as the × on project tiles — the ✦ sits on its
       x-height midline by default, which reads low next to the tile's visual
       centre. position:top keeps transform: scale free for the hover pulse. */
    position: relative;
    top: -1px;
    /* Give the scale transform a stable origin so the pulse stays in place. */
    transform-origin: center center;
    will-change: transform, opacity;
}
.home-tile-create:hover .home-tile-create-glyph {
    opacity: 1;
    animation: home-star-pulse 1.2s ease-in-out infinite;
}
@keyframes home-star-pulse {
    0%, 100% { transform: scale(1); }
    50%      { transform: scale(1.08); }
}

/* ---------- Create tile → composer morph ----------
   Normal state: the tile is a ~60px row showing "Create something new" + ✦.
   On click, `.is-composing` is added to the tile + `.home-stack.is-composing`
   to the parent. The rest layer cross-fades to the composer layer, the tile
   grows to composer height, and sibling project tiles fade out / collapse.
   Same DOM element, same border/radius/background — geometry is seamless. */
.home-tile-create {
    /* Override the base flex so the two content layers (rest + composer) can
       stack absolutely-positioned when composing. In rest state only
       .home-tile-create-rest is visible and takes the full tile. */
    position: relative;
    display: block;
    padding: 0;
    /* Same base min-height as .home-tile (60) — height grows via .is-composing. */
    transition: min-height 0.28s cubic-bezier(0.2, 0.8, 0.2, 1);
    will-change: min-height;
}
/* Resting layer: the original "Create something new" + ✦ row. Inherit the
   composer shell's horizontal padding so its look is unchanged from Phase 1. */
.home-tile-create-rest {
    display: flex;
    align-items: center;
    gap: 16px;
    padding: 18px 20px;
    min-height: 60px;
    transition: opacity 0.14s ease-out;
}
/* Composer layer: absolutely positioned on top of the rest. Hidden at rest.
   Padding mirrors .agent-input to reserve space for the bottom action row.
   Transitions are asymmetric — the base rule below is the EXIT transition
   (quick 80ms fade, no delay) so the buttons don't hang around while the
   tile is collapsing. The delayed ENTER transition lives on the
   `.is-composing` override further down. */
.home-tile-composer {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    padding: 10px 12px 44px 12px;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.08s ease-out;
}
.home-tile-create.is-composing {
    /* Matches .agent-input on the project page (min-height: 108px) so the
       two composers feel identical when the user lands on the project. */
    min-height: 108px;
    cursor: default;
    /* Kill the active-scale click jiggle once composing. */
    transform: none;
    /* Keep the accent border after click-to-compose. The project-page composer
       doesn't do this, but here it nudges the user to notice the input is live
       and ready for their message. Intentional home-only affordance. */
    border-color: var(--accent);
}
.home-tile-create.is-composing:hover {
    /* Don't brighten the background on hover while composing — it's not a
       click-to-navigate target anymore. Border stays accent (inherited). */
    background: var(--bg-input);
}
.home-tile-create.is-composing .home-tile-create-rest {
    opacity: 0;
    pointer-events: none;
}
.home-tile-create.is-composing .home-tile-composer {
    /* Enter transition — delay 100ms so the tile finishes growing first, then
       200ms fade-in. On exit (class removed) the base 80ms transition applies. */
    transition: opacity 0.20s ease-out 0.10s;
    opacity: 1;
    pointer-events: auto;
}
/* Composer editor — the contenteditable. Placeholder via ::before on :empty. */
.home-composer-editor {
    flex: 1 1 auto;
    color: var(--text);
    font-size: 13px;
    line-height: 1.5;
    /* Fills the composer's content area (108 tile - 10 top pad - 44 bottom pad
       = 54). Keeps click-anywhere-in-the-shell usable as a text target. */
    min-height: 54px;
    outline: none;
    overflow-y: auto;
    white-space: pre-wrap;
    word-break: break-word;
    cursor: text;
}
.home-composer-editor:empty::before {
    content: attr(data-placeholder);
    color: var(--text-dim);
    pointer-events: none;
}
/* Bottom-left upload (+) button. Mirrors .agent-upload-btn geometry exactly.
   The + glyph is rendered via ::before with a -1px nudge so it sits on the
   button's true vertical midline (same optical fix we used on the ×). */
.home-composer-upload {
    position: absolute;
    left: 8px; bottom: 8px;
    width: 30px; height: 30px;
    background: transparent;
    color: var(--text-dim);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 0;
    cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.home-composer-upload::before {
    content: "+";
    display: block;
    font: 400 18px/1 -apple-system, "system-ui", "Segoe UI", Roboto, sans-serif;
    transform: translateY(-1px);
}
.home-composer-upload:hover {
    color: var(--accent);
    border-color: var(--accent);
    background: rgba(160, 182, 140, 0.08);
}
/* Bottom-right: dual-state send/audio button. Matches Claude's composer —
   when the editor is empty we show a waveform ("speak to the agent", future
   functionality) in the same visual weight as the + button. As soon as the
   user types, the button morphs into a filled accent send arrow. Glyph is
   injected via JS innerHTML so we can swap <svg> ↔ ↑. */
.home-composer-send {
    position: absolute;
    right: 8px; bottom: 8px;
    width: 30px; height: 30px;
    border-radius: 6px;
    padding: 0;
    cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    transition: color 0.15s, border-color 0.15s, background 0.15s;
    /* Default: audio-mode — same minimal shell as the + button so the two
       reads as a balanced pair when the composer is empty. */
    background: transparent;
    color: var(--text-dim);
    border: 1px solid var(--border);
}
.home-composer-send:hover {
    color: var(--accent);
    border-color: var(--accent);
    background: rgba(160, 182, 140, 0.08);
}
.home-composer-send svg { display: block; }
/* Send mode (editor has text) — filled accent + white arrow. */
.home-composer-send.is-ready {
    background: var(--accent);
    color: white;
    border: 1px solid transparent;
    font: 400 18px/1 -apple-system, "system-ui", "Segoe UI", Roboto, sans-serif;
}
.home-composer-send.is-ready:hover {
    background: var(--accent-hover);
    color: white;
    border-color: transparent;
}

/* ---------- Thinking state ----------
   After the user hits Send, we don't navigate straight away — we fire the
   agent's first turn from the home page so the project page can land with
   the correct drawer layout already chosen. During that (~3-10s) the tile
   cross-fades to this thinking layer: composer + rest layer fade out, just
   a pulsing ✦ in the middle. Tile stays at the same 108px geometry so
   there's no reflow. */
.home-tile-thinking {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.08s ease-out;
}
.home-tile-create.is-thinking .home-tile-create-rest,
.home-tile-create.is-thinking .home-tile-composer {
    opacity: 0;
    pointer-events: none;
}
.home-tile-create.is-thinking .home-tile-thinking {
    transition: opacity 0.20s ease-out 0.10s;
    opacity: 1;
}
/* Keep the 108px shell + accent border from .is-composing active while
   thinking — the tile never reverts to the rest state until nav completes. */
.home-tile-create.is-thinking {
    min-height: 108px;
    cursor: default;
    transform: none;
    border-color: var(--accent);
}
.home-tile-create.is-thinking:hover {
    background: var(--bg-input);
}
.home-thinking-glyph {
    font-size: 24px;
    line-height: 1;
    color: var(--accent);
    animation: home-thinking-pulse 1.6s ease-in-out infinite;
}
@keyframes home-thinking-pulse {
    0%, 100% { transform: scale(1); opacity: 0.55; }
    50%      { transform: scale(1.15); opacity: 1; }
}

/* ---------- Sibling tiles collapse during compose ----------
   Project tiles fade their opacity first, then collapse out of flex flow via
   max-height + margin/padding. The 12px flex gap is also consumed because the
   stack reduces to a single flex child. */
.home-stack.is-composing .home-tile-project {
    opacity: 0;
    max-height: 0;
    min-height: 0;
    margin: 0;
    padding-top: 0;
    padding-bottom: 0;
    border-width: 0;
    pointer-events: none;
    overflow: hidden;
    transition: opacity 0.16s ease-out,
                max-height 0.24s cubic-bezier(0.2, 0.8, 0.2, 1) 0.10s,
                margin 0.24s cubic-bezier(0.2, 0.8, 0.2, 1) 0.10s,
                padding 0.24s cubic-bezier(0.2, 0.8, 0.2, 1) 0.10s,
                border-width 0.01s linear 0.30s;
}
/* Give the project tiles a ceiling max-height so the collapse animates from
   a known value; tiles are ~60px so 80 is a safe cap. */
.home-tile-project {
    max-height: 100px;
    transition: max-height 0.24s cubic-bezier(0.2, 0.8, 0.2, 1);
}

/* ---------- Cross-document horizontal slide ----------
   View Transitions API: the browser snapshots the OLD page, snapshots the
   NEW page, then animates between them in one coordinated frame. No
   navigation paint gap, no flash, no "drawers settle into their saved
   state on arrival" — the new page is fully painted before the morph runs.
   Header is given its own view-transition-name so it stays anchored as a
   separate group instead of getting swept along with the root.

   Browser support: Chrome 126+ / Safari 18+. Firefox falls back to a plain
   hard navigation (no animation). Acceptable degradation.

   Opt-in to cross-doc VT lives in templates/base.html as @view-transition;
   the navigation: auto rule there flips on cross-document transitions for
   every same-origin nav. */

/* Pin the top nav as its own transition group — captured separately from
   the root so the root slide doesn't sweep it along. Same DOM on both
   sides ⇒ browser shows it static throughout. */
.top-nav { view-transition-name: top-nav; }

/* Root slide — old page exits left, new page enters from right, in lockstep.
   Single decelerate curve so it reads as one continuous motion. */
::view-transition-old(root) {
    animation: vt-slide-out-left 0.4s cubic-bezier(0.25, 1, 0.5, 1) both;
}
::view-transition-new(root) {
    animation: vt-slide-in-from-right 0.4s cubic-bezier(0.25, 1, 0.5, 1) both;
}
/* The nav's own group gets no animation — it stays put. */
::view-transition-old(top-nav),
::view-transition-new(top-nav) { animation: none; }

@keyframes vt-slide-out-left {
    from { transform: translateX(0);     opacity: 1; }
    to   { transform: translateX(-100%); opacity: 0; }
}
@keyframes vt-slide-in-from-right {
    from { transform: translateX(100%); opacity: 1; }
    to   { transform: translateX(0);    opacity: 1; }
}

/* Reverse direction — fires when navigating from a project page back to
   the home listing (e.g. clicking the "Create by New Forest" brand link).
   The "backward" type is added in the pageswap/pagereveal handlers in
   base.html. Old slides off RIGHT, new comes in from LEFT — feels like
   the user has scrolled the page back the way they came. */
:active-view-transition-type(backward)::view-transition-old(root) {
    animation: vt-slide-out-right 0.4s cubic-bezier(0.25, 1, 0.5, 1) both;
}
:active-view-transition-type(backward)::view-transition-new(root) {
    animation: vt-slide-in-from-left 0.4s cubic-bezier(0.25, 1, 0.5, 1) both;
}
@keyframes vt-slide-out-right {
    from { transform: translateX(0);    opacity: 1; }
    to   { transform: translateX(100%); opacity: 0; }
}
@keyframes vt-slide-in-from-left {
    from { transform: translateX(-100%); opacity: 1; }
    to   { transform: translateX(0);     opacity: 1; }
}

/* Delete X — subtle, muted, hover-revealed so the tile reads clean by
   default. Sits inside the <a> tile; JS stops propagation. */
.home-tile-delete {
    flex: 0 0 auto;
    width: 28px; height: 28px;
    display: inline-flex; align-items: center; justify-content: center;
    background: transparent;
    border: none;
    padding: 0;
    margin: 0 -6px 0 4px;
    color: var(--text-dim);
    /* The × glyph sits optically low in its line-box (centred on x-height
       rather than cap-height). Nudge 1px up so its visual midline lands on
       the project name's midline. */
    font: 300 22px/1 -apple-system, "system-ui", "Segoe UI", Roboto, sans-serif;
    cursor: pointer;
    opacity: 0;
    border-radius: 6px;
    transition: opacity 0.14s ease-out, color 0.14s ease-out,
                background 0.14s ease-out;
}
.home-tile-delete::before {
    content: "\00D7";   /* Replaces the &times; entity from the template. */
    display: block;
    transform: translateY(-1px);
    line-height: 1;
}
.home-tile-project:hover .home-tile-delete,
.home-tile-delete:focus-visible {
    opacity: 0.6;
}
.home-tile-delete:hover {
    opacity: 1;
    color: #e4675a;
    background: rgba(228, 103, 90, 0.08);
}

.home-empty {
    color: var(--text-dim);
    text-align: center;
    font-size: 13px;
    margin-top: 32px;
}
.home-empty em { color: var(--text); font-style: normal; }

/* ---------- Admin users page ---------- */
.admin-page {
    max-width: 1500px;
    margin: 24px auto;
    padding: 0 24px;
}
.admin-card.no-pad { padding: 0; }
.admin-table td, .admin-table th { white-space: nowrap; }

/* Explicit column widths so the Add-user row (in-table, tied to a virtual
   form via the HTML5 `form` attribute) aligns cleanly with data rows below.
   Display name + Username get the lion's share — people's names are long. */
.users-table .col-display-name { width: 20%; }
.users-table .col-username     { width: 16%; }
.users-table .col-role         { width: 10%; }
.users-table .col-slots        { width: 7%;  }
.users-table .col-last-login   { width: 13%; }
.users-table .col-active       { width: 7%;  }
.users-table .col-password     { width: 19%; }
.users-table .col-action       { width: auto; }  /* Add / Delete buttons */

/* Projects table reuses the admin-table styling with its own column widths */
.projects-table .col-project-name       { width: 32%; }
.projects-table .col-project-count      { width: 9%;  }
.projects-table .col-project-storage    { width: 11%; }
.projects-table .col-project-created-by { width: 14%; }
.projects-table .col-project-updated    { width: 14%; }
.projects-table .col-project-action     { width: auto; }

/* Row-click affordance — signal that the whole row is interactive */
.admin-table tr.clickable-row { cursor: pointer; }
.admin-table tr.clickable-row:hover td { background: rgba(160, 182, 140, 0.05); }

/* Legacy-attribution placeholder — styled plain like real creators. */
.dim-legacy { opacity: 0.7; }

/* Disabled state on rename/delete buttons — applied when the current user
   isn't the creator or an admin. Browser default on <button disabled> is
   often too subtle on our dark theme; explicit dim + not-allowed cursor
   signals "you can't touch this" while the title attribute explains why. */
button[disabled].rename-btn,
button[disabled].btn-link,
button[disabled].home-rename-btn,
button[disabled].project-rename-btn,
button[disabled].rename-asset-btn,
button[disabled].rename-video-btn,
button[disabled].asset-delete-btn,
button[disabled].delete-btn {
    opacity: 0.35;
    cursor: not-allowed;
    pointer-events: auto;  /* keep hover tooltips working */
}
button[disabled].asset-delete-btn:hover,
button[disabled].delete-btn:hover { background: var(--danger); filter: none; }

/* Per-tile attribution footer — full-width strip below the action row,
   slightly lighter than the card background so it reads as a separate
   zone. Content = creator display name only (searchable via the gallery
   filter's substring match on card.textContent). */
.card-footer {
    padding: 6px 12px;
    font-size: 11px;
    color: var(--text-dim);
    background: rgba(255, 255, 255, 0.03);
    border-top: 1px solid var(--border);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.projects-table .project-link {
    color: var(--text);
    font-weight: 500;
    font-size: 14px;
}
.projects-table .project-link:hover { color: var(--accent); }
.projects-table .project-name-text { display: inline-block; }
/* Activity log table */
.activity-table .col-activity-when    { width: 16%; }
.activity-table .col-activity-who     { width: 15%; }
.activity-table .col-activity-action  { width: 14%; }
.activity-table .col-activity-target  { width: 10%; }
.activity-table .col-activity-details { width: auto; }
.activity-table .activity-details { white-space: normal; }
.activity-table .activity-meta {
    display: inline-block;
    margin-right: 10px;
    font-size: 12px;
}
.admin-filters {
    display: flex;
    gap: 14px;
    margin-bottom: 14px;
    align-items: center;
    flex-wrap: wrap;
}
.admin-filters label { font-size: 13px; color: var(--text-dim); }
.admin-filters select {
    margin-left: 4px;
    padding: 4px 8px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 4px;
    color: var(--text);
    font-size: 13px;
}
.admin-pagination {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-top: 16px;
    padding: 0 4px;
}

.projects-table .admin-add-row input[type="text"] {
    width: 100%;
    padding: 6px 10px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text);
    font-size: 13px;
}

.users-table .admin-add-row td {
    background: rgba(160, 182, 140, 0.04);
    border-bottom: 2px solid var(--border);
}
.users-table .admin-add-row input[type="text"],
.users-table .admin-add-row input[type="password"],
.users-table .admin-add-row select {
    width: 100%;
    padding: 6px 10px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text);
    font-size: 13px;
}
.pwd-and-submit {
    display: flex;
    gap: 8px;
    align-items: center;
}
.pwd-and-submit .pwd-wrap { flex: 1; min-width: 0; }
.pwd-and-submit .pwd-wrap input { padding-right: 60px; }

.admin-hint {
    font-size: 12px;
    color: var(--text-dim);
    margin-top: 12px;
    padding: 0 4px;
}

/* Password field + eye toggle + generate button */
.pwd-wrap {
    position: relative;
    display: inline-flex;
    align-items: center;
    flex: 1;
    min-width: 180px;
}
.pwd-wrap input {
    flex: 1;
    padding-right: 68px;  /* reserve for eye + generate buttons */
    font-family: ui-monospace, SFMono-Regular, monospace;
}
.pwd-eye, .pwd-generate {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    background: none;
    border: none;
    color: var(--text-dim);
    cursor: pointer;
    padding: 0 4px;
    font-size: 14px;
    line-height: 1;
    height: 22px;
}
.pwd-eye { right: 6px; }
.pwd-generate { right: 30px; font-size: 13px; }
.pwd-eye:hover, .pwd-generate:hover { color: var(--accent); }
.pwd-eye.on { color: var(--accent); }

/* Inline reset-password modal */
.pwd-reset-modal {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.55);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 999;
}
.pwd-reset-modal .pwd-reset-box {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: 20px;
    width: 420px;
    max-width: 90vw;
}
.pwd-reset-box h3 { font-size: 15px; margin-bottom: 4px; color: var(--text); }
.pwd-reset-box p { font-size: 12px; color: var(--text-dim); margin-bottom: 14px; }
.pwd-reset-box .pwd-wrap { width: 100%; }
.pwd-reset-box .pwd-wrap input {
    width: 100%;
    padding: 10px 70px 10px 12px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text);
    font-size: 14px;
}
.pwd-reset-actions {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
    margin-top: 14px;
}
.admin-heading { font-size: 22px; margin-bottom: 16px; color: var(--text); }
.admin-disk-widget {
    display: inline-flex; align-items: center; gap: 8px;
    padding: 6px 12px; margin-bottom: 16px;
    background: rgba(107,143,74,0.12); border-radius: 6px;
    font-size: 13px; color: var(--text-muted);
}
.admin-disk-widget .admin-disk-value { color: var(--text); font-variant-numeric: tabular-nums; }
.admin-disk-widget .admin-disk-pct { color: var(--text-muted); }
.admin-disk-widget.warn { background: rgba(214,158,46,0.15); color: #e6c066; }
.admin-disk-widget.warn .admin-disk-value,
.admin-disk-widget.warn .admin-disk-pct { color: #e6c066; }
.admin-disk-widget.danger { background: rgba(214,75,69,0.18); color: #f08b82; }
.admin-disk-widget.danger .admin-disk-value,
.admin-disk-widget.danger .admin-disk-pct { color: #f08b82; }
.admin-disk-widget .admin-disk-alert { font-weight: 500; }
.admin-sub-nav {
    display: flex;
    gap: 4px;
    margin-bottom: 16px;
    border-bottom: 1px solid var(--border);
}
.admin-sub-nav a {
    padding: 8px 14px;
    color: var(--text-dim);
    font-size: 13px;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
}
.admin-sub-nav a:hover { color: var(--text); }
.admin-sub-nav a.active {
    color: var(--accent);
    border-bottom-color: var(--accent);
}
.admin-subheading { font-size: 14px; color: var(--text-dim); margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
.admin-card {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: 20px;
    margin-bottom: 20px;
}
.admin-add-form {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    align-items: center;
}
.admin-add-form input, .admin-add-form select {
    padding: 8px 12px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text);
    font-size: 13px;
}
.admin-add-form input[type="number"] { width: 80px; }
.admin-add-form input[type="text"], .admin-add-form input[type="password"] { flex: 1; min-width: 140px; }
.admin-hint { font-size: 12px; color: var(--text-dim); margin-top: 8px; }
.admin-table { width: 100%; border-collapse: collapse; }
.admin-table th, .admin-table td {
    padding: 10px 8px;
    text-align: left;
    border-bottom: 1px solid var(--border);
    font-size: 13px;
    vertical-align: middle;
}
.admin-table th { color: var(--text-dim); font-weight: 500; text-transform: uppercase; font-size: 11px; letter-spacing: 0.5px; }
.admin-table td.mono { font-family: ui-monospace, SFMono-Regular, monospace; color: var(--text-dim); }
.admin-table td.dim { color: var(--text-dim); font-size: 12px; }
.admin-table tr.inactive td { opacity: 0.45; }
.admin-table .role-select, .admin-table .slots-input {
    padding: 4px 8px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 4px;
    color: var(--text);
    font-size: 13px;
}
.admin-table .slots-input { width: 60px; }
/* Password column: stretch the password-entry wrapper AND the reset button
   to the full cell width so both rows share the same lozenge footprint.
   Default `.pwd-wrap` is inline-flex with min-width 180px — override in
   the users-table context so it tracks the cell instead. */
.users-table .pwd-wrap {
    display: block;
    width: 100%;
    min-width: 0;
}
.users-table .pwd-wrap input { width: 100%; }
.users-table .reset-pwd-btn { width: 100%; }
.admin-table .btn-link {
    background: none;
    border: none;
    color: var(--text-dim);
    cursor: pointer;
    font-size: 13px;
    margin-left: 4px;
    padding: 0 4px;
}
/* Keep the rename pencil glued to the right edge of the name cell so the
   write icon lines up with the column's right-hand gutter instead of
   crowding the name text. Absolute-position the button inside a regular
   table-cell — using `display: flex` on a <td> breaks row-baseline
   alignment with the other cells. */
.admin-table td.name-cell {
    position: relative;
    padding-right: 32px;
}
.admin-table td.name-cell > .btn-link {
    position: absolute;
    right: 8px;
    top: 50%;
    transform: translateY(-50%);
    margin: 0;
}
.admin-table .btn-link:hover { color: var(--accent); }
.admin-table .user-display-name { font-weight: 500; }
.admin-table .btn-sm { padding: 5px 10px; font-size: 12px; }

/* ---------- Container ---------- */
.container { max-width: 900px; margin: 0 auto; padding: 24px; }

/* ---------- Flash ---------- */
.flash { padding: 10px 16px; border-radius: var(--radius); margin-bottom: 16px; }
.flash-error { background: rgba(239,68,68,0.15); color: var(--danger); border: 1px solid rgba(239,68,68,0.3); }
.flash-success { background: rgba(34,197,94,0.15); color: var(--success); border: 1px solid rgba(34,197,94,0.3); }

/* ---------- Page toast (global error banner) ----------
   Sits above everything (z-index 10001 — above modal-overlay at 200 and
   the lightbox close at 210). Drops down from the top; auto-hides via
   `window.showPageToast(msg)`. */
.page-toast {
    position: fixed;
    top: 16px;
    left: 50%;
    transform: translateX(-50%);
    background: rgba(239, 68, 68, 0.96);
    color: #fff;
    padding: 10px 44px 10px 16px;
    border-radius: 8px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
    z-index: 10001;
    font-size: 13px;
    max-width: min(560px, 92vw);
    display: flex;
    align-items: center;
    gap: 8px;
    animation: page-toast-in 0.18s ease-out;
    pointer-events: auto;
}
.page-toast.hidden { display: none; }
.page-toast-close {
    position: absolute;
    top: 50%;
    right: 10px;
    transform: translateY(-50%);
    background: transparent;
    border: none;
    color: #fff;
    font-size: 14px;
    cursor: pointer;
    opacity: 0.85;
    padding: 4px 6px;
    line-height: 1;
}
.page-toast-close:hover { opacity: 1; }
@keyframes page-toast-in {
    from { transform: translate(-50%, -12px); opacity: 0; }
    to { transform: translate(-50%, 0); opacity: 1; }
}

/* ---------- Login ---------- */
.login-container {
    max-width: 360px;
    /* Mirror .home-page top margin so the first form input lines up at
       the same viewport y as the "Create something new" tile (56 px nav
       + 56 px container margin = 112 px). Login template also clears the
       .container class on <main> so there's no extra padding to fight. */
    margin: 56px auto 80px;
    padding: 0 20px;
    text-align: center;
}
.login-form { display: flex; flex-direction: column; gap: 12px; }
.login-form input {
    padding: 10px 14px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text);
    font-size: 14px;
}
.login-form input:focus { outline: none; border-color: var(--accent); }
.login-form button {
    padding: 10px;
    background: var(--accent);
    color: white;
    border: none;
    border-radius: var(--radius);
    font-size: 14px;
    cursor: pointer;
    transition: background 0.15s;
}
.login-form button:hover { background: var(--accent-hover); }

/* ---------- Mode Tabs ---------- */
.mode-tabs {
    display: flex;
    gap: 4px;
    background: var(--bg-card);
    padding: 4px;
    border-radius: var(--radius);
    margin-bottom: 20px;
}
.mode-tab {
    flex: 1;
    display: inline-flex; align-items: center; justify-content: center;
    height: 36px; padding: 0 12px;
    background: transparent;
    color: var(--text-dim);
    border: none;
    border-radius: 6px;
    cursor: pointer;
    font-size: 13px;
    line-height: 1;
    transition: color 0.1s;
}
.mode-tab:hover { color: var(--text); }
.mode-tab.active { background: var(--accent); color: white; }

.tab-content { display: none; }
.tab-content.active { display: block; }

/* ---------- Upload Zone ---------- */
.upload-zone {
    border: 2px dashed var(--border);
    border-radius: var(--radius-lg);
    padding: 32px;
    text-align: center;
    cursor: pointer;
    transition: all 0.2s;
    margin-bottom: 12px;
}
.upload-zone:hover, .upload-zone.dragover {
    border-color: var(--accent);
    background: rgba(99,102,241,0.05);
}
.upload-zone p { color: var(--text-dim); }
.upload-hint { font-size: 12px; margin-top: 4px; color: var(--text-dim); opacity: 0.7; }

/* ---------- File Chips ---------- */
.file-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-bottom: 12px;
}
.file-chip {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 4px 10px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 20px;
    font-size: 13px;
}
.file-chip img {
    width: 24px;
    height: 24px;
    border-radius: 4px;
    object-fit: cover;
}
.file-chip .chip-icon {
    width: 24px;
    height: 24px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--bg-hover);
    border-radius: 4px;
    font-size: 11px;
}
.file-chip .chip-label { color: var(--accent); font-weight: 600; }
.file-chip .chip-name { color: var(--text-dim); max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.file-chip .chip-remove {
    background: none; border: none; color: var(--text-dim); cursor: pointer;
    font-size: 16px; line-height: 1; padding: 0 2px;
}
.file-chip .chip-remove:hover { color: var(--danger); }

/* ---------- Frame Uploads (I2V) ---------- */
.frame-uploads { display: flex; gap: 16px; margin-bottom: 12px; }
.frame-upload { flex: 1; }
.frame-upload > label { display: block; margin-bottom: 6px; font-size: 13px; color: var(--text-dim); }
.frame-drop {
    border: 2px dashed var(--border);
    border-radius: var(--radius);
    padding: 24px;
    text-align: center;
    cursor: pointer;
    transition: all 0.2s;
}
.frame-drop:hover, .frame-drop.dragover {
    border-color: var(--accent); background: rgba(107,143,74,0.08);
}
.frame-drop p { color: var(--text-dim); font-size: 13px; }
.frame-preview { margin-top: 8px; position: relative; }
.frame-preview img {
    max-width: 100%;
    max-height: 160px;
    border-radius: var(--radius);
    border: 1px solid var(--border);
}
.frame-clear-btn {
    position: absolute; top: 4px; right: 4px;
    background: rgba(0,0,0,0.7); border: none; color: white;
    width: 20px; height: 20px; border-radius: 50%; cursor: pointer;
    font-size: 12px; display: flex; align-items: center; justify-content: center;
    line-height: 1;
}
.frame-clear-btn:hover { background: var(--danger); }
.rerun-note { font-size: 11px; color: var(--text-dim); margin-top: 4px; }

/* ---------- Prompt ---------- */
.prompt-input {
    width: 100%;
    min-height: 80px;
    padding: 12px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text);
    font-size: 14px;
    font-family: inherit;
    resize: vertical;
    margin-bottom: 16px;
}
.prompt-input:focus { outline: none; border-color: var(--accent); }

/* Contenteditable prompt editor with inline filename pills */
.mention-editor {
    flex: 1;
    min-height: 0;
    margin: 0;
    cursor: text;
    white-space: pre-wrap;
    word-break: break-word;
    line-height: 1.5;
    overflow-y: auto;
    padding: 12px;
    background: transparent;
    border: none;
    color: var(--text);
    font-size: 14px;
    font-family: inherit;
    transition: background 0.1s;
    position: relative;
}
.mention-editor:focus { outline: none; }
/* Absolute-positioned so the placeholder is out of the caret's text-flow. If
   it were in-flow, clicking inside the placeholder would land the caret
   mid-line visually, then snap to start on first keystroke (DOM position is
   always 0 when empty). */
.mention-editor.is-empty::before {
    content: attr(data-placeholder);
    color: var(--text-dim);
    pointer-events: none;
    position: absolute;
    top: 12px;
    left: 12px;
    right: 12px;
}
.mention-editor.dragover {
    background: rgba(160, 182, 140, 0.08);
}

/* First / Last frame slots — pinned inside the prompt wrapper's bottom row */
.frame-slots {
    flex: 0 0 auto;
    display: flex;
    gap: 6px;
    padding: 6px 8px 8px 8px;
    background: var(--bg-input);
    border-bottom-left-radius: var(--radius);
    border-bottom-right-radius: var(--radius);
}
.frame-slot {
    flex: 1;
    display: inline-flex;
    align-items: center;
    justify-content: flex-start;
    gap: 4px;
    min-width: 0;
    padding: 1px 8px;
    min-height: 22px;
    border-radius: 10px;
    font-size: 12px;
    font-weight: 600;
    line-height: 1.3;
    cursor: pointer;
    position: relative;
    transition: border-color 0.12s, background 0.12s;
}
.frame-slot.empty {
    border: 1px dashed var(--border);
    color: var(--text-dim);
    background: transparent;
}
.frame-slot.empty:hover {
    border-color: var(--accent);
    color: var(--text);
}
.frame-slot.filled {
    border: 1px solid rgba(160, 182, 140, 0.5);
    background: rgba(160, 182, 140, 0.18);
    color: var(--accent);
    font-weight: 600;
}
.frame-slot.dragover {
    border-style: solid;
    border-color: var(--accent);
    background: rgba(160, 182, 140, 0.12);
    color: var(--text);
}
.frame-slot .frame-slot-display {
    display: flex;
    align-items: center;
    gap: 6px;
    min-width: 0;
    flex: 1;
}
.frame-slot .frame-slot-icon { font-size: 12px; opacity: 0.8; }
.frame-slot .frame-slot-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}
.frame-slot .frame-slot-thumb {
    width: 16px; height: 16px; object-fit: cover; border-radius: 3px; flex-shrink: 0;
}
.frame-slot-display { margin-right: auto; }
.frame-slot .frame-slot-clear {
    display: none;
    background: none; border: none; color: var(--accent);
    opacity: 0.55; font-size: 14px; line-height: 1;
    padding: 0 2px; margin-left: 2px; cursor: pointer; border-radius: 50%;
}
.frame-slot.filled .frame-slot-clear { display: inline-flex; }
.frame-slot .frame-slot-clear:hover { opacity: 1; color: var(--danger); background: rgba(255,255,255,0.08); }

/* Real face toggle — lives in .frame-slots next to First/Last Frame, styled
   as a compact pill that matches the empty-slot look. flex:0 so it doesn't
   stretch; sibling slots take the free space. */
.frame-slot-toggle {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-height: 22px;
    padding: 1px 10px;
    border-radius: 10px;
    font-size: 12px;
    border: 1px dashed var(--border);
    color: var(--text-dim);
    background: transparent;
    cursor: pointer;
    user-select: none;
    white-space: nowrap;
}
.frame-slot-toggle:hover { border-color: var(--accent); color: var(--text); }
.frame-slot-toggle input[type="checkbox"] {
    margin: 0;
    accent-color: var(--accent);
    cursor: pointer;
}
.frame-slot-toggle:has(input:checked) {
    border: 1px solid rgba(160, 182, 140, 0.5);
    background: rgba(160, 182, 140, 0.18);
    color: var(--accent);
    /* No font-weight change — bolding the label would resize the pill and
       squash First/Last Frame siblings. Colour + border is enough. */
}
.frame-slot-toggle-name { line-height: 1; }

/* Inline picker state — the slot becomes a search input with a dropdown below */
.frame-slot.picker-active {
    border: 1px solid var(--accent);
    background: var(--bg-input);
    cursor: text;
}
.frame-slot .frame-slot-input {
    flex: 1;
    min-width: 0;
    background: transparent;
    border: none;
    outline: none;
    color: var(--text);
    font-size: 12px;
    padding: 0 2px;
}
.frame-slot .frame-slot-input::placeholder { color: var(--text-dim); }
/* Frame-slot picker dropdown — reparented to .prompt-wrapper at open
   time so it spans the full prompt-area width (matches @-mention UX).
   Selector is deliberately parent-agnostic: the dropdown works whether
   it ends up inside .prompt-wrapper (usual) or falls back to the slot
   (defensive). `top: 100%` on .prompt-wrapper drops it immediately
   below the prompt card; a small gap would collide with frame-slot
   bottom padding, so we sit flush. */
.slot-dropdown {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    margin-top: 4px;
    max-height: 280px;
    overflow-y: auto;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: 0 8px 24px rgba(0,0,0,0.4);
    z-index: 100;
}
.slot-dropdown.hidden { display: none; }
.mention-pill {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 1px 8px;
    margin: 0 1px;
    background: rgba(160, 182, 140, 0.18);
    border: 1px solid rgba(160, 182, 140, 0.5);
    border-radius: 10px;
    color: var(--accent);
    font-size: 12px;
    font-weight: 600;
    user-select: none;
    vertical-align: baseline;
    line-height: 1.3;
    white-space: nowrap;
    max-width: 220px;
    overflow: hidden;
    text-overflow: ellipsis;
}
.mention-pill .pill-icon { font-size: 10px; opacity: 0.7; }
.mention-pill .pill-name { overflow: hidden; text-overflow: ellipsis; max-width: 160px; }
.mention-pill .mention-pill-x {
    background: none;
    border: none;
    color: var(--accent);
    opacity: 0.55;
    padding: 0 2px;
    margin-left: 2px;
    font-size: 14px;
    line-height: 1;
    cursor: pointer;
    border-radius: 50%;
}
.mention-pill .mention-pill-x:hover { opacity: 1; color: var(--danger); background: rgba(255,255,255,0.08); }

/* Ghost pill: referenced asset has been deleted since the prompt was written.
   Still clickable (popover) and removable (×), but visually muted + dashed. */
.mention-pill.ghost {
    background: rgba(160, 160, 160, 0.08);
    border: 1px dashed rgba(200, 200, 200, 0.4);
    color: var(--text-dim);
    opacity: 0.75;
}
.mention-pill.ghost .mention-pill-x { color: var(--text-dim); }
.mention-pill.ghost .pill-name { text-decoration: line-through; text-decoration-thickness: 1px; }

.file-chip.ghost {
    border-color: rgba(200, 200, 200, 0.35);
    opacity: 0.7;
}
.file-chip.ghost .chip-name { text-decoration: line-through; color: var(--text-dim); }

/* Popover tombstone state when asset is deleted */
.pill-popover .pill-popover-deleted {
    width: 100%;
    aspect-ratio: 16/9;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(0, 0, 0, 0.35);
    border: 1px dashed rgba(200, 200, 200, 0.35);
    border-radius: 6px;
    color: var(--text-dim);
    font-size: 12px;
    text-align: center;
    padding: 10px;
}

/* ---------- Controls Row ---------- */
.controls-row {
    display: flex;
    flex-wrap: nowrap;
    gap: 20px;
    align-items: stretch;
    justify-content: center;
    margin-bottom: 20px;
    padding: 12px 16px;
    background: var(--bg-card);
    border-radius: var(--radius);
    border: 1px solid var(--border);
}
.control-group {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
}
.control-group > label:first-child {
    display: block;
    font-size: 11px;
    color: var(--text-dim);
    margin-bottom: 6px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    text-align: center;
    flex-shrink: 0;
    white-space: nowrap;
}
.control-group select,
.control-group input[type="number"] {
    height: 32px;
    padding: 0 10px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text);
    font-size: 13px;
}
.control-group select:focus,
.control-group input:focus { outline: none; border-color: var(--accent); }
.seed-input { width: 110px; }
/* Hide native spinner arrows — seeds aren't ordinal so ±1 is meaningless,
   and the arrows eat ~16px of width we'd rather give back to the row. */
.seed-input::-webkit-outer-spin-button,
.seed-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.seed-input { -moz-appearance: textfield; appearance: textfield; }

/* Toggle group (Fast/Standard) */
.toggle-group { display: flex; gap: 2px; background: var(--bg-input); border-radius: var(--radius); padding: 2px; height: 32px; align-items: center; }
.toggle-btn {
    padding: 4px 12px;
    background: transparent;
    border: none;
    color: var(--text-dim);
    border-radius: 6px;
    cursor: pointer;
    font-size: 13px;
    transition: all 0.15s;
    line-height: 1;
}
.toggle-btn.active { background: var(--accent); color: white; }
/* Static pill — looks like an active toggle button but isn't interactive
   (used when an engine has a single variant, e.g. Seedream "5.0 LITE"). */
.toggle-btn.static { cursor: default; pointer-events: none; }

/* Duration slider */
.duration-control { display: flex; align-items: center; gap: 6px; height: 32px; }
.duration-control input[type="range"] { width: 80px; accent-color: var(--accent); }
#duration-value { font-size: 13px; color: var(--text); min-width: 24px; }

/* Count control */
.count-control { display: flex; align-items: center; gap: 6px; height: 32px; }
.count-btn {
    width: 28px; height: 28px;
    background: var(--bg-input); border: 1px solid var(--border);
    border-radius: var(--radius); color: var(--text);
    font-size: 16px; line-height: 1; cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    transition: all 0.15s;
}
.count-btn:hover { background: var(--bg-hover); border-color: var(--accent); }
#count-value { font-size: 14px; min-width: 16px; text-align: center; }

/* Reset button */
.reset-btn {
    height: 32px;
    padding: 0 14px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text-dim);
    font-size: 12px;
    cursor: pointer;
    transition: all 0.15s;
}
.reset-btn:hover { background: var(--bg-hover); color: var(--text); border-color: var(--accent); }

/* Audio toggle */
.switch { position: relative; display: inline-flex; align-items: center; width: 40px; height: 32px; cursor: pointer; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider-toggle {
    position: absolute; left: 0; top: 5px; width: 40px; height: 22px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 22px;
    transition: 0.2s;
}
.slider-toggle::before {
    content: "";
    position: absolute;
    width: 16px; height: 16px;
    left: 2px; bottom: 2px;
    background: var(--text-dim);
    border-radius: 50%;
    transition: 0.2s;
}
.switch input:checked + .slider-toggle { background: var(--accent); border-color: var(--accent); }
.switch input:checked + .slider-toggle::before { transform: translateX(18px); background: white; }

/* ---------- Generate Button ---------- */
.generate-btn {
    width: 100%;
    padding: 12px;
    background: var(--accent);
    color: white;
    border: none;
    border-radius: var(--radius);
    font-size: 15px;
    font-weight: 600;
    cursor: pointer;
    transition: all 0.15s;
}
.generate-btn:hover { background: var(--accent-hover); }
.generate-btn:disabled { opacity: 0.5; cursor: not-allowed; }

/* ---------- Active Tasks (generate page — full width) ---------- */
#active-tasks {
    margin-top: 24px;
    display: flex;
    flex-direction: column;
    gap: 16px;
}
#active-tasks .video-container video {
    width: 100%;
}

/* Spinner */
.spinner {
    width: 24px; height: 24px;
    border: 3px solid var(--border);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ---------- Gallery ---------- */
.gallery-page h2 { margin-bottom: 20px; }
.empty-state { color: var(--text-dim); text-align: center; padding: 48px 0; }

.gallery-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 16px;
}
.gallery-card {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    overflow: hidden;
    transition: border-color 0.15s;
    display: flex;
    flex-direction: column;
}
.gallery-card:hover { border-color: var(--accent); }

.video-container { position: relative; background: #000; aspect-ratio: 16/9; overflow: hidden; }
.video-click-overlay {
    position: absolute !important; top: 0; left: 0; right: 0; bottom: 0;
    cursor: pointer; z-index: 2;
}
.video-container video { width: 100%; height: 100%; display: block; object-fit: contain; background: #000; }

.image-container { position: relative; background: #000; aspect-ratio: 16/9; overflow: hidden; }
.image-container img { width: 100%; height: 100%; display: block; object-fit: contain; background: #000; }

.gallery-card.generating .card-actions { opacity: 0.4; pointer-events: none; }

.video-placeholder {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    aspect-ratio: 16/9;
    width: 100%;
    background: var(--bg-input);
    color: var(--text-dim);
    gap: 8px;
}
.video-placeholder.failed { color: var(--danger); }
.placeholder-icon { font-size: 24px; }

.card-body { padding: 12px; display: flex; flex-direction: column; flex: 1; }
/* Name row — used for project names, sub-project names, video names */
.name-row {
    display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 4px;
}
.name-text {
    font-size: 14px; font-weight: 600; color: var(--accent);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
.card-prompt-area { margin-bottom: 8px; display: flex; align-items: flex-start; gap: 6px; }
.card-prompt {
    font-size: 13px;
    color: var(--text);
    line-height: 1.4;
    flex: 1; min-width: 0;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
.card-prompt.truncated { cursor: pointer; }
.card-prompt.expanded { -webkit-line-clamp: unset; user-select: text; cursor: text; }
.prompt-copy-btn {
    background: none; border: none; color: var(--text-dim); cursor: pointer;
    font-size: 14px; padding: 0; flex-shrink: 0; margin-top: 1px;
}
.prompt-copy-btn:hover { color: var(--accent); }
.prompt-copy-btn.copied { color: var(--success); }
.card-badges { display: flex; flex-wrap: nowrap; gap: 4px; margin-bottom: 8px; overflow: hidden; }
.badge {
    font-size: 11px;
    padding: 2px 6px;
    border-radius: 4px;
    background: var(--bg-input);
    color: var(--text-dim);
    border: 1px solid var(--border);
}
.badge-seed { cursor: pointer; }
.badge-seed:hover { border-color: var(--accent); color: var(--accent); }
.badge-seed.copied { border-color: var(--success); color: var(--success); }
.badge.fast { background: rgba(245,158,11,0.15); color: var(--warning); border-color: transparent; }
.badge.standard { background: rgba(99,102,241,0.15); color: var(--accent); border-color: transparent; }
.badge.nb2, .badge.sd2 { background: rgba(107,143,74,0.2); color: var(--accent); border-color: transparent; }
.badge.wan { background: rgba(181,136,85,0.22); color: #d7a86e; border-color: transparent; }
.badge.kling { background: rgba(180,86,158,0.22); color: #e28fc5; border-color: transparent; }
.badge.veo { background: rgba(86,130,205,0.22); color: #8cb5f2; border-color: transparent; }
.badge.edit { background: rgba(160,160,160,0.18); color: var(--text-dim); border-color: transparent; text-transform: uppercase; letter-spacing: 0.04em; }
.badge.upload-source { background: rgba(120,160,220,0.22); color: #9cc0ea; border-color: transparent; }

/* Engine selector (inside Video tab, full-width above ref zone) */
.engine-select {
    display: block;
    width: 100%;
    height: 32px;
    padding: 0 32px 0 10px;
    margin-bottom: 8px;
    background-color: var(--bg-input);
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8' fill='none' stroke='%238a8a8a' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'><polyline points='1,2 6,7 11,2'/></svg>");
    background-repeat: no-repeat;
    background-position: right 12px center;
    background-size: 10px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text);
    font-size: 13px;
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    cursor: pointer;
}

/* Card bottom alignment */
.card-bottom { margin-top: auto; }

/* Prompt placeholder for assets without descriptions */
.card-prompt-placeholder {
    font-size: 13px; color: var(--text-dim); opacity: 0.5;
    line-height: 1.4; cursor: pointer; font-style: italic;
    min-height: 2.8em; display: flex; align-items: center;
}
.card-prompt-placeholder:hover { opacity: 0.8; color: var(--accent); }

.card-meta { margin-bottom: 8px; }
.card-time { font-size: 12px; color: var(--text-dim); }

.card-actions { display: flex; gap: 8px; justify-content: space-between; align-items: center; }
.card-actions-left { display: flex; gap: 8px; align-items: center; }
.card-actions-right { display: flex; gap: 8px; align-items: center; }
.btn {
    padding: 6px 14px;
    border-radius: var(--radius);
    font-size: 13px;
    cursor: pointer;
    border: 1px solid var(--border);
    background: var(--bg-input);
    color: var(--text);
    text-decoration: none;
    transition: all 0.15s;
}
.btn:hover { background: var(--bg-hover); color: var(--text); }
.btn-sm { padding: 4px 10px; font-size: 12px; }
.btn-danger { color: var(--danger); border-color: rgba(239,68,68,0.3); }
.btn-danger:hover { background: rgba(239,68,68,0.1); color: var(--danger-hover); }
.btn-icon {
    display: flex; align-items: center; justify-content: center;
    width: 30px; height: 30px; border-radius: var(--radius);
    border: 1px solid var(--border); background: var(--bg-input);
    color: var(--text-dim); font-size: 18px; text-decoration: none;
    cursor: pointer; transition: all 0.15s;
}
.btn-icon:hover { background: var(--bg-hover); color: var(--accent); border-color: var(--accent); }

/* Like/heart button */
.like-btn {
    position: absolute; bottom: 6px; right: 6px;
    background: rgba(0,0,0,0.4); border: none; color: var(--text-dim);
    width: 26px; height: 26px; border-radius: 50%; cursor: pointer;
    font-size: 14px; display: flex; align-items: center; justify-content: center;
    opacity: 0; transition: all 0.15s;
}
.asset-card:hover .like-btn, .gallery-card:hover .like-btn { opacity: 1; }
.like-btn.liked { opacity: 1; color: #e74c3c; background: rgba(0,0,0,0.5); }
.like-btn:hover { color: #e74c3c; opacity: 1; }
/* For generated video cards, position in the actions row instead */
.card-actions .like-btn {
    position: static; opacity: 1; background: var(--bg-input);
    border: 1px solid var(--border); width: 30px; height: 30px;
}
.card-actions .like-btn:hover { border-color: #e74c3c; }

/* Filter buttons in tabs */
.filter-btn {
    flex-shrink: 0;
    display: inline-flex; align-items: center; justify-content: center;
    background: transparent; border: none;
    color: var(--text-dim); font-size: 16px; line-height: 1; cursor: pointer;
    height: 36px; padding: 0 10px; border-radius: 6px; transition: all 0.15s;
}
.like-filter-btn:hover { color: #e74c3c; }
.like-filter-btn.active { color: #e74c3c; background: rgba(231,76,60,0.1); }
/* Filter cluster (Filter input + ✦ + ♥) sits right-aligned within the tab
   row, with a uniform 12px internal gap that's independent of the 4px gap
   used between tab buttons. The `.tabs-spacer` before it absorbs any free
   horizontal space (right-alignment) while guaranteeing a minimum 12px gap
   from the last tab when the drawer is tight — flex-basis 4px + the row's
   two 4px gaps flanking it equal a 12px visual separator. */
.tab-filter-group {
    display: flex; align-items: center; gap: 12px;
    flex-shrink: 0;
}
.tabs-spacer { flex: 1 1 4px; min-width: 4px; }

/* ---------- Responsive filter overflow (narrow ASSETS drawer) ----------
   Container query on `.asset-panel` swaps the inline filter cluster for
   a single `+` overflow button when the drawer is too narrow to show the
   full cluster inline alongside all 6 tabs. Click → popover with the same
   three controls (moved in DOM by JS so wiring + IDs stay stable). */
.asset-panel { container-type: inline-size; container-name: assets; }
.filter-overflow-btn {
    display: none;
    flex-shrink: 0;
    align-items: center; justify-content: center;
    height: 36px; padding: 0 16px;      /* matches .project-tab typographic rhythm */
    background: transparent; border: none;
    color: var(--text-dim); font-size: 13px; line-height: 1;
    border-radius: 6px; cursor: pointer;
    transition: color 0.15s, background 0.15s;
}
.filter-overflow-btn:hover { color: var(--text); background: var(--bg-hover); }
.filter-overflow-btn[aria-expanded="true"] { color: var(--text); background: var(--bg-hover); }
.filter-overflow-popover {
    position: absolute;
    top: calc(100% + 4px);
    right: 4px;
    display: flex; align-items: center; gap: 12px;
    padding: 8px; z-index: 30;
    background: var(--bg-card); border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: 0 6px 18px rgba(0,0,0,0.35);
}
.filter-overflow-popover.hidden { display: none; }

@container assets (max-width: 880px) {
    /* Inline filter cluster hides; spacer stays so the Filter button still
       bottoms out 12px (spacer 4 + two 4px row gaps) from the All tab, then
       the drawer can't shrink further without overflowing. */
    .asset-panel .tab-filter-group    { display: none; }
    .asset-panel .filter-overflow-btn { display: inline-flex; }
}

.gallery-search {
    /* Fixed 200px — `flex: 0 0 200px` prevents the input from shrinking
       when the drawer is tight; row overflows instead of squeezing. */
    flex: 0 0 200px;
    height: 36px;
    padding: 0 4px 0 10px;  /* tighter right padding so the × sits close to the edge */
    background: var(--bg-input); border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text); font-size: 13px; line-height: 20px;
}
.gallery-search:focus { outline: none; border-color: var(--accent); }
.gallery-search::placeholder { color: var(--text-dim); }
/* Native WebKit/Blink search-input clear button is a default-blue raster glyph.
   Replace with a text-dim `×` drawn via a data-URL SVG mask so it matches the
   rest of the app's icon vocabulary. */
.gallery-search::-webkit-search-cancel-button {
    -webkit-appearance: none;
    appearance: none;
    /* Hit target 24×24; glyph stays 12×12 centred inside via explicit mask-size.
       Bigger button = easier to click without growing the visible X. */
    width: 24px;
    height: 24px;
    margin-left: 2px;
    cursor: pointer;
    background: currentColor;
    color: var(--text-dim);
    -webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'><path d='M3 3l8 8M11 3l-8 8' stroke='black' stroke-width='1.6' stroke-linecap='round' fill='none'/></svg>") no-repeat center / 12px 12px;
            mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'><path d='M3 3l8 8M11 3l-8 8' stroke='black' stroke-width='1.6' stroke-linecap='round' fill='none'/></svg>") no-repeat center / 12px 12px;
}
.gallery-search::-webkit-search-cancel-button:hover { color: var(--text); }
.generated-filter-btn:hover { color: var(--warning); }
.generated-filter-btn.active { color: var(--warning); background: rgba(124,92,191,0.15); }

/* ---------- Projects Page ---------- */
.projects-page { max-width: 1000px; margin: 0 auto; }
.projects-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
.projects-header h2 { margin: 0; }
.new-project-form { display: flex; gap: 8px; }
.new-project-input {
    padding: 8px 12px; background: var(--bg-input); border: 1px solid var(--border);
    border-radius: var(--radius); color: var(--text); font-size: 14px; width: 240px;
}
.new-project-input:focus { outline: none; border-color: var(--accent); }
.btn-accent {
    padding: 8px 16px; background: var(--accent); color: white; border: none;
    border-radius: var(--radius); cursor: pointer; font-size: 14px;
}
.btn-accent:hover { background: var(--accent-hover); }

.projects-grid {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 16px;
}
.project-card-wrapper { position: relative; }
.project-card-wrapper .rename-btn {
    position: absolute; top: 12px; right: 12px; opacity: 0; transition: opacity 0.15s;
}
.project-card-wrapper:hover .rename-btn { opacity: 0.7; }
.project-card {
    display: block; padding: 20px; background: var(--bg-card); border: 1px solid var(--border);
    border-radius: var(--radius-lg); text-decoration: none; color: var(--text);
    transition: all 0.15s;
}
.project-card:hover { border-color: var(--accent); color: var(--text); }
.project-name { font-size: 18px; margin-bottom: 8px; }
.project-stats { display: flex; gap: 12px; font-size: 13px; color: var(--text-dim); margin-bottom: 6px; }
.project-date { font-size: 12px; color: var(--text-dim); opacity: 0.7; }

/* ---------- Unified Layout (3-column, resizable) ---------- */
.unified-layout {
    display: flex;
    gap: 0;
    height: calc(100vh - 56px);
    /* Horizontal scroll: when the user opens multiple drawers wide, the row
       can overflow the viewport. Scroll happens INSIDE the layout, so the
       top nav stays fixed above. Per-drawer MIN/MAX widths are enforced in
       JS (initDrawerPanels → MIN_EXPANDED / MAX_EXPANDED). */
    overflow-x: auto;
    overflow-y: hidden;
}

.unified-layout.dragging { user-select: none; cursor: col-resize; }
.unified-layout.dragging iframe,
.unified-layout.dragging video { pointer-events: none; }

/* (Old layout-slide-in-from-right keyframe removed — superseded by the
   cross-document View Transitions animation defined near the top of this
   file (search ::view-transition-old). VT runs on every same-origin
   navigation and morphs the whole document, including new-project
   bootstrap, so the per-page entry animation is no longer needed. The
   `entry_from_home` server flag is left in place for future use. */
/* Trailing resizer (after ASSETS) — same 4px strip, just has no right
   neighbour. Drag extends ASSETS to the right; the overflow-x on the
   layout takes care of the rest. */
.panel-resizer-trailing { flex: 0 0 4px; }

/* ---------- Drawer panel (shared base for AGENT · WRITE · GENERATE · EDIT · ASSETS) ---------- */
/* Drawers start with transitions disabled so the pre-paint state restore
   (inline script in project_detail.html) doesn't animate between the server's
   hardcoded defaults and the user's saved widths on every navigation. JS
   re-enables transitions by adding `drawers-ready` to <html> after one
   frame, so user-initiated open/close/drag still animates smoothly. */
.drawer-panel {
    flex-shrink: 0;
    background: var(--bg-card);
    border-right: 1px solid var(--border);
    height: calc(100vh - 56px);
    display: flex; flex-direction: column;
    overflow: hidden;
    /* position: relative anchors the absolutely-positioned
       .drawer-rail-hide-btn injected inside each non-AGENT rail. */
    position: relative;
}
html.drawers-ready .drawer-panel { transition: width 180ms ease-out; }
.drawer-panel:last-child { border-right: none; }
.unified-layout.dragging .drawer-panel { transition: none; }

.drawer-panel[data-state="rail"] { width: 44px; }
.drawer-panel[data-state="expanded"] { width: var(--drawer-width, 320px); }
.drawer-panel[data-state="rail"] .drawer-expanded { display: none; }
.drawer-panel[data-state="expanded"] .drawer-rail-btn { display: none; }

.drawer-rail-btn {
    width: 100%; height: 100%;
    background: transparent; border: none;
    color: var(--text-dim);
    cursor: pointer;
    display: flex; flex-direction: column; align-items: center; justify-content: flex-start;
    /* Padding-top matches .drawer-header padding-top (0) so the ✦ sits at the
       exact same y-position (12px from the top of the panel) in both rail and
       expanded states. */
    padding: 0 0 18px 0;
    gap: 14px;
    transition: color 0.15s, background 0.15s;
    overflow: visible;
}
.drawer-rail-btn:hover { color: var(--accent); background: var(--bg-input); }

/* Rail hide button — injected by initDrawerPanels for every non-AGENT
   drawer. Pinned at the bottom of the 44px rail; matches .agent-upload-btn
   geometry exactly (30×30 square, 1px border, 6px radius, text-dim → accent
   on hover) so the rail's `−` and the composer's `+` read as a matched
   pair. Only rendered when the drawer is at rail state — CSS hides it the
   moment the drawer is expanded or hidden. */
.drawer-rail-hide-btn {
    position: absolute;
    left: 7px;    /* (44 rail - 30 btn) / 2 = 7 — centred horizontally */
    bottom: 8px;  /* mirrors .agent-upload-btn bottom offset */
    width: 30px; height: 30px;
    background: transparent;
    color: var(--text-dim);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 0;
    cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    font: 400 18px/1 -apple-system, "system-ui", "Segoe UI", Roboto, sans-serif;
    transition: color 0.15s, border-color 0.15s, background 0.15s;
    z-index: 3;
}
.drawer-rail-hide-btn:hover {
    color: var(--accent);
    border-color: var(--accent);
    background: rgba(160, 182, 140, 0.08);
}
/* Only show when the drawer is at rail state (hidden when expanded). */
.drawer-panel[data-state="expanded"] .drawer-rail-hide-btn,
.drawer-panel[data-agent-hidden="true"] .drawer-rail-hide-btn { display: none; }

/* Rail drag handle — sits ABOVE the hide button so the rail's bottom
   cluster reads top-to-bottom: ≡ (drag) → − (hide). Same 30×30 box
   geometry as .drawer-rail-hide-btn so the two stack cleanly. Glyph
   matches the .subfolder-grip used in BREAKDOWN folder reordering for
   visual consistency. Only visible at rail state. */
.drawer-rail-drag-btn {
    position: absolute;
    left: 7px;
    bottom: 46px;  /* 8 (hide bottom) + 30 (hide height) + 8 (gap) */
    width: 30px; height: 30px;
    background: transparent;
    color: var(--text-dim);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 0;
    cursor: grab;
    display: flex; align-items: center; justify-content: center;
    transition: color 0.15s, border-color 0.15s, background 0.15s;
    z-index: 3;
    user-select: none;
}
.drawer-rail-drag-btn::before {
    content: "\2261";  /* ≡ U+2261 — matches .subfolder-grip */
    display: block;
    font: 400 16px/1 -apple-system, "system-ui", "Segoe UI", Roboto, sans-serif;
    /* The ≡ glyph sits ~2px low in most fonts. Lift it for true vertical
       centring (matches the .agent-upload-btn::before nudge pattern). */
    transform: translateY(-2px);
}
.drawer-rail-drag-btn:hover {
    color: var(--accent);
    border-color: var(--accent);
    background: rgba(160, 182, 140, 0.08);
}
.unified-layout.drag-reordering .drawer-rail-drag-btn { cursor: grabbing; }
.drawer-panel[data-state="expanded"] .drawer-rail-drag-btn,
.drawer-panel[data-agent-hidden="true"] .drawer-rail-drag-btn { display: none; }
/* The dragged rail dims so the user can see it's the one moving. */
.drawer-panel.is-dragging-source { opacity: 0.4; }
/* Drop indicator — a 3px accent vertical bar appended to <body> by JS,
   positioned via fixed coords on each pointermove. */
.drawer-drop-indicator {
    position: fixed;
    width: 3px;
    background: var(--accent);
    pointer-events: none;
    z-index: 9999;
    border-radius: 2px;
    box-shadow: 0 0 6px rgba(160, 182, 140, 0.5);
    display: none;
}
.drawer-drop-indicator.is-active { display: block; }
.drawer-rail-btn .drawer-glyph {
    font-size: 20px; line-height: 1;
    /* Match the drawer-header layout: top of star sits 12px above the leader
       line (which in rail state doesn't render, but the spacing matches so the
       glyph y-position is stable across the toggle). */
    margin-top: 12px;
}
.drawer-rail-label {
    writing-mode: vertical-rl; transform: rotate(180deg);
    font-size: 10px; letter-spacing: 3px; text-transform: uppercase;
    opacity: 0.8;
}
/* (Compass drawer reverted to the standard text rail + title pattern;
   custom wordmark CSS removed. The compass-wordmark.png is still used in
   the WRITE / EDIT pills via .demo-compass-pill — those are unaffected.) */

.drawer-expanded {
    display: flex; flex-direction: column;
    height: 100%; width: 100%;
    min-width: 0;
}
/* Header: top of the ✦ glyph sits at y=12 (matching the rail-btn glyph y so
   the star doesn't jump when toggling state). Content area is set to the
   glyph's height (20px) so align-items: center vertically aligns the title
   text and chevron on the star's midline (y=22). No horizontal leader. */
.drawer-header {
    flex: 0 0 auto;
    height: 20px;
    padding: 12px 14px 0 14px;
    display: flex; align-items: center; gap: 10px;
    box-sizing: content-box;
    overflow: visible;
    position: relative;
    z-index: 2;
    cursor: pointer;  /* whole header clickable — collapses to rail */
}
.drawer-header:hover { background: rgba(255,255,255,0.02); }
.drawer-glyph-header {
    color: var(--accent); font-size: 20px; line-height: 1;
    flex-shrink: 0;
}
.drawer-title {
    font-size: 10px;
    letter-spacing: 3px;
    text-transform: uppercase;
    color: var(--text-dim);
    opacity: 0.8;
    line-height: 12px;
    white-space: nowrap;
}
.drawer-model {
    font-size: 8px; color: var(--text-dim);
    letter-spacing: 0.5px; text-transform: uppercase;
    padding: 0 4px; border: 1px solid var(--border); border-radius: 2px;
    line-height: 10px; height: 12px;
    display: flex; align-items: center;
}
.drawer-collapse-btn {
    margin-left: auto;
    background: transparent; border: none; color: var(--text-dim);
    font-size: 16px; line-height: 12px; cursor: pointer;
    width: 18px; height: 12px;
    display: flex; align-items: center; justify-content: center;
    padding: 0;
    transition: color 0.15s;
}
.drawer-collapse-btn:hover { color: var(--accent); }
.drawer-body {
    flex: 1 1 auto;
    min-height: 0;
    display: flex; flex-direction: column;
    overflow: hidden;
}

/* Fixed 12px gap between the drawer header and the scroll area. Lives on
   the agent-body (outside the scroll container), so chat content stops
   12px short of the header instead of scrolling up through the gap. */
.drawer-body.agent-body { padding-top: 12px; }

/* BREAKDOWN drawer — wraps the project home chip + subfolder list (moved
   from the old GENERATE bottom pane). Mirrors .gen-panel-bottom's padding
   so the chips feel the same size/position as before. */
.drawer-body.breakdown-body { padding: 12px 16px 16px 16px; }

.agent-messages {
    flex: 1 1 auto;
    overflow-y: auto;
    /* No top padding — the 12px breathing room sits above this on the body.
       Side + bottom only, so scrolled content fills the panel cleanly. */
    padding: 0 14px 18px 14px;
    display: flex; flex-direction: column; gap: 18px;
}
.agent-msg { font-size: 13px; line-height: 1.55; max-width: 92%; }
.agent-msg-assistant {
    align-self: flex-start;
    display: flex; gap: 10px;
}
.agent-msg-assistant .agent-glyph-inline {
    color: var(--accent); flex-shrink: 0;
    /* line-height picked to match the body-text line-box exactly
       (13px * 1.55 = 20.15px → 14px * 1.44 ≈ 20.15px) so the glyph's
       vertical centre lines up with the first line's text centre. */
    font-size: 14px; line-height: 1.44;
    margin-top: 0;
}
/* Strip any top margin on the first child of the body so the first line
   sits at the same y as the glyph's line box — matters for replies that
   open with a list, tool card, or any block that usually has a top margin. */
.agent-msg-assistant .agent-msg-body > :first-child { margin-top: 0; }
.agent-msg-assistant .agent-msg-body p {
    margin: 0 0 6px 0; color: var(--text);
}
.agent-msg-assistant .agent-msg-body p:last-child { margin-bottom: 0; }

/* Inline markdown inside assistant replies. Bold uses the existing text
   weight stack; italic is italic; inline code gets a faint bg chip + a
   monospace stack. Lists drop the default user-agent padding a touch so
   they sit comfortably inside the chat bubble. */
.agent-msg-body code {
    font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
    font-size: 0.92em;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 3px;
    padding: 1px 4px;
}
.agent-msg-body strong { font-weight: 600; color: var(--text); }
.agent-msg-body em { font-style: italic; }
.agent-msg-body ul,
.agent-msg-body ol {
    margin: 4px 0 6px 0;
    padding-left: 20px;
}
.agent-msg-body ul:last-child,
.agent-msg-body ol:last-child { margin-bottom: 0; }
.agent-msg-body li { margin-bottom: 2px; }
.agent-msg-body li:last-child { margin-bottom: 0; }

.agent-msg-user {
    align-self: flex-end;
    background: var(--bg-input);
    padding: 9px 12px;
    border-radius: 12px 12px 3px 12px;
}
.agent-msg-user .agent-msg-body p { margin: 0 0 4px 0; color: var(--text); }
.agent-msg-user .agent-msg-body p:last-child { margin-bottom: 0; }
/* Pills inside a user bubble are rendered readonly (no × button). They sit on
   the bubble's bg-input fill, so bump their border/background one step darker
   for visual separation. Clickable — opens the media popover. */
.agent-msg-user .mention-pill { background: var(--bg-card); border-color: var(--border); cursor: pointer; }
.agent-msg-user .mention-pill:hover { border-color: var(--accent); }
.agent-msg-assistant .mention-pill { cursor: pointer; }
.agent-msg-assistant .mention-pill:hover { border-color: var(--accent); }

/* Pending-assistant state while awaiting the /agent/chat round-trip.
   Same font-size as the static star (14px inherited) so removing the class
   doesn't snap the glyph to a different size. Keyframes start and end at
   scale(1) / opacity(1) — matching the static state — so the handoff from
   animated to locked is visually imperceptible. Pulse is a subtle midpoint
   dim + swell. inline-block is required for transform to apply to a span. */
.agent-msg-pending .agent-glyph-inline {
    animation: agent-glyph-pulse 1.2s ease-in-out infinite;
    transform-origin: center center;
    display: inline-block;
}
@keyframes agent-glyph-pulse {
    0%, 100% { opacity: 1;    transform: scale(1); }
    50%      { opacity: 0.45; transform: scale(1.08); }
}

/* One-line italic breadcrumb — no glyph, no border, slightly smaller + faint.
   Running state fades opacity; on `done` the card is removed by JS. */
.agent-tool-card {
    margin: 2px 0;
    padding: 0;
    font-size: 12px; line-height: 1.55;
    color: var(--text-dim);
    opacity: 0.7;
    font-style: italic;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.agent-tool-running {
    animation: agent-tool-fade 1.1s ease-in-out infinite;
}
@keyframes agent-tool-fade {
    0%, 100% { opacity: 0.35; }
    50%      { opacity: 0.85; }
}

/* Error line inside a streaming bubble when the SSE stream fails mid-reply. */
.agent-error-line { color: var(--text-dim); font-style: italic; }

/* Marker appended to a streaming bubble when the user hit Stop mid-reply.
   Same faint grey as the thinking lines so it reads as metadata, not content. */
.agent-stopped-line { color: var(--text-dim); font-style: italic; font-size: 11px; opacity: 0.7; margin-top: 4px; }

/* Accent pulse on the prompt editor when Claude writes into it — a visible
   "the agent just did this" cue without being shouty. */
.mention-editor.agent-flash {
    animation: agent-flash 600ms ease-out;
}
@keyframes agent-flash {
    0%   { box-shadow: 0 0 0 0 rgba(107, 143, 74, 0.6); }
    60%  { box-shadow: 0 0 0 4px rgba(107, 143, 74, 0.18); }
    100% { box-shadow: 0 0 0 0 rgba(107, 143, 74, 0); }
}

/* Footer holds the input wrapper. No separator line above — visual breathing
   room around the wrapper instead, matching the Generate-button gutter. */
.agent-input-footer {
    flex: 0 0 auto;
    padding: 12px;
}
/* Wrapper styled like the main prompt input — bg-input fill, border. Border
   only goes accent when something is being dragged onto the editor (a clear,
   intentional action); typing into the editor leaves the border alone so the
   composer reads as a calm, present input rather than a focused form field.
   Drag-in is signalled by the .dragover class JS adds to the inner editor;
   :has() lifts that to the wrap so the whole shell lights up. */
.agent-input-wrap {
    position: relative;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    transition: border-color 0.12s;
}
.agent-input-wrap:has(.mention-editor.dragover) { border-color: var(--accent); }
.agent-input {
    /* Padding-bottom reserves space for the absolutely-positioned send button
       so typed text never runs under it. */
    padding: 10px 12px 44px 12px;
    min-height: 108px;     /* ~3× the previous 36px */
    max-height: 240px;
    color: var(--text); font-size: 13px; line-height: 1.5;
    overflow-y: auto;
    cursor: text;
    /* mention-editor defaults `min-height: 0` and `flex: 1` — override so the
       agent input keeps its explicit min-height. */
    flex: none;
    white-space: pre-wrap;
    word-break: break-word;
}
/* Mention-editor placeholder is absolute-positioned via .is-empty::before.
   Override left/top so it lines up with this editor's padding (12px 12px). */
.agent-input.mention-editor.is-empty::before {
    left: 12px;
    top: 10px;
    right: 44px;  /* keep clear of the send button's corner */
}

/* Flip the @mention dropdown to open UPWARD — the agent input sits at the
   bottom of the panel, so downward would go off-screen. */
#agent-mention-dropdown {
    top: auto;
    bottom: 100%;
    margin-bottom: 4px;
}
/* Dual-state right-hand button — same shell as the + on the left when the
   editor is empty (audio-mode, future "speak to the agent"), swapping to the
   filled accent ↑ send button the moment the user types. Stop mode during
   an in-flight SSE reply shows a red-neutral ■. Glyph is injected via JS
   innerHTML so we can swap between <svg>, ↑, and ■. */
.agent-send-btn {
    position: absolute;
    right: 8px; bottom: 8px;
    width: 30px; height: 30px;
    border-radius: 6px;
    padding: 0;
    cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    transition: color 0.15s, border-color 0.15s, background 0.15s;
    /* Default = audio-mode: minimal transparent shell, muted waveform. */
    background: transparent;
    color: var(--text-dim);
    border: 1px solid var(--border);
    font: 400 18px/1 -apple-system, "system-ui", "Segoe UI", Roboto, sans-serif;
}
.agent-send-btn:hover {
    color: var(--accent);
    border-color: var(--accent);
    background: rgba(160, 182, 140, 0.08);
}
.agent-send-btn svg { display: block; }
/* Send mode — editor has content. Filled accent + white ↑. */
.agent-send-btn.is-ready {
    background: var(--accent);
    color: white;
    border-color: transparent;
}
.agent-send-btn.is-ready:hover {
    background: var(--accent-hover);
    color: white;
    border-color: transparent;
}
/* Stop mode — active while an SSE reply is streaming. Same real estate and
   colour as send; only the glyph switches to a filled square. */
.agent-send-btn.is-stop {
    background: var(--accent);
    color: white;
    border-color: transparent;
    font-size: 14px;
}
.agent-send-btn.is-stop:hover { filter: brightness(1.1); }

/* "New chat" text link in the drawer header — sits after the model pill
   via the header's flex row. Hidden until there's at least one message in
   history (set in JS). Intentionally small + dim so it doesn't compete with
   the title; hover colour is the usual accent. */
.agent-new-chat-btn {
    background: transparent; border: none; padding: 0;
    color: var(--text-dim);
    font-size: 8px; letter-spacing: 0.5px; text-transform: uppercase;
    line-height: 10px; height: 12px;
    cursor: pointer;
    display: flex; align-items: center;
    /* Optical kerning: nudge 1px down so the cap-height visually lines up
       with the CLAUDE 4.7 pill's text baseline next to it. */
    transform: translateY(1px);
}
.agent-new-chat-btn:hover { color: var(--accent); }
.agent-new-chat-btn[hidden] { display: none; }

/* Hit zone wrapping the Claude pill + New chat button. Padded margins
   extend the clickable area beyond the visible text so near-miss clicks
   still land (JS routes them to the new-chat handler and eats bubbling
   so the drawer-header's collapse handler doesn't fire). Negative margin
   preserves the visible layout. */
.agent-header-actions {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    padding: 8px 12px;
    margin: -8px -12px;
}
.agent-header-actions:hover .agent-new-chat-btn:not([hidden]) {
    color: var(--accent);
}

/* "+" upload button pinned bottom-left of the input wrap — mirrors the
   send button's corner geometry so the footer feels balanced. Opens a
   file picker for image/video/audio (straight to the library) or writing
   files (extracted inline as an ephemeral "context only" pill). */
.agent-upload-btn {
    position: absolute;
    left: 8px; bottom: 8px;
    width: 30px; height: 30px;
    background: transparent;
    color: var(--text-dim);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 0;
    cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.agent-upload-btn::before {
    content: "+";
    display: block;
    font: 400 18px/1 -apple-system, "system-ui", "Segoe UI", Roboto, sans-serif;
    transform: translateY(-1px);
}
.agent-upload-btn:hover {
    color: var(--accent);
    border-color: var(--accent);
    background: rgba(160, 182, 140, 0.08);
}
.agent-upload-btn.is-uploading { opacity: 0.5; cursor: progress; }
.agent-upload-btn.is-uploading::before { content: ""; }

/* Model pill — sits inside the composer's input wrap, just left of the
   send/audio button at the bottom-right. Vertically centred on the visual
   midline of the + and send buttons (both 30px tall pinned at bottom: 8px
   ⇒ midline is at bottom: 23px). The 12px-tall pill lands at bottom: 17px
   to share that midline. Right offset of 46px clears the 30px send button
   + an 8px gap. Visual style mirrors the old .drawer-model header pill. */
.composer-model-pill {
    position: absolute;
    right: 46px;
    bottom: 17px;
    font-size: 8px;
    color: var(--text-dim);
    letter-spacing: 0.5px;
    text-transform: uppercase;
    padding: 0 4px;
    border: 1px solid var(--border);
    border-radius: 2px;
    line-height: 10px;
    height: 12px;
    display: flex;
    align-items: center;
    pointer-events: none;
    user-select: none;
    white-space: nowrap;
}

/* Ephemeral "text excerpt" pill — dashed accent border distinguishes it
   from real library refs; the 📄 icon + tooltip say "context only, lives
   only in this chat". Popover shows a preview of the extracted text. */
.mention-pill.is-text-excerpt {
    background: rgba(160, 182, 140, 0.10);
    border: 1px dashed rgba(160, 182, 140, 0.55);
}

/* ---------- Resizer ---------- */
.panel-resizer {
    flex: 0 0 4px;
    background: transparent;
    position: relative;
    cursor: col-resize;
    z-index: 10;
    transition: background 0.12s ease-out;
}
.panel-resizer::before {
    content: '';
    position: absolute;
    top: 0; bottom: 0;
    left: -3px; right: -3px;
}
.panel-resizer:hover,
.panel-resizer.panel-resizer-active {
    background: var(--accent);
}
/* Hide a resizer when either adjacent drawer is in rail state — the rail is
   thin enough that dragging it feels wrong; use the header click to toggle. */
.panel-resizer[data-hidden="true"] { display: none; }

/* ---------- Gen panel (drawer body — Generate drawer) ---------- */
/* .gen-panel now sits on the drawer-body, so it inherits flex from .drawer-body.
   This rule only defines inner layout; outer width/height/background live on
   .drawer-panel. */
.gen-panel {
    display: flex; flex-direction: column;
    height: 100%; width: 100%;
    overflow: hidden;
}
.gen-panel-top {
    flex: 0 0 auto;
    padding: 12px 16px 8px 16px;
}
.gen-panel-bottom {
    flex: 1 1 auto; min-height: 140px;
    padding: 8px 16px 16px 16px;
    border-top: 1px solid var(--border);
    display: flex; flex-direction: column;
    overflow: hidden;
}
/* ---------- Asset panel (drawer body — Assets drawer) ---------- */
.asset-panel {
    padding: 0 20px; overflow-y: auto; height: 100%; width: 100%;
}
/* Tabs are the first thing in the scroll container so they sit at y=0 and
   stick at top: 0 without any initial translation. The old 12px margin-top
   was the cause of the header appearing to drift up during the first 12px
   of scroll before the sticky rule engaged. */
.asset-panel .project-tabs { margin-top: 0; }
.asset-panel .project-tab-content { padding-top: 0; }

/* Gen panel header */
.gen-header { margin-bottom: 0; }
.gen-divider { border: none; border-top: 1px solid var(--border); margin: 12px 0; }
/* Home chip (project name, pinned above the scrollable folder list) */
.subfolder-home {
    flex-shrink: 0; margin-bottom: 6px;
    font-weight: 500;
}
.subfolder-home .subfolder-link { flex: 1; min-width: 0; }
.subfolder-home .subfolder-actions { opacity: 0.6; }
.subfolder-home:hover .subfolder-actions { opacity: 1; }
/* When the home chip is active (green background), keep icons fully opaque
   so white-on-green reads crisply — 0.6 × white-ish = washed-out grey. */
.subfolder-chip.subfolder-home.active .subfolder-actions { opacity: 1; }
.home-rename-btn, .home-delete-btn {
    background: none; border: none; color: var(--text-dim); cursor: pointer;
    font-size: 12px; padding: 2px 4px; line-height: 1;
}
.home-rename-btn:hover { color: var(--accent); }
.home-delete-btn:hover { color: var(--danger); }
.subfolder-chip.active .home-rename-btn,
.subfolder-chip.active .home-delete-btn { color: rgba(255,255,255,0.8); }
.subfolder-chip.active .home-rename-btn:hover { color: white; }
.subfolder-chip.active .home-delete-btn:hover { color: #fecaca; }

/* Sub-folder section (bottom pane) */
.subfolder-section {
    flex: 1 1 auto; min-height: 0;
    display: flex; flex-direction: column;
    margin-bottom: 12px;
}
.subfolder-section-header {
    flex-shrink: 0;
    display: flex; align-items: center; justify-content: space-between;
    font-size: 11px; color: var(--text-dim); text-transform: uppercase;
    letter-spacing: 0.5px; margin-bottom: 8px;
}
.new-subproject-form { flex-shrink: 0; }
.subfolder-list {
    flex: 1 1 auto; min-height: 0; overflow-y: auto;
    display: flex; flex-direction: column; gap: 3px;
    padding-right: 4px;  /* breathing room for the scrollbar */
}
.subfolder-chip {
    display: flex; align-items: center; gap: 6px;
    padding: 6px 8px; border-radius: var(--radius);
    color: var(--text-dim); font-size: 13px;
    text-decoration: none; cursor: pointer;
    border: 1px solid transparent;
    transition: background 0.1s, border-color 0.1s, color 0.1s;
}
.subfolder-chip:hover { background: var(--bg-hover); color: var(--text); }
.subfolder-chip.active { background: var(--accent); color: white; border-color: var(--accent); }
.subfolder-chip.active .subfolder-count { color: white; opacity: 0.9; }
.subfolder-all { font-weight: 500; }
/* Left-side icon/grip — same fixed width so every chip's content sits at
   an identical x-coordinate. Centre-align the glyph inside that slot. */
.subfolder-icon, .subfolder-grip {
    flex-shrink: 0; width: 16px;
    display: inline-flex; align-items: center; justify-content: center;
    padding: 0;
}
.subfolder-icon { font-size: 13px; opacity: 0.7; }
.subfolder-grip {
    cursor: grab; font-size: 14px; color: var(--text-dim);
    opacity: 0.5; user-select: none;
}
.subfolder-grip:active { cursor: grabbing; }
.subfolder-item:hover .subfolder-grip { opacity: 1; }
.subfolder-link {
    flex: 1; min-width: 0; display: flex; align-items: center; gap: 8px;
    color: inherit; text-decoration: none;
}
/* Global `a:hover { color: var(--accent-hover) }` would otherwise win on
   specificity and paint chip text light-olive — unreadable on an active
   (olive-background) chip. Pin chip-link colour to inherit in every state. */
.subfolder-chip .subfolder-link,
.subfolder-chip .subfolder-link:hover,
.subfolder-chip .subfolder-link:focus,
.subfolder-chip .subfolder-link:active { color: inherit; }
.subfolder-name {
    flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.subfolder-count {
    flex-shrink: 0; font-size: 11px; color: var(--text-dim); opacity: 0.8;
}
.subfolder-actions {
    display: flex; gap: 2px; flex-shrink: 0;
    opacity: 0; transition: opacity 0.1s;
}
.subfolder-item:hover .subfolder-actions { opacity: 1; }
/* Fixed-width slot so every action column (+, pencil, ×) lines up across
   every chip type. Placeholders (`<span>`) occupy the same width as buttons
   so a chip missing one still reserves the column and the count + other
   icons stay vertically aligned with their siblings. */
.subfolder-action-slot {
    width: 22px; height: 22px;
    display: inline-flex; align-items: center; justify-content: center;
    flex-shrink: 0;
    background: none; border: none; color: var(--text-dim); cursor: pointer;
    font-size: 13px; line-height: 1; padding: 0;
}
.subfolder-action-placeholder {
    pointer-events: none;
}
.subfolder-rename-btn:hover { color: var(--accent); }
.subfolder-delete-btn:hover { color: var(--danger); }
.subfolder-add-child-btn:hover { color: var(--accent); }
/* All three action-slot glyphs (+, ✎, ×) share the same font-size; the
   parent `.subfolder-action-slot` rule sets 15px so they read as one set. */
/* Sub-sub-folder: indented 12px under its parent sub-folder. */
.subfolder-chip.subfolder-nested { margin-left: 12px; }
/* Disclosure chevron — matches the visual language of the `‹` collapse
   icon in the drawer headers (U+2039 / U+203A). Collapsed state points right
   (`›`), expanded rotates 90° to point down. Sits to the left of the count
   so the folder name stays fixed when the chevron isn't rendered. Only
   emitted server-side on folders that actually have children. */
.subfolder-haskids {
    flex-shrink: 0;
    display: inline-flex; align-items: center; justify-content: center;
    width: 10px;                       /* subtle — matches count-glyph footprint */
    margin-right: 6px;
    font-size: 14px;
    line-height: 1;
    font-weight: 600;
    opacity: 0.55;
    transition: transform 0.15s ease, opacity 0.15s ease;
    transform-origin: center;
}
.subfolder-chip:not(.collapsed) .subfolder-haskids {
    transform: rotate(90deg);           /* expanded → points down */
}
.subfolder-chip.collapsed .subfolder-haskids {
    transform: none;                    /* collapsed → points right */
}
.subfolder-chip.active .subfolder-haskids { opacity: 0.9; }
/* Inline 'new sub-sub' input — appears beneath a sub chip while the user types. */
.new-subsubproject-form {
    display: flex; gap: 6px; margin-left: 12px; padding: 4px 8px;
    background: var(--bg-input); border-radius: var(--radius);
}
.new-subsubproject-form input {
    flex: 1; background: transparent; border: none; color: var(--text);
    font-size: 12px; outline: none;
}
.subfolder-chip.active .subfolder-rename-btn,
.subfolder-chip.active .subfolder-delete-btn,
.subfolder-chip.active .subfolder-add-child-btn { color: rgba(255,255,255,0.9); }
.subfolder-chip.active .subfolder-rename-btn:hover { color: white; }
.subfolder-chip.active .subfolder-delete-btn:hover { color: #fecaca; }
.subfolder-chip.active .subfolder-add-child-btn:hover { color: white; }

/* Drag states */
.subfolder-chip.dragover {
    background: rgba(107,143,74,0.25);
    border-color: var(--accent);
    color: var(--text);
}
.subfolder-chip.reorder-hover { border-top: 2px solid var(--accent); }
/* Trello-style drop-indicator bar. Absolutely-positioned inside the subfolder
   list (which becomes position:relative below) so it doesn't perturb the flex
   layout as the user drags. JS computes its Y on every dragover and sets
   `--y` in content-coords (accounting for scrollTop). */
#subfolder-list { position: relative; }
.subfolder-reorder-indicator {
    position: absolute;
    left: 2px; right: 6px;  /* account for padding-right scrollbar gap */
    height: 3px;
    top: var(--y, 0);
    transform: translateY(-1px);  /* sit visually in the 3px inter-chip gap */
    background: var(--accent);
    border-radius: 2px;
    box-shadow: 0 0 6px rgba(131,149,111,0.6);
    pointer-events: none;
    z-index: 5;
}
.subfolder-chip.dragging { opacity: 0.4; }
.subfolder-chip.drop-flash {
    animation: subfolder-drop-flash 0.4s ease-out;
}
@keyframes subfolder-drop-flash {
    0% { background: rgba(107,143,74,0.5); }
    100% { background: transparent; }
}

/* Remove-from-sub-folder button on tiles */
.gallery-card { position: relative; }
.sub-remove-btn {
    position: absolute; top: 6px; right: 6px; z-index: 3;
    width: 24px; height: 24px; border-radius: 50%;
    background: rgba(0,0,0,0.65); color: var(--text);
    border: none; cursor: pointer; font-size: 14px;
    display: flex; align-items: center; justify-content: center;
    opacity: 0; transition: opacity 0.1s, background 0.1s;
}
.gallery-card:hover .sub-remove-btn { opacity: 1; }
.sub-remove-btn:hover { background: var(--danger); color: white; }
.gen-controls { margin-bottom: 12px; }
.gen-control-row { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; flex-wrap: wrap; }
.gen-control-row.single { justify-content: center; }
.gen-ctrl { display: flex; flex-direction: column; align-items: center; }
.gen-ctrl label { font-size: 10px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3px; white-space: nowrap; }
.gen-ctrl select, .gen-ctrl input[type="number"] { height: 28px; padding: 0 6px; background: var(--bg-input); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); font-size: 12px; min-width: 64px; }
.gen-ctrl .seed-input { width: 60px; }
.gen-ctrl .toggle-group { height: 28px; }
.gen-ctrl .toggle-btn { padding: 3px 10px; font-size: 12px; }
.gen-ctrl .duration-control { height: 28px; }
.gen-ctrl .duration-control input[type="range"] { width: 70px; }
.gen-ctrl .count-btn { width: 24px; height: 24px; font-size: 14px; }
.gen-ctrl .switch { height: 28px; }

.gen-buttons { display: flex; gap: 8px; margin-bottom: 12px; align-items: stretch; }
.gen-buttons .generate-cluster { flex: 1 1 auto; }
.gen-buttons .reset-btn { padding: 10px 14px; height: auto; font-size: 14px; }

/* Generate cluster — Generate button is full width with text centred; rolls
   stepper overlays the right edge inside the same accent-coloured pill.
   Stepper buttons are siblings of the submit button (HTML doesn't allow nested
   <button> elements), positioned absolutely on top of it. Clicks on the
   stepper buttons fire their own handlers and never reach the submit. */
.generate-cluster {
    position: relative;
    width: 100%;
}
.generate-cluster .generate-btn {
    width: 100%;
    padding: 10px;
    font-size: 14px;
}
.generate-rolls {
    position: absolute;
    top: 50%; right: 6px;
    transform: translateY(-50%);
    display: flex; align-items: center; gap: 2px;
    padding: 2px;
    background: rgba(0, 0, 0, 0.18);
    border-radius: 6px;
}
.rolls-mini-btn {
    width: 22px; height: 22px;
    background: transparent; border: none;
    color: rgba(255, 255, 255, 0.85);
    font-size: 13px; font-weight: 600; line-height: 1;
    cursor: pointer;
    border-radius: 4px;
    display: flex; align-items: center; justify-content: center;
    transition: background 0.12s, color 0.12s;
}
.rolls-mini-btn:hover { background: rgba(255, 255, 255, 0.18); color: white; }
.rolls-mini-value {
    font-size: 13px; font-weight: 700;
    min-width: 14px; text-align: center;
    color: white;
    /* The number isn't clickable for submit (it sits on the stepper backdrop,
       not on the underlying button), but make the cursor reflect that explicitly. */
    cursor: default;
    user-select: none;
}

/* Engine row — select fills, Reset link on the right */
.engine-row { display: flex; gap: 8px; align-items: stretch; margin-bottom: 8px; }
.engine-row .engine-select { flex: 1; margin-bottom: 0; }
.reset-link {
    background: transparent;
    border: 1px solid var(--border);
    color: var(--text-dim);
    padding: 0 14px;
    font-size: 13px;
    border-radius: var(--radius);
    cursor: pointer;
    height: 32px;
    line-height: 30px;
    transition: color 0.1s, border-color 0.1s;
}
.reset-link:hover { color: var(--text); border-color: var(--accent); }

.mode-tabs.compact { margin-bottom: 12px; }
.mode-tabs.compact .mode-tab { padding: 8px 16px; font-size: 13px; }
.mode-tab.nb2-tab.active { background: var(--accent); }
.nb2-controls { margin-bottom: 12px; }

/* Compact prompt and drop zone */
.gen-panel .prompt-input { min-height: 60px; font-size: 13px; margin-bottom: 10px; }
.gen-panel .prompt-wrapper { height: 200px; margin-bottom: 8px; }
.gen-panel .generate-drop-zone { padding: 16px; margin-bottom: 0; }
.gen-panel .generate-drop-zone p { font-size: 14px; }
.gen-panel .ref-drop-compact { padding: 16px; }
.gen-panel .frame-uploads.compact { gap: 8px; margin-bottom: 8px; }
.gen-panel .frame-drop { padding: 16px; }
.gen-panel .frame-drop p { font-size: 14px; }
.or-divider {
    text-align: center; color: var(--text-dim); font-size: 11px;
    margin: 6px 0; letter-spacing: 1px; opacity: 0.6;
}

/* Legacy compat */
.project-layout { display: flex; gap: 0; min-height: calc(100vh - 56px); }
.project-sidebar {
    width: 220px; flex-shrink: 0; padding: 20px 16px;
    background: var(--bg-card); border-right: 1px solid var(--border);
}
.sidebar-back { font-size: 13px; color: var(--text-dim); display: block; margin-bottom: 16px; }
.sidebar-back:hover { color: var(--text); }
.sidebar-project-name { font-size: 14px; font-weight: 600; line-height: 1.3; }
.sidebar-name-row { margin-bottom: 16px; }
.sidebar-name-row h3 { margin: 0; }
.sidebar-generate-btn { display: block; width: 100%; text-align: center; margin-bottom: 20px; }

.rename-btn {
    background: none; border: none; color: var(--text-dim); cursor: pointer;
    font-size: 13px; padding: 2px 4px; opacity: 0.5; transition: opacity 0.15s;
}
.rename-btn:hover { opacity: 1; color: var(--accent); }

.sidebar-section { margin-top: 16px; }
.sidebar-section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; font-size: 12px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.5px; }
.sidebar-add-btn {
    width: 22px; height: 22px; border-radius: 4px; background: var(--bg-input);
    border: 1px solid var(--border); color: var(--text-dim); cursor: pointer;
    font-size: 14px; display: flex; align-items: center; justify-content: center;
}
.sidebar-add-btn:hover { border-color: var(--accent); color: var(--accent); }
.sidebar-input {
    width: 100%; padding: 4px 8px; background: var(--bg-input); border: 1px solid var(--border);
    border-radius: var(--radius); color: var(--text); font-size: 13px;
}

.subproject-list { display: flex; flex-direction: column; gap: 2px; }
.subproject-item-row { display: flex; align-items: center; gap: 4px; }
.subproject-item {
    display: flex; align-items: center; justify-content: space-between;
    padding: 6px 10px; border-radius: var(--radius); font-size: 13px;
    color: var(--text-dim); text-decoration: none; flex: 1;
}
.subproject-item:hover { background: var(--bg-hover); color: var(--text); }
.subproject-item.active { background: var(--accent); color: white; }
.subproject-name-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.subproject-count { font-size: 11px; opacity: 0.7; flex-shrink: 0; }
.subproject-delete-btn {
    background: none; border: none; color: var(--text-dim); cursor: pointer;
    font-size: 12px; padding: 2px 4px; opacity: 0.5;
}
.subproject-delete-btn:hover { color: var(--danger); opacity: 1; }

.new-subproject-form { display: flex; gap: 4px; margin-top: 6px; }
.new-subproject-form.hidden { display: none; }

.project-main { flex: 1; padding: 20px 24px; overflow-y: auto; }

/* Project breadcrumb */
.project-breadcrumb { font-size: 14px; color: var(--text-dim); margin-bottom: 16px; }
.project-breadcrumb a { color: var(--accent); }
.project-breadcrumb span { margin: 0 4px; }
.generate-breadcrumb { margin-bottom: 20px; }

/* Project tabs — sticky at top of asset panel */
.project-tabs {
    display: flex; gap: 4px; margin-bottom: 0; background: var(--bg-card);
    padding: 4px; border-radius: var(--radius);
    position: sticky; top: 0; z-index: 10;
}
/* Rectangular solid backdrop inside the tabs stacking context — fills the
   4 rounded-corner slivers left by border-radius so tiles scrolling behind
   don't peek through those transparent triangles. Paints BEHIND the tabs
   own bg (z-index: -1 within the stacking context) but still ABOVE any
   sibling tile content because the tabs context is z-index: 10. */
.project-tabs::before {
    content: "";
    position: absolute;
    inset: 0;
    background: var(--bg-card);
    z-index: -1;
}
.project-tab {
    display: inline-flex; align-items: center; justify-content: center;
    height: 36px; padding: 0 16px;
    /* Reserve width for a 3-digit count so the tab row doesn't reflow as
       counts tick up or down. `compact` filter shortens 4+ digit values to
       1.2K / 12K / 1.2M above 999, so this min-width stays adequate as
       libraries grow. `flex-shrink: 0` pins the tab at that width — when
       the drawer is narrower than the row, content overflows instead of
       squeezing individual tabs. */
    min-width: 90px;
    flex-shrink: 0;
    background: transparent; color: var(--text-dim); border: none;
    border-radius: 6px; cursor: pointer; font-size: 13px; line-height: 1; transition: all 0.15s;
}
.project-tab:hover { color: var(--text); }
.project-tab.active { background: var(--accent); color: white; }
.tab-count { font-size: 11px; opacity: 0.7; margin-left: 4px; }
.project-tab-content { display: none; }
.project-tab-content.active { display: block; }

.all-grid-truncated-note {
    margin-top: 12px;
    padding: 8px 12px;
    font-size: 12px;
    color: var(--text-dim);
    border: 1px dashed var(--border);
    border-radius: var(--radius);
    text-align: center;
}

/* Asset drop-zone backdrop — sticky header band behind the rounded dashed
   drop box. Carries: the sticky behaviour, the opaque solid bg that extends
   edge-to-edge (covering the drop zone's rounded-corner gaps), and the 12px
   buffer below the drop zone before tiles begin. The drop zone itself is
   then static inside this wrapper, with no sticky rules of its own.
   Extends -20px into .asset-panel's horizontal padding so occlusion is
   fully edge-to-edge. */
.asset-drop-band {
    position: sticky;
    top: 44px;  /* below the 44px tabs bar */
    z-index: 9;
    margin: 0 -20px;
    /* 12px padding top AND bottom — visible bg strips above + below the
       drop zone, both inside the sticky region so tiles scrolling past are
       occluded by solid var(--bg-card) on every side. */
    padding: 12px 20px 12px;
    background: var(--bg-card);
}
.asset-upload-zone {
    border: 2px dashed var(--border); border-radius: var(--radius-lg);
    padding: 16px; text-align: center; cursor: pointer; transition: all 0.2s;
    background: var(--bg);
}
.asset-upload-zone:hover, .asset-upload-zone.dragover {
    border-color: var(--accent); background: rgba(99,102,241,0.05);
}
/* Grid sits flush against the 12px built-in padding of .asset-drop-band. */
.asset-drop-band + .gallery-grid,
.asset-drop-band + .all-grid-truncated-note {
    margin-top: 0;
}

/* Asset grid */
.asset-grid {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 12px;
}
.asset-card {
    position: relative; background: var(--bg-input); border: 1px solid var(--border);
    border-radius: var(--radius); overflow: hidden; transition: border-color 0.15s;
}
.asset-card:hover { border-color: var(--accent); }
.asset-card[draggable] { cursor: grab; }
.asset-card[draggable]:active { cursor: grabbing; }
.gallery-card[draggable] { cursor: grab; }
.gallery-card[draggable]:active { cursor: grabbing; }
.asset-card .asset-preview-trigger {
    aspect-ratio: 16/9; overflow: hidden; background: var(--bg-input);
}
.asset-card img, .gallery-card .asset-preview-trigger img {
    width: 100%; height: 100%; object-fit: cover; display: block;
    opacity: 0; transition: opacity 0.15s;
}
.asset-card img.loaded, .gallery-card .asset-preview-trigger img.loaded { opacity: 1; }
/* Letterbox images in tiles — preserve aspect ratio with black bars on non-16:9
   sources (e.g. a 1:1 crop in a 16:9 tile). Overrides the object-fit:cover from
   the rule above by equal-specificity + later source order. The video tile gets
   this naturally because its .asset-preview-trigger sits on a sibling overlay,
   not on the .video-container itself. */
.gallery-card .image-container img { object-fit: contain; background: #000; }
.asset-icon {
    width: 100%; height: 120px; display: flex; align-items: center; justify-content: center;
    font-size: 32px; color: var(--text-dim); background: var(--bg-card);
}
.asset-info { padding: 8px; }
.asset-meta { font-size: 11px; color: var(--text-dim); }
/* Face flag icon */
.face-flag {
    position: absolute; top: 6px; left: 6px;
    width: 12px; height: 12px; border-radius: 50%;
    background: #b91c1c;
    cursor: help;
}

/* Clickable preview area */
.asset-preview-trigger { position: relative; cursor: pointer; }

/* Lightbox */
.preview-modal-content {
    background: var(--bg-card); border: 1px solid var(--border);
    border-radius: var(--radius-lg); padding: 16px; max-width: 80vw; max-height: 85vh;
    display: flex; flex-direction: column; align-items: center; position: relative;
    min-width: 320px;
}
.preview-modal-content img { max-width: 100%; max-height: 70vh; border-radius: var(--radius); }
.preview-body { display: flex; justify-content: center; align-items: center; min-height: 100px; }
.preview-footer { display: flex; justify-content: space-between; align-items: center; width: 100%; margin-top: 12px; padding: 0 4px; }
.preview-footer-left { display: flex; align-items: center; gap: 10px; min-width: 0; }
.preview-title { font-size: 14px; color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.preview-counter { font-size: 13px; color: var(--text-dim); opacity: 0.7; }
.preview-download { text-decoration: none; color: var(--text-dim); }
.preview-download:hover { color: var(--accent); }
.preview-close {
    position: fixed; top: 16px; right: 16px;
    background: rgba(255,255,255,0.15); border: none; color: white;
    width: 36px; height: 36px; border-radius: 50%;
    font-size: 22px; cursor: pointer; z-index: 210;
    display: flex; align-items: center; justify-content: center;
    backdrop-filter: blur(4px);
}
.preview-close:hover { background: rgba(255,255,255,0.3); }
.preview-close:hover { color: var(--text); }

/* Lightbox nav arrows */
.lightbox-nav {
    position: absolute; top: 50%; transform: translateY(-50%);
    background: rgba(0,0,0,0.5); border: none; color: white;
    font-size: 36px; width: 48px; height: 64px; cursor: pointer;
    border-radius: var(--radius); display: flex; align-items: center; justify-content: center;
    transition: background 0.15s; z-index: 201;
}
.lightbox-nav:hover { background: rgba(99,102,241,0.7); }
.lightbox-prev { left: 16px; }
.lightbox-next { right: 16px; }


/* Reference prompt area */
.ref-prompt-area { position: relative; }
.ref-chips-bar { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
.prompt-wrapper {
    position: relative;
    display: flex;
    flex-direction: column;
    height: 220px;
    margin-bottom: 12px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    transition: border-color 0.1s;
}
.prompt-wrapper:focus-within { border-color: var(--accent); }

/* Compact drop zone on generate page */
.generate-drop-zone { padding: 14px; margin-bottom: 12px; }
.generate-drop-zone p { font-size: 13px; }

/* @autocomplete dropdown */
.mention-dropdown {
    position: absolute; left: 0; right: 0; top: 100%; z-index: 100;
    background: var(--bg-card); border: 1px solid var(--border);
    border-radius: var(--radius); max-height: 280px; overflow-y: auto;
    box-shadow: 0 8px 24px rgba(0,0,0,0.4);
}
.mention-dropdown.hidden { display: none; }
.mention-item {
    display: flex; align-items: center; gap: 8px; padding: 8px 12px; cursor: pointer;
    font-size: 13px; color: var(--text); border-bottom: 1px solid var(--border);
}
.mention-item:last-child { border-bottom: none; }
.mention-item:hover, .mention-item.selected { background: var(--bg-hover); }
.mention-item img { width: 32px; height: 32px; object-fit: cover; border-radius: 4px; background: var(--bg-input); }
/* Audio waveform PNGs are coloured strokes on a transparent canvas — use
   contain (not cover) so the whole envelope stays visible in the 32×32
   thumb instead of cropping to a featureless centre. URL sniff keeps this
   scoped to the audio waveform endpoint; video posters + image files
   still get the cover crop. */
.mention-item img.mention-thumb[src*="/waveform"] { object-fit: contain; padding: 2px; }
.mention-item .mention-icon { width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; background: var(--bg-input); border-radius: 4px; font-size: 14px; }
/* Voice-picker thumb — inline SVG envelope painted by paintVoicePickerRows()
   from the peaks cached at /voices/peaks. Matches the voice-tile envelope
   tone (olive) on a dark input chip so voices read as their own family
   even before the envelope paints. */
.mention-item .voice-thumb-svg {
    width: 32px; height: 32px; flex-shrink: 0;
    background: var(--bg-input);
    border-radius: 4px;
    display: flex; align-items: center; justify-content: center;
    padding: 3px; box-sizing: border-box;
}
.mention-item .voice-thumb-svg svg { width: 100%; height: 100%; display: block; }
.mention-item .voice-thumb-path { fill: rgba(131, 149, 111, 0.7); }
.mention-item .mention-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mention-item .mention-type { font-size: 11px; color: var(--text-dim); }
.mention-item .mention-match { background: transparent; color: var(--accent); font-weight: 600; }

/* Pill popover (shown on pill click) */
.pill-popover {
    position: fixed;
    z-index: 1000;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 10px;
    box-shadow: 0 10px 32px rgba(0,0,0,0.55);
    min-width: 240px;
    max-width: 360px;
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.pill-popover .pill-popover-media { width: 100%; display: block; border-radius: 6px; background: #000; }
.pill-popover .pill-popover-image,
.pill-popover .pill-popover-video { aspect-ratio: 16/9; object-fit: contain; }
.pill-popover .pill-popover-audio { background: transparent; height: 40px; }
.pill-popover .pill-popover-name {
    font-size: 13px; color: var(--accent); font-weight: 600;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pill-popover .pill-popover-actions { display: flex; gap: 8px; justify-content: flex-end; }

/* Writing / text-excerpt popover body — scrollable preview of the extracted
   text so the user can confirm what's being sent as context. */
.pill-popover .pill-popover-text {
    display: flex; flex-direction: column; gap: 6px;
}
.pill-popover .pill-popover-text-meta {
    font-size: 11px; color: var(--text-dim); letter-spacing: 0.2px;
}
.pill-popover .pill-popover-text-body {
    max-height: 260px; max-width: 100%;
    overflow: auto;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 4px;
    padding: 8px 10px;
    font: 12px/1.4 -apple-system, "system-ui", "Segoe UI", Roboto, sans-serif;
    color: var(--text);
    white-space: pre-wrap;
    word-break: break-word;
    margin: 0;
}

/* Sidebar danger zone */
.sidebar-danger-zone { flex-shrink: 0; padding-top: 16px; border-top: 1px solid var(--border); }

/* Modal dialogs */
.modal-overlay {
    position: fixed; inset: 0; background: rgba(0,0,0,0.6);
    display: flex; align-items: center; justify-content: center; z-index: 200;
}
.modal-overlay.hidden { display: none; }

/* Prompt viewer — modal with full selectable text */
.prompt-viewer-overlay {
    position: fixed; inset: 0; background: rgba(0,0,0,0.6);
    display: flex; align-items: center; justify-content: center; z-index: 250;
}
.prompt-viewer {
    position: relative;
    background: var(--bg-card); border: 1px solid var(--border);
    border-radius: var(--radius-lg); padding: 20px 20px 16px;
    width: min(600px, 90vw);
    max-height: 80vh; display: flex; flex-direction: column; gap: 12px;
    box-shadow: 0 10px 40px rgba(0,0,0,0.6);
}
.prompt-viewer-close {
    position: absolute; top: 6px; right: 10px;
    background: transparent; border: none;
    color: var(--text-dim); font-size: 22px; cursor: pointer;
    padding: 2px 6px; line-height: 1; border-radius: 50%;
}
.prompt-viewer-close:hover { color: var(--text); background: rgba(255,255,255,0.08); }
.prompt-viewer-body {
    flex: 1; min-height: 0; overflow-y: auto;
    font-size: 14px; line-height: 1.5;
    color: var(--text);
    white-space: pre-wrap; word-break: break-word;
    user-select: text;
    padding: 8px 4px 0 0;
}
/* Footer uses .card-actions so styling matches tile actions 1:1.
   No modal-specific rules needed. */
.modal-dialog {
    position: relative;
    background: var(--bg-card); border: 1px solid var(--border);
    border-radius: var(--radius-lg); padding: 24px; max-width: 420px; width: 90%;
}
.modal-dialog h3 { margin-bottom: 8px; }
/* Floating close — anchored to the viewport's top-right, not the modal
   dialog, so it reads identically to the image / video / audio lightbox
   close buttons in ASSETS (`.preview-close`). `position: fixed` escapes
   the dialog's bounding box; z-index sits above the modal-overlay (200)
   so it stays clickable over the dialog. */
.modal-close {
    position: fixed;
    top: 16px;
    right: 16px;
    width: 36px;
    height: 36px;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.15);
    border: none;
    color: white;
    font-size: 22px;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 210;
    backdrop-filter: blur(4px);
    transition: background 0.12s, color 0.12s;
}
.modal-close:hover {
    background: rgba(255, 255, 255, 0.3);
    color: var(--text);
}
.modal-dialog p { color: var(--text-dim); font-size: 14px; margin-bottom: 16px; }
.modal-label { display: block; font-size: 13px; color: var(--text-dim); margin-bottom: 6px; }
.modal-input {
    width: 100%; padding: 8px 12px; background: var(--bg-input);
    border: 1px solid var(--border); border-radius: var(--radius);
    color: var(--text); font-size: 14px; margin-bottom: 16px;
}
.modal-input:focus { outline: none; border-color: var(--accent); }
.modal-actions { display: flex; gap: 8px; justify-content: flex-end; }
.modal-actions-stacked { flex-direction: column; }
.modal-actions-stacked .btn { width: 100%; text-align: center; }
.modal-error { color: var(--danger); font-size: 13px; margin-top: 8px; }
.modal-error.hidden { display: none; }

/* ---------------- WRITE drawer: BB demo script viewer ---------------- */
.demo-script-shell {
    display: flex; flex-direction: column;
    height: 100%; overflow: hidden;
    color: var(--text);
}
/* Project-selector row at the top of the WRITE drawer (mirrors the EDIT
   drawer's demo-edit-project-row, in turn mirroring the Image tab's
   engine-row). Sits above the Clean/Annotated toggle. */
.demo-write-project-row {
    flex: 0 0 auto;
    padding: 10px 14px 0 14px;
    margin-bottom: 6px;
}
.demo-script-topbar {
    flex: 0 0 auto;
    padding: 2px 14px 6px 14px;
    display: flex; align-items: center;
    gap: 8px;
}
/* Compass wordmark pill — shaped to match the Clean/Annotated toggle
   lozenge next to it. Keeps the wordmark legible on the dark bg without
   re-colouring the asset. */
.demo-compass-pill {
    display: inline-flex; align-items: center;
    /* Compass sits immediately to the right of Clean/Annotated; the toggle
       itself carries margin-left:auto to push the pair to the right edge. */
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 999px;
    padding: 3px 14px;
    height: 26px;   /* matches .demo-view-toggle height */
    cursor: pointer;
    transition: border-color 0.12s, background 0.12s;
}
.demo-compass-pill:hover { border-color: var(--accent); background: var(--bg-hover); }
.demo-compass-pill img {
    height: 18px;
    width: auto;
    display: block;
    /* Logo is grey on transparent — bump contrast on dark bg. */
    opacity: 0.85;
    filter: brightness(1.25);
    /* Nudge down to sit optically centred in the pill; the wordmark's
       ascenders bias its visual weight upward. */
    transform: translateY(2px);
}
.demo-compass-pill:hover img { opacity: 1; }
.demo-script-title {
    font-size: 11px; letter-spacing: 2px; text-transform: uppercase;
    color: var(--text-dim);
}
/* Dark-mode toggle — round pill matching the view toggle's height. Sits
   immediately to the left of Clean/Annotated. Crescent-moon glyph flips to
   filled-accent when active. */
.demo-theme-toggle {
    display: inline-flex; align-items: center; justify-content: center;
    width: 26px; height: 26px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 999px;
    color: var(--text-dim);
    font-size: 14px;
    line-height: 1;
    cursor: pointer;
    transition: border-color 0.12s, background 0.12s, color 0.12s;
}
.demo-theme-toggle:hover { border-color: var(--accent); color: var(--text); }
.demo-theme-toggle.active {
    background: var(--accent);
    border-color: var(--accent);
    color: #0f1117;
}

/* Ghost pill — used for placeholder/dummy text buttons like "Shot list".
   Same 26 px height as the view toggle + theme toggle so the whole row
   reads as one flush baseline. */
.demo-ghost-pill {
    display: inline-flex; align-items: center;
    height: 26px;
    padding: 0 12px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 999px;
    color: var(--text-dim);
    font-size: 12px;
    cursor: pointer;
    transition: border-color 0.12s, color 0.12s, background 0.12s;
}
.demo-ghost-pill:hover {
    border-color: var(--accent);
    color: var(--text);
    background: var(--bg-hover);
}

.demo-view-toggle {
    display: inline-flex; background: var(--bg-input);
    border: 1px solid var(--border); border-radius: 999px;
    padding: 2px;
    /* Right-align the toggle (and the Compass pill immediately after it)
       within the script-row — Annotated would come from a Compass test, so
       Clean/Annotated sits adjacent to the Compass kickoff button. */
    margin-left: auto;
}
.demo-view-btn {
    background: transparent; border: none;
    color: var(--text-dim); font-size: 12px;
    padding: 4px 10px; cursor: pointer;
    border-radius: 999px;
    transition: background 0.12s, color 0.12s;
}
.demo-view-btn:hover { color: var(--text); }
.demo-view-btn.active {
    background: var(--accent); color: #fff;
}
.demo-minute-nav {
    flex: 0 0 auto;
    display: flex; flex-wrap: wrap; gap: 3px;
    padding: 6px 14px 0 14px;
}
.demo-minute-chip {
    font-size: 10px; font-family: ui-monospace, SFMono-Regular, monospace;
    min-width: 22px; height: 18px;
    display: inline-flex; align-items: center; justify-content: center;
    padding: 0 5px;
    border-radius: 3px;
    background: var(--bg-input); color: var(--text-dim);
    cursor: pointer;
    border: 1px solid transparent;
    transition: border-color 0.1s, transform 0.06s;
    user-select: none;
}
.demo-minute-chip:hover { border-color: var(--accent); color: var(--text); }
.demo-minute-chip:active { transform: scale(0.92); }
.demo-minute-chip.zone-leaky         { background: #8a2c24; color: #ffd9d4; }
.demo-minute-chip.zone-nearly_leaky  { background: rgba(138,44,36,0.35); color: #f3aaa3; }
.demo-minute-chip.zone-neutral       { background: var(--bg-input); color: var(--text-dim); }
.demo-minute-chip.zone-nearly_sticky { background: rgba(107,143,74,0.35); color: #c1dba5; }
.demo-minute-chip.zone-sticky        { background: #4f6a37; color: #e5f1d5; }
.demo-minute-nav-legend {
    flex: 0 0 auto;
    display: flex; gap: 6px;
    padding: 4px 14px 8px 14px;
    font-size: 9px; letter-spacing: 1px; text-transform: uppercase;
    color: var(--text-dim);
    border-bottom: 1px solid var(--border);
}
.legend-chip {
    padding: 1px 6px; border-radius: 3px;
}
.legend-chip.zone-leaky         { background: #8a2c24; color: #ffd9d4; }
.legend-chip.zone-nearly_leaky  { background: rgba(138,44,36,0.35); color: #f3aaa3; }
.legend-chip.zone-sticky        { background: #4f6a37; color: #e5f1d5; }
.legend-chip.zone-nearly_sticky { background: rgba(107,143,74,0.35); color: #c1dba5; }

/* WRITE drawer script body — screenplay aesthetic, matching
   audience_test_demo/static/style.css. Courier Prime on a warm off-white
   "page" with a tight column width and proper screenplay typography.
   Note: the extract strips compass's outer #sara-script-content wrapper,
   so the two top-level divs are #script-annotated and #script-clean.
   Font lives on .demo-script-body so it inherits down through both. */
.demo-script-body {
    /* The scroll container IS the visual "page" now, so the rounded top
       corners live on it (always visible) rather than on the inner
       #script-annotated element (which would scroll away and lose them).
       `overflow-y: auto` does the scrolling; `overflow-x: hidden`
       guarantees clipping at the rounded edges. */
    flex: 1 1 auto;
    min-height: 0;
    overflow: hidden auto;
    margin: 12px auto 0 auto;   /* 12 px of dark drawer bg above, centred */
    max-width: 780px;
    width: 100%;
    box-sizing: border-box;
    /* Flush-left layout with a deliberately wide right gutter (double the
       classic 1.0" screenplay right margin) reserved for per-minute
       annotations. 26 px left puts the script text 12 px inside the
       minute-nav-chip edge (14 px from drawer-left); 23.6% right gives
       roughly 2.0" at normal drawer widths. */
    padding: 36px 23.6% 48px 26px;
    background: #FAFAF5;        /* off-white page — drawer dark bg shows around */
    box-shadow: 0 2px 10px rgba(0,0,0,0.35);
    border-radius: 2px 2px 0 0; /* top corners only; bottom stays flush with drawer */
    color: #1a1a1a;
    scroll-behavior: smooth;
    font-family: 'Courier Prime', 'Courier New', Courier, monospace;
    /* 15 px ≈ 11.25 pt ≈ 94 % of true Final Draft 12-pt. A noticeable
       bump from 13 px, close to authentic type size without overflowing
       the drawer's max width. */
    font-size: 15px;
    /* Final Draft single-spacing = 6 LPI on 12-pt Courier = 1.00 ratio.
       1.15 adds just enough screen breathing room without drifting into
       the looser prose-style 1.4+ range. */
    line-height: 1.15;
}
/* Dark mode — toggle drops the off-white "page" and lets the drawer's
   dark card bg show through, inverting type colours. Zone bg tints get a
   small bump (0.10 → 0.18) so leaky/sticky bands still read against the
   darker backdrop; border-lefts stay the same accent colours. */
.demo-script-body.theme-dark {
    background: transparent;
    box-shadow: none;
    color: var(--text);
}
#bb-script-body.theme-dark .minute-header {
    color: var(--text);
    border-bottom-color: var(--border);
}
#bb-script-body.theme-dark .action,
#bb-script-body.theme-dark .action p,
#bb-script-body.theme-dark .character,
#bb-script-body.theme-dark .dialogue { color: var(--text); }
#bb-script-body.theme-dark .parenthetical { color: var(--text-dim); }
#bb-script-body.theme-dark #script-annotated .block-leaky {
    background: rgba(192, 57, 43, 0.18);
}
#bb-script-body.theme-dark #script-annotated .block-sticky {
    background: rgba(30, 132, 73, 0.18);
}
/* The compass view container serves as our "page" — sized like a US Letter
   page scaled to max 660 px wide. Horizontal padding is percentage-based
   so the 1.5" / 1.0" margin ratio holds at narrower widths too (the WRITE
   drawer can get pretty thin). Page = 8.5" model:
     · left margin: 1.5/8.5 ≈ 17.6%
     · right margin: 1.0/8.5 ≈ 11.8%
     · inner content area (the 6" usable column) = 70.6%
   Child elements' percentage paddings below are therefore percentages of
   that ~70% usable width, keeping character/dialogue tabs visually right. */
.demo-script-body > #script-annotated,
.demo-script-body > #script-clean {
    /* Page visuals (bg / padding / rounded top / shadow) moved up to
       .demo-script-body so the rounded corners survive scrolling. These
       two containers are now just transparent scroll-region children. */
    max-width: none;
    margin: 0;
    padding: 0;
    background: transparent;
    box-shadow: none;
    border-radius: 0;
}
/* Hide compass's own chrome + nav (we supply our own above the script). */
#bb-script-body .nav,
#bb-script-body .top-toggle,
#bb-script-body #btn-clean,
#bb-script-body #btn-annotated,
#bb-script-body #btn-report,
#bb-script-body #script-report,
#bb-script-body .report-view,
#bb-script-body h1,
#bb-script-body .legend-dot,
#bb-script-body .legend-item,
#bb-script-body .legend-label { display: none !important; }

/* Clean / Annotated view swap (annotated shell removed from demo view,
   but both still render; keep the toggle semantics alive in case either
   view carries distinct content in the future). */
#bb-script-body[data-view="clean"]     #script-annotated { display: none !important; }
#bb-script-body[data-view="clean"]     #script-clean     { display: block !important; }
#bb-script-body[data-view="annotated"] #script-annotated { display: block !important; }
#bb-script-body[data-view="annotated"] #script-clean     { display: none !important; }

/* Minute / scene headers — subtle rule + uppercase caps à la Final Draft.
   Left edge anchors at the minute-nav chip row (14 px from drawer-left).
   Page padding-left is 26 px, so margin-left: -12 px brings the rule back
   to 14 px from drawer-left exactly. Right-side negative margin extends
   the rule into the 23.6% annotation gutter; -30% of content-width lands
   ~14 px from drawer-right across the drawer's normal width range. */
#bb-script-body .minute-header {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: #1a1a1a;
    margin: 1.6em -30% 0.7em -12px;
    padding-bottom: 4px;
    border-bottom: 1px solid #d4d4d0;
    /* Leaves 12 px between the top of the scroll viewport and the minute
       header when the nav scrolls the script to a minute via scrollIntoView. */
    scroll-margin-top: 12px;
}
#bb-script-body .minute-header:first-child { margin-top: 0; }

/* Scene headers (slug lines) — Final Draft style. UPPERCASE, bold, with
   breathing room above and below. Injected at scene boundaries by
   bb_demo_extract.py's SCENE_HEADERS map. Sits flush with the action text
   (no left-column offset) so it reads as a proper slug line. */
#bb-script-body .scene-header {
    font-family: 'Courier Prime', 'Courier New', Courier, monospace;
    font-size: 15px;
    text-transform: uppercase;
    letter-spacing: 0;
    color: #1a1a1a;
    margin: 1.8em 0 0.9em 0;
    padding: 0;
}
#bb-script-body.theme-dark .scene-header { color: var(--text); }

/* Screenplay typography — percentages mirror audience_test_demo's
   .sp-character / .sp-dialogue indents so both demos share the same
   look. Character is UPPERCASE but NOT bold (Final Draft / Fountain
   convention). Dialogue column is narrower than action, with symmetric
   left+right padding so it stays readable even at narrower widths. */
/* Clean + neutral blocks get no colouring — plain screenplay page. */
#bb-script-body .block-clean,
#bb-script-body .block-neutral {
    padding: 0 !important;
    background: transparent !important;
    border: none !important;
}

/* Zone colouring — ANNOTATED view only (clean view has no block-zone
   classes, so these selectors simply don't match there). Vertical line
   + tinted band sit on the LEFT of each zoned block, extending across the
   same horizontal extent as the minute-header rule (-12 px left, -30%
   right) so the colour band aligns with the minute-separator above.
   padding-left: 12 px keeps the script text inside at 26 px from
   drawer-left — identical to non-zoned blocks, so text never shifts
   between zoned and unzoned minutes. */
#bb-script-body #script-annotated .block-leaky,
#bb-script-body #script-annotated .block-nearly_leaky,
#bb-script-body #script-annotated .block-sticky,
#bb-script-body #script-annotated .block-nearly_sticky {
    margin: 0.25em -30% 0.25em -12px;
    /* padding-right: 30% exactly cancels margin-right: -30% for the inner
       text — the tinted band + border still extend into the annotation
       gutter, but text stops at the same right edge as non-zoned blocks. */
    padding: 0.35em 30% 0.45em 12px;
}
#bb-script-body #script-annotated .block-leaky {
    background: rgba(192, 57, 43, 0.10);
    border-left: 3px solid #c0392b;
}
#bb-script-body #script-annotated .block-nearly_leaky {
    border-left: 3px solid rgba(192, 57, 43, 0.45);
}
#bb-script-body #script-annotated .block-sticky {
    background: rgba(30, 132, 73, 0.10);
    border-left: 3px solid #1e8449;
}
#bb-script-body #script-annotated .block-nearly_sticky {
    border-left: 3px solid rgba(30, 132, 73, 0.45);
}

#bb-script-body .minute-row { display: block; }
#bb-script-body .script-col { width: 100%; }

/* Action — full column width. Inner <p> children carry the paragraph
   rhythm (see bb_demo_extract.py's sentence-splitter). */
#bb-script-body .action,
#bb-script-body .action p {
    color: #1a1a1a;
    margin: 0 0 0.9em 0;
}
#bb-script-body .action p:last-child { margin-bottom: 0; }
#bb-script-body .action { margin: 0.75em 0; }

/* Percentages below are of the INNER content area (the 6" usable column
   after the 1.5"/1.0" page margins). Values match Final Draft's default
   tabs: character at 3.7" (37% in), parenthetical at 3.1" (27%), dialogue
   at 2.5" (17%). Dialogue wraps at 35 chars via the 25% right inset. */

/* Character cue — uppercase, regular weight. Final Draft tabs to 3.7" from
   the page left, i.e. 2.2" into the 6" usable area = 37%. */
#bb-script-body .character {
    color: #1a1a1a;
    text-transform: uppercase;
    font-weight: 400;
    letter-spacing: 0;
    padding-left: 37%;
    margin: 1em 0 0 0;
    text-align: left;
}

/* Parenthetical — in parentheses, slightly shallower tab than character. */
#bb-script-body .parenthetical {
    color: #1a1a1a;
    padding-left: 27%;
    padding-right: 32%;
    margin: 0 0 0.1em 0;
    font-style: normal;
}

/* Dialogue — 35-char wrap column: 17% left inset, 25% right inset so the
   content column sits at 58% of usable width ≈ 35 chars at 12-pt Courier. */
#bb-script-body .dialogue {
    color: #1a1a1a;
    padding-left: 17%;
    padding-right: 25%;
    margin: 0.1em 0 0.5em 0;
    text-align: left;
}

/* Transition (CUT TO: / SMASH CUT TO:) — right-aligned uppercase, blank
   line each side (approximated by the 1em margins). */
#bb-script-body .transition {
    color: #1a1a1a;
    text-align: right;
    text-transform: uppercase;
    margin: 1em 0;
}

/* Demo simplification: hide every annotation layer compass injects per
   minute, so only the script prose + its minute header remain. Hidden:
   "Estimated early drop-off" / drop-off %, alignment note rows, the Align
   lozenge, and the right-margin stack (Heat / Hook / genres / adjectives).
   Revisit when we re-introduce real per-minute analysis UI. */
#bb-script-body .note-row,
#bb-script-body .ann,
#bb-script-body .ann-align,
#bb-script-body .ann-leaky,
#bb-script-body .ann-sticky,
#bb-script-body .ann-neutral,
#bb-script-body .intensity-box,
#bb-script-body .outlined-box,
#bb-script-body .margin-right { display: none !important; }

/* ---------------- EDIT drawer: BB video + vertical timeline ---------------- */
.demo-edit-shell {
    display: flex; flex-direction: column;
    height: 100%;
    overflow-y: auto;
    background: var(--bg-card);
    color: var(--text);
}
/* Project-selector row above the video — matches the .engine-row used on
   the Image tab so the visual rhythm (dropdown + action button) carries
   across. Padding mirrors the video-wrap's so both feel like one header. */
/* Spacing ladder matches the WRITE drawer's:
     · 8 px between project-row and script-row
     · 12 px between script-row and the video/content below
   so that:
       .demo-edit-project-row   padding-bottom 0 + margin-bottom 6
     + .demo-edit-script-row    padding-top 2                       = 8 px
     + .demo-edit-script-row    padding-bottom 6
     + .demo-edit-video-wrap    padding-top 6                       = 12 px
*/
.demo-edit-project-row {
    flex: 0 0 auto;
    padding: 10px 12px 0 12px;
    margin-bottom: 6px;
}
.demo-edit-script-row {
    flex: 0 0 auto;
    padding: 2px 12px 6px 12px;
    display: flex; align-items: center; gap: 8px;
}
/* Sticky header — wraps project-row + script-row + video-wrap so they all
   stay pinned as the timeline scrolls underneath. Background fill prevents
   the timeline bleeding through during scroll. */
.demo-edit-sticky-header {
    flex: 0 0 auto;
    position: sticky; top: 0;
    z-index: 3;
    background: var(--bg-card);
}
.demo-edit-video-wrap {
    flex: 0 0 auto;
    background: var(--bg-card);
    padding: 6px 12px 10px 12px;
}
#bb-edit-video {
    width: 100%; max-height: 260px;
    display: block; background: #000;
    border-radius: var(--radius);
}
/* Hide Chrome's shadow-DOM focus rings on the native scrub thumb +
   enclosure. ::-webkit-media-controls-* is the only way to reach into the
   user-agent shadow DOM from outside. Apply without the :focus qualifier
   because Chrome's focus-visible logic inside the shadow tree doesn't
   consistently match external :focus selectors. Blanket outline + box-
   shadow suppression is safer. Focus is also released via a focusin
   listener in hydrateEdit, so there's no navigational reason to keep it. */
#bb-edit-video,
#bb-edit-video::-webkit-media-controls,
#bb-edit-video::-webkit-media-controls-enclosure,
#bb-edit-video::-webkit-media-controls-panel,
#bb-edit-video::-webkit-media-controls-timeline,
#bb-edit-video::-webkit-media-controls-volume-slider,
#bb-edit-video::-webkit-media-controls-play-button,
#bb-edit-video::-webkit-media-controls-mute-button,
#bb-edit-video::-webkit-media-controls-fullscreen-button,
#bb-edit-video::-webkit-media-controls-overflow-button,
#bb-edit-video::-webkit-media-controls-current-time-display,
#bb-edit-video::-webkit-media-controls-time-remaining-display {
    outline: none !important;
    box-shadow: none !important;
}
/* Zoom stepper — lives in .demo-edit-script-row next to Clean/Annotated.
   Pill-shaped with [−] LABEL [+]. Same 26 px height as the view toggle
   and Compass pill so the whole row is visually flush. Cmd/Ctrl+wheel on
   the timeline itself also zooms, so this is a secondary control. */
.demo-zoom-stepper {
    display: inline-flex; align-items: center;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 999px;
    height: 26px;
    padding: 0 2px;
}
.demo-zoom-btn {
    background: transparent; border: none;
    color: var(--text-dim);
    width: 22px; height: 22px;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 14px; line-height: 1;
    border-radius: 999px;
    cursor: pointer;
    transition: background 0.12s, color 0.12s;
}
.demo-zoom-btn:hover { color: var(--text); background: var(--bg-hover); }
.demo-zoom-label {
    font-size: 11px; letter-spacing: 1px; text-transform: uppercase;
    color: var(--text-dim);
    text-align: center;
    padding: 0 6px;
    user-select: none;
}
.demo-edit-timeline {
    flex: 0 0 auto;
    position: relative;
    /* Top pad reserves visual headroom for the playhead's time-label at t=0.
       Without it, the label renders above the timeline edge and gets hidden
       behind the sticky video-wrap. Playhead JS offsets by the same amount. */
    padding: 14px 12px 40px 12px;
}
/* Playhead — horizontal accent-coloured line that tracks video.currentTime.
   Positioned absolute inside .demo-edit-timeline (padding-box). Starts at
   the left edge of the time-label column and spans full width. z-index
   intentionally BELOW the sticky video-wrap (z:3) so the line never paints
   over the video when the timeline scrolls underneath it. */
.demo-timeline-playhead {
    position: absolute;
    left: 0; right: 0;
    height: 2px;
    background: var(--accent);
    box-shadow: 0 0 0 1px rgba(0,0,0,0.4), 0 0 6px rgba(107,143,74,0.45);
    z-index: 2;
    cursor: ns-resize;
    touch-action: none;
    transform: translateZ(0);  /* keep repaint cheap during drag */
}
.demo-timeline-playhead-time {
    /* Also a valid grab zone — sits above the thin line and inherits the
       playhead's pointerdown handler via bubbling. Removed pointer-events:none
       so clicking the time pill actually picks up the scrub. */
    position: absolute;
    right: 6px; top: -9px;
    background: var(--accent); color: #fff;
    font-family: ui-monospace, SFMono-Regular, monospace;
    font-size: 10px;
    padding: 1px 5px;
    border-radius: 3px;
    cursor: ns-resize;
    user-select: none;
}
.demo-edit-timeline.dragging { cursor: ns-resize; }
.demo-edit-timeline.dragging .demo-timeline-slice { pointer-events: none; }
/* Each row is a "slice" — a time-labelled strip with a scene beat. Height
   is driven by --slice-h inline so the zoom slider can rescale without
   reflowing the DOM. */
.demo-timeline-slice {
    /* align-items: stretch lets the beat cell fill the row height so text
       that wraps past the row is clipped cleanly by the beat's own
       overflow:hidden rather than spilling into the row below. The timecode
       label overrides this with align-self: flex-start + margin-top to sit
       centred on the row's top tick — that negative margin is why we do
       NOT set overflow:hidden here (it would clip the label's upper half). */
    display: flex; align-items: stretch; gap: 8px;
    height: var(--slice-h, 32px);
    padding: 0;
    /* Faint grey bucket divider at the top of each row. Acts as the "tick"
       for the slice's timecode. Alpha stays quiet at every zoom. */
    border-top: 1px solid rgba(255,255,255,0.06);
}
.demo-timeline-slice:first-child { border-top: none; }
/* Minute boundaries sit a touch louder for orientation at coarser zooms. */
.demo-timeline-slice.minute-boundary { border-top-color: rgba(255,255,255,0.14); }
.demo-slice-time {
    /* 64px fits MM:SS:FF (8 monospace glyphs at 10px) with breathing room. */
    flex: 0 0 64px;
    align-self: flex-start;   /* don't stretch — label's natural height only */
    font-family: ui-monospace, SFMono-Regular, monospace;
    font-size: 10px;
    line-height: 14px;
    color: var(--text-dim);
    /* Pull the label up so its VERTICAL CENTRE sits on the slice's top
       border (our tick mark). The scrubber line also sits on a slice top
       when parked on a slice-boundary frame, so centring the label there
       makes the scrubber and the label it names visually coincide. */
    margin-top: -7px;
    padding: 0 0 0 2px;
    /* Clicking any timecode scrolls the drawer back to the top — helpful
       when scrolling deep into the timeline has hidden the dropdown + view
       toggle under the sticky video-wrap. */
    cursor: pointer;
    transition: color 0.12s;
}
.demo-slice-time:hover { color: var(--text); }
/* Zone bar — vertical strip on the LEFT of each slice, mirroring the WRITE
   drawer's per-minute retention annotations. Sits at the timeline's left
   edge, in line with the video frame's left edge above, so it reads as a
   continuous retention band when many consecutive slices share a zone.
   Leaky/sticky = solid colour line; nearly_* = dim line; neutral =
   invisible. No slice-bg tint in this drawer — vertical line only.
   Column stays in the flex layout even when the bar has no colour, so the
   timecode's horizontal position is constant across zones and views. */
.demo-slice-bar {
    flex: 0 0 4px; align-self: stretch;
    border-radius: 1px;
    background: transparent;
    cursor: pointer;   /* click-anywhere-in-left-column → scroll to top */
}
.demo-slice-bar.zone-leaky         { background: #c0392b; }
.demo-slice-bar.zone-nearly_leaky  { background: rgba(192, 57, 43, 0.45); }
.demo-slice-bar.zone-sticky        { background: #1e8449; }
.demo-slice-bar.zone-nearly_sticky { background: rgba(30, 132, 73, 0.45); }
/* Clean view suppresses the zone colour (but keeps the column's width so
   timecodes don't shift). */
.demo-edit-timeline.script-hidden .demo-slice-bar { background: transparent; }
/* Shot blocks sit BEHIND slice content as a full-width background track.
   Slice rows need an explicit stacking context above the track (z-index:1)
   so their beat text overlays. Playhead z:3 wins over both. */
.demo-timeline-slice { position: relative; z-index: 1; }
.demo-shots-track {
    position: absolute;
    top: 14px;             /* matches TIMELINE_TOP_PAD — aligns with first slice */
    left: 96px;            /* timeline pad 12 + bar 4 + gap 8 + time 64 + gap 8 */
    right: 12px;           /* mirrors timeline pad-right */
    pointer-events: none;
    z-index: 0;            /* under slice content */
}
.demo-shot {
    position: absolute;
    left: 0; right: 0;
    background: rgba(107, 143, 74, 0.16);
    border-radius: 4px;
    overflow: hidden;   /* clip embedded thumbnails to the rounded box */
}
.demo-shot.alt {
    background: rgba(107, 143, 74, 0.28);
}
/* Drop-target highlight — shown on dragover from the ASSETS gallery. */
.demo-shot.drag-over {
    background: rgba(107, 143, 74, 0.55);
    outline: 1px dashed var(--accent);
    outline-offset: -2px;
}
/* Populated shot — image or video dropped from the gallery. Media fills
   the shot box (object-fit: cover so wider assets still fill a tall shot
   block). The green tint underneath is replaced by the asset itself. */
.demo-shot.has-content {
    background: #0f1117;   /* letterbox fill for off-ratio media */
}
.demo-shot img,
.demo-shot video {
    width: 100%; height: 100%;
    object-fit: cover;
    display: block;
    pointer-events: none;   /* don't intercept timeline scrub */
}
/* Script visibility toggle (left of the Zoom slider). */
.demo-script-toggle {
    display: inline-flex; align-items: center; gap: 5px;
    font-size: 11px; letter-spacing: 1px; text-transform: uppercase;
    color: var(--text-dim);
    cursor: pointer;
    user-select: none;
    margin-right: 4px;
}
.demo-script-toggle input {
    margin: 0;
    accent-color: var(--accent);
    cursor: pointer;
}
.demo-edit-timeline.script-hidden .demo-slice-beat { display: none; }
.demo-slice-beat {
    flex: 1 1 auto; min-width: 0;
    /* Row-height-constrained so wrapped text can't leak into the next slice.
       min-height:0 lets flex shrink the cell below its natural content size. */
    min-height: 0;
    align-self: stretch;
    font-size: 11px; line-height: 1.4;
    color: var(--text);
    overflow: hidden;
    word-break: break-word;
    padding-top: 2px;
}
.demo-slice-speaker {
    color: var(--text-dim);
    font-weight: 500;
    margin-right: 6px;
    text-transform: uppercase;
    font-size: 10px;
    letter-spacing: 1px;
}

/* Video frame picker — wider modal to fit a legible video preview. */
.video-frame-picker-dialog { max-width: 720px; }
.video-frame-picker-dialog .frame-picker-hint {
    font-size: 12px; color: var(--text-dim);
    margin-bottom: 12px;
}
.frame-picker-video-wrap {
    background: #000; border-radius: var(--radius);
    overflow: hidden; margin-bottom: 16px;
    display: flex; justify-content: center;
}
#frame-picker-video {
    display: block;
    width: 100%; max-height: 60vh;
    object-fit: contain;
}

/* Override container for project/unified layout */
.project-layout + .container, .project-layout { max-width: none; }
.unified-layout + .container, .unified-layout { max-width: none; }

/* ---------- Upscale popover ---------- */
.upscale-popover {
    position: absolute;
    z-index: 2000;
    background: var(--bg-elev, #1a1a1a);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 12px 14px;
    box-shadow: 0 8px 32px rgba(0,0,0,0.4);
    min-width: 280px;
    font-size: 13px;
    color: var(--text);
}
.upscale-popover .up-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 10px;
    gap: 12px;
}
.upscale-popover .up-row.up-hint {
    justify-content: flex-start;
    color: var(--text-dim);
    font-size: 11px;
    margin-top: 4px;
    border-top: 1px solid var(--border);
    padding-top: 8px;
}
.upscale-popover .up-row.up-actions {
    justify-content: flex-end;
    gap: 8px;
    margin-bottom: 0;
}
.upscale-popover .up-label {
    color: var(--text-dim);
    font-size: 12px;
    min-width: 90px;
}
.upscale-popover .up-pillgroup {
    display: flex;
    gap: 2px;
    background: var(--bg-input);
    border-radius: 6px;
    padding: 2px;
}
.upscale-popover .up-pill {
    padding: 5px 12px;
    background: transparent;
    border: none;
    color: var(--text-dim);
    border-radius: 4px;
    cursor: pointer;
    font-size: 12px;
    line-height: 1;
    transition: all 0.15s;
}
.upscale-popover .up-pill.active {
    background: var(--accent);
    color: white;
}
/* Unified scale popover — tab header + panels */
.scale-popover .scale-tabs {
    display: flex;
    gap: 2px;
    background: var(--bg-input);
    border-radius: 6px;
    padding: 2px;
    margin-bottom: 12px;
}
.scale-popover .scale-tab {
    flex: 1;
    padding: 6px 10px;
    background: transparent;
    border: none;
    color: var(--text-dim);
    border-radius: 4px;
    cursor: pointer;
    font-size: 12px;
    line-height: 1;
    transition: all 0.15s;
}
.scale-popover .scale-tab.active {
    background: var(--accent);
    color: white;
}
.scale-popover .scale-checkbox {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    cursor: pointer;
}
.scale-popover .scale-checkbox input[type="checkbox"] {
    accent-color: var(--accent);
    cursor: pointer;
}
.scale-popover .scale-source-info {
    color: var(--text-dim);
    font-size: 11px;
    margin-bottom: 10px;
    padding-bottom: 8px;
    border-bottom: 1px solid var(--border);
}

/* Floating toast shown while downscale is running */
.scale-toast {
    position: fixed;
    bottom: 24px;
    left: 50%;
    transform: translateX(-50%);
    background: var(--bg-elev, #1a1a1a);
    border: 1px solid var(--border);
    border-radius: 20px;
    padding: 8px 16px;
    display: flex;
    align-items: center;
    gap: 10px;
    z-index: 3000;
    box-shadow: 0 4px 16px rgba(0,0,0,0.4);
    font-size: 12px;
    color: var(--text);
}
.scale-toast .spinner {
    width: 14px;
    height: 14px;
    border: 2px solid var(--border);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
}
.scale-toast-label { color: var(--text-dim); }

/* ---------- Audio tile waveform ---------------------------------------
   Gallery tiles show the same SVG envelope as the lightbox focus
   waveform (grey-white on black), at the modal's 80px height. Populated
   client-side by renderAudioTileSvg which fetches /peaks?n=300 per tile.
*/
.audio-waveform-container {
    position: relative;
    height: 80px;
    width: 100%;
    /* Matches the modal's .audio-wave-scroll backdrop — subtle white tint
       over whatever the tile's card background is. Reads as a consistent
       surface across tile and modal, no hard black edge. */
    background: rgba(255, 255, 255, 0.02);
    overflow: hidden;
    cursor: pointer;
}
.audio-tile-svg {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    display: block;
}
.audio-tile-path {
    /* Slightly brighter than the modal's unplayed waveform (0.32) so the
       tile reads clearly without the modal's interactive context. */
    fill: rgba(255, 255, 255, 0.45);
}
/* Placeholder used by TTS "Generating…" tiles before the asset lands. */
.audio-tile-placeholder {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    color: var(--text-dim);
    font-size: 12px;
}
.audio-waveform-container.is-pending .audio-tile-svg { display: none; }

/* ---------- Audio editor (lightbox) -----------------------------------
   SVG envelope + draggable IN/OUT handles + toolbar. Replaces the older
   bar-based .audio-player markup. Waveform sits in white/grey so olive
   stays reserved for accents (selection band + handles). Handle feel
   mirrors the EDIT tab scrubber.
*/
.audio-editor {
    /* Fills the modal's content area so the rounded-box edges land flush
       with the download button's left edge and the like button's right
       edge in the footer below. See the `:has(.audio-editor)` overrides
       further down the file for the modal + footer padding tweaks that
       make this alignment work. Max-width bumped to 960px so the
       MM:SS.ss readouts + Zoom cluster + Trim/Reset all fit on one line
       without wrapping. */
    width: 100%;
    max-width: 960px;
    padding: 16px 20px 18px;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    display: flex;
    flex-direction: column;
    gap: 12px;
}
/* Modal layout overrides when the preview-body contains an audio editor:
   tighten the outer padding to 12px so the editor box sits 12px from the
   modal's top/left/right edges, and flush the footer's inner padding so
   the download + like buttons align exactly with the editor's edges. */
.preview-modal-content:has(.audio-editor) {
    padding: 12px;
    /* Widen the cap so the toolbar never wraps onto two lines on typical
       laptop widths. 1000px fits a 960px editor + 24px padding with some
       breathing room. */
    max-width: min(1000px, 92vw);
}
.preview-modal-content:has(.audio-editor) .preview-body {
    width: 100%;
}
.preview-modal-content:has(.audio-editor) .preview-footer {
    padding: 0;
}
/* .audio-editor-row removed — focus waveform + minimap now sit as
   full-width column children of .audio-editor. */
/* Two-layer structure so handle grip tabs can sit outside the scroll-
   clipped area. Outer .audio-wave-viewport has overflow:visible so handles
   (its direct children, siblings of the scroll box) render in full. Inner
   .audio-wave-scroll is the actual scroll container (waveform SVG +
   selection band + playhead live in .audio-wave-track inside it). */
.audio-wave-viewport {
    position: relative;
    width: 100%;
    height: 80px;
    min-width: 0;
    overflow: visible;
    user-select: none;
}
.audio-wave-scroll {
    position: relative;
    width: 100%;
    height: 100%;
    cursor: grab;
    border-radius: 4px;
    background: rgba(255, 255, 255, 0.02);
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: thin;
    scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
    touch-action: pan-y pinch-zoom;
}
.audio-wave-scroll.is-panning { cursor: grabbing; }
.audio-wave-scroll::-webkit-scrollbar { height: 6px; }
.audio-wave-scroll::-webkit-scrollbar-track { background: transparent; }
.audio-wave-scroll::-webkit-scrollbar-thumb {
    background: rgba(255, 255, 255, 0.2);
    border-radius: 3px;
}
.audio-wave-track {
    position: relative;
    width: calc(100% * var(--zoom, 1));
    min-width: 100%;
    height: 100%;
}
.audio-wave-svg {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    display: block;
}
/* Base = unplayed (dim white). Played = brighter white, revealed by
   clip-path inset as the playhead advances. Two identical paths stacked;
   only the played copy gets its clip-path mutated per RAF tick. */
.audio-wave-base {
    fill: rgba(255, 255, 255, 0.32);
}
.audio-wave-played {
    fill: rgba(255, 255, 255, 0.82);
    clip-path: inset(0 100% 0 0);
}
.audio-selection {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: 100%;
    background: rgba(107, 143, 74, 0.14);
    border-left: 1px solid rgba(107, 143, 74, 0.55);
    border-right: 1px solid rgba(107, 143, 74, 0.55);
    pointer-events: none;
}
.audio-playhead {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: 2px;
    background: rgba(255, 255, 255, 0.9);
    border-radius: 1px;
    pointer-events: none;
    /* --playhead-frac runs 0→1 as the playhead moves across the track.
       Scaling the translate by that fraction (0px at left:0, -2px at
       left:100%) keeps the 2px playhead fully visible at both extremes
       rather than letting half of it clip behind the overflow of the
       scroll container. */
    transform: translateX(calc(var(--playhead-frac, 0) * -2px));
}
/* IN / OUT drag handles. They now sit OUTSIDE the scroll container as
   direct children of .audio-wave-viewport — `left` is set in pixels by
   JS (`syncHandles`), accounting for the viewport's scrollLeft so the
   handles track the content visually while never getting clipped by the
   scroll box's overflow. Grip tabs sit flush with the top / bottom of the
   80px viewport (not outside it) so they always render in full. */
.audio-handle {
    position: absolute;
    top: -4px;
    bottom: -4px;
    width: 14px;
    transform: translateX(-7px);
    cursor: ew-resize;
    touch-action: none;
    z-index: 3;
}
.audio-handle::before {
    content: "";
    position: absolute;
    top: 4px;
    bottom: 4px;
    left: 50%;
    transform: translateX(-50%);
    width: 2px;
    border-radius: 1px;
}
.audio-handle-grip {
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    width: 10px;
    height: 10px;
    border: 1px solid rgba(0, 0, 0, 0.35);
    border-radius: 2px;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.audio-handle-in .audio-handle-grip { top: 0; }
.audio-handle-out .audio-handle-grip { bottom: 0; }
/* Classic traffic-light pairing: green for IN, red for OUT. Separately
   themed so the user can tell them apart at a glance even when IN and OUT
   are near each other. */
.audio-handle-in::before { background: #5caa42; }
.audio-handle-in .audio-handle-grip { background: #5caa42; }
.audio-handle-in:hover::before,
.audio-handle-in:active::before { background: #73c055; }
.audio-handle-in:hover .audio-handle-grip,
.audio-handle-in:active .audio-handle-grip { background: #73c055; }
.audio-handle-out::before { background: #c73e3e; }
.audio-handle-out .audio-handle-grip { background: #c73e3e; }
.audio-handle-out:hover::before,
.audio-handle-out:active::before { background: #d65858; }
.audio-handle-out:hover .audio-handle-grip,
.audio-handle-out:active .audio-handle-grip { background: #d65858; }
/* Single minimalist control row. Every element is 30px tall (matching the
   download btn-icon elsewhere) and every text element shares the same
   13px font size so nothing jumps as times tick. Flex-wrap is left on so
   the row reflows gracefully on narrow windows. */
.audio-editor-toolbar {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 0;
    font-size: 13px;
    color: var(--text);
    flex-wrap: wrap;
    font-variant-numeric: tabular-nums;
}
.audio-editor-toolbar .audio-time,
.audio-editor-toolbar .audio-io-label {
    display: inline-flex;
    align-items: center;
    height: 30px;
    font-size: 13px;
    white-space: nowrap;
    gap: 4px;
}
.audio-editor-toolbar .audio-slash {
    color: var(--text-dim);
    margin: 0 2px;
}
.audio-editor-toolbar .audio-io-label {
    color: var(--text);
}
/* Colour the IN / OUT labels to match their respective handles on the
   waveform — green for IN, red for OUT. Numerical value stays white. */
.audio-editor-toolbar .audio-io-in .audio-io-key { color: #5caa42; font-weight: 600; }
.audio-editor-toolbar .audio-io-out .audio-io-key { color: #c73e3e; font-weight: 600; }
.audio-editor-toolbar .audio-io-label .audio-sel-in,
.audio-editor-toolbar .audio-io-label .audio-sel-out {
    color: var(--text);
}
/* Explicit spacer pushes Zoom + Reset to the right of the row. Cleaner
   than margin-left:auto on zoom because Reset sits further right still. */
.audio-toolbar-spacer { flex: 1; }
.audio-zoom-cluster {
    display: inline-flex;
    align-items: center;
    gap: 6px;
}
.audio-zoom-cluster .btn {
    width: 30px;
    height: 30px;
    min-width: 30px;
    padding: 0;
    font-size: 14px;
    line-height: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.audio-zoom-cluster .btn:disabled { opacity: 0.35; cursor: not-allowed; }
.audio-zoom-label {
    color: var(--text-dim);
    font-size: 13px;
}
/* Reset + Trim — regular text buttons, 30px tall, same height as the
   zoom/play/download buttons so the row reads as one ruler. */
.audio-action-reset,
.audio-action-save {
    height: 30px;
    padding: 0 12px;
    font-size: 13px;
    line-height: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
/* Pre-sized so the label can swap Trim ↔ Saved on success without the
   button (or the surrounding toolbar) jumping width. 70px comfortably
   fits the longest "Saved" string at the 13px/padding settings above. */
.audio-action-save { min-width: 70px; }
.audio-action-save.is-saved {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--bg);
}
/* Overview minimap — sits between the main waveform row and the toolbar.
   Full-clip envelope (dim white) plus a translucent viewport rectangle
   that mirrors scroll + zoom state. Click outside the rect to jump the
   main viewport; drag the rect to pan. */
.audio-minimap {
    position: relative;
    height: 28px;
    width: 100%;
    background: rgba(255, 255, 255, 0.02);
    border-radius: 4px;
    cursor: pointer;
    user-select: none;
    overflow: hidden;
}
.audio-minimap-svg {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    display: block;
}
.audio-minimap-path { fill: rgba(255, 255, 255, 0.28); }
/* Selection mirror — subtle olive tint so the user sees the IN/OUT region
   at a glance without it dominating the overview. */
.audio-minimap-selection {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: 0;
    background: rgba(131, 149, 111, 0.18);
    border-left: 1px solid rgba(131, 149, 111, 0.5);
    border-right: 1px solid rgba(131, 149, 111, 0.5);
    pointer-events: none;
}
.audio-minimap-viewport {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: 100%;
    background: rgba(255, 255, 255, 0.06);
    border: 1px solid rgba(255, 255, 255, 0.45);
    border-radius: 3px;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25) inset;
    cursor: grab;
    box-sizing: border-box;
    transition: background 0.12s, border-color 0.12s;
}
.audio-minimap-viewport:hover {
    background: rgba(255, 255, 255, 0.09);
    border-color: rgba(255, 255, 255, 0.6);
}
.audio-minimap-viewport:active { cursor: grabbing; }
.audio-action-save:disabled {
    opacity: 0.45;
    cursor: not-allowed;
}

/* ---------- Legacy audio-player rules (pre-editor, kept for safety) --- */
.audio-player {
    display: flex;
    align-items: center;
    gap: 16px;
    width: min(720px, 72vw);
    padding: 14px 18px;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
}
/* Play / pause button — neutral greyscale (matches the simple-classic
   iconography elsewhere in the app, not the olive accent). Circle
   vertically centres on the bar row because the .audio-player flex row
   now holds the bars directly as a sibling (no intermediate column). */
.audio-play-btn {
    flex-shrink: 0;
    width: 30px;
    height: 30px;
    border-radius: var(--radius);
    border: 1px solid var(--border);
    background: var(--bg-input);
    color: var(--text);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    transition: background 0.12s, transform 0.08s, border-color 0.12s;
}
.audio-play-btn:hover {
    background: rgba(255, 255, 255, 0.06);
    border-color: rgba(255, 255, 255, 0.25);
}
.audio-play-btn:active { transform: scale(0.96); }
.audio-icon { display: block; fill: currentColor; }
/* Optical centring — the triangle's geometric centre-of-mass is left-of
   the bounding box so we push it ~1px right to sit in the circle's
   optical centre. Pause glyph stays at 0 (geometry already balanced). */
.audio-icon-play { transform: translateX(1px); }
.audio-icon-pause { display: none; }
.audio-play-btn.is-playing .audio-icon-play { display: none; }
.audio-play-btn.is-playing .audio-icon-pause { display: block; }
.audio-bars-wrap {
    flex: 1;
    min-width: 0;
    cursor: pointer;
    padding: 4px 0;
}
.audio-bars {
    display: grid;
    /* Slightly wider bars + tighter gap emphasise the thick/thin contrast.
       80 slices × 1fr across ~560-620px of available waveform width gives
       each bar ~6px, which reads as clearly varied when paired with the
       shaped amplitude mapping in JS. */
    grid-template-columns: repeat(80, 1fr);
    gap: 1px;
    width: 100%;
    height: 56px;
    align-items: center;
}
.audio-bar {
    height: calc(var(--h, 0.5) * 100%);
    min-height: 1px;
    background: rgba(107, 143, 74, 0.35);
    border-radius: 1px;
    transition: background 0.08s linear;
}
.audio-bar.played {
    background: #b8d689;  /* lighter olive — "as that part plays" */
}
.audio-time {
    flex-shrink: 0;
    font-size: 12px;
    color: var(--text-dim);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* ---------- Audio tab ----------------------------------------------------
   Three bottom slots in the prompt card: Design voice, Select voice,
   Drive voice. The icon column uses `⊕` on all three. Design is a pure
   button (opens the design modal) — solid border, accent hover. Select
   + Drive are drop targets — they inherit `.frame-slot.empty`'s dashed
   border. Select also doubles as an inline search picker on click
   (→ `.picker-active` solid-accent), and fills as a pill on voice-tile
   drop (→ `.filled` olive). */
.frame-slot.audio-design-slot {
    border: 1px solid var(--border);
    background: transparent;
    color: var(--text-dim);
    cursor: pointer;
}
.frame-slot.audio-design-slot:hover {
    border-color: var(--accent);
    color: var(--text);
}
.audio-drive-slot.drag-over,
.audio-select-slot.drag-over {
    border-style: solid;
    border-color: var(--accent);
    background: rgba(131, 149, 111, 0.12);
    color: var(--text);
}
/* Select-voice + Design-voice modals share an identical footprint — the
   Design modal changes internal content across pre/post-preview states
   (textarea only → 3 previews + Save-as fields), but the outer frame
   stays the same size throughout so it doesn't pop open/shrink.
   Width / min-height are matched; modal-actions pin to the bottom via
   `margin-top: auto` so a half-populated body doesn't leave the action
   buttons floating mid-dialog. */
.voice-select-dialog,
.voice-design-dialog {
    /* Matches the rendered width of the audio-editor modal in ASSETS
       (~708px). `min(708px, 92vw)` lets the modals narrow gracefully on
       phones while pinning to 708px on desktop. */
    width: min(708px, 92vw);
    max-width: min(708px, 92vw);
    /* Tight 12px padding all round — matches the audio-editor modal in
       ASSETS. Combined with `margin-bottom: 0` on the last visible
       child, the content's bottom edge sits exactly 12px from the
       dialog bottom. */
    padding: 12px;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
}
/* Only the Select-voice modal keeps a fixed minimum height — it needs a
   stable surface so the scrollable voice list looks consistent whether
   there are few or many voices. The Design modal is content-driven so
   its bottom padding stays exactly 12px regardless of state. */
.voice-select-dialog { min-height: 580px; }
/* Kill trailing margin on the last visible element so the dialog's
   12px bottom padding is the only space between content and edge. */
.voice-design-dialog > *:last-child { margin-bottom: 0; }
.voice-design-dialog .voice-design-previews { margin: 4px 0 0; }
.voice-select-dialog .audio-voice-list {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    margin: 4px 0 0;
    border: 1px solid var(--border);
    border-radius: var(--radius);
}
.audio-voice-popover.hidden { display: none; }
.audio-voice-popover-header {
    padding: 8px 12px;
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    color: var(--text-dim);
    border-bottom: 1px solid var(--border);
}
.audio-voice-group-label {
    padding: 8px 12px 4px;
    font-size: 10px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-dim);
}
.audio-voice-item {
    display: block;
    width: 100%;
    text-align: left;
    padding: 8px 12px;
    background: transparent;
    border: none;
    color: var(--text);
    font-size: 13px;
    cursor: pointer;
}
.audio-voice-item:hover { background: rgba(107, 143, 74, 0.12); }
.audio-voice-item-name { font-weight: 500; }
.audio-voice-desc {
    color: var(--text-dim);
    font-size: 11px;
}
/* `+ Design new voice…` row — visually distinct so it doesn't read like a
   voice. Sits above the stock-voice header via a thin separator below it. */
.audio-voice-design-row {
    color: var(--accent);
    font-weight: 500;
}
.audio-voice-design-row .audio-voice-item-name { color: var(--accent); }
.audio-voice-sep {
    height: 1px;
    background: var(--border);
    margin: 2px 0;
}
/* Designed-voice row: voice button on the left, small trash on the right.
   Trash is revealed on row hover. */
.audio-voice-row {
    display: flex;
    align-items: center;
    position: relative;
}
.audio-voice-row .audio-voice-item { flex: 1; min-width: 0; }
.audio-voice-designed-mark {
    color: var(--accent);
    font-size: 10px;
    margin-left: 4px;
}
.audio-voice-delete {
    flex-shrink: 0;
    background: transparent;
    border: none;
    color: var(--text-dim);
    font-size: 14px;
    padding: 8px 10px;
    cursor: pointer;
    opacity: 0;
    transition: opacity 0.12s, color 0.12s;
}
.audio-voice-row.is-designed:hover .audio-voice-delete { opacity: 1; }
.audio-voice-delete:hover { color: var(--danger); }

/* + Design… link beside the voice pill in the audio-voice-bar */
.audio-voice-design-link {
    margin-left: auto;
    background: transparent;
    border: none;
    color: var(--accent);
    font-size: 11px;
    cursor: pointer;
    padding: 3px 6px;
    border-radius: 4px;
    transition: background 0.12s;
}
.audio-voice-design-link:hover { background: rgba(107, 143, 74, 0.14); }

/* ---------- Voice Design modal ----------
   Dialog size/layout defined up top (shared with .voice-select-dialog). */

/* Prompt-box styling for description + preview-line contenteditable divs.
   Mirrors the main Generate `.prompt-wrapper` + `.prompt-input` visual
   so the modal reads as the same material the user types into on the
   Video / Image / Audio tabs. */
.voice-design-promptbox {
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    transition: border-color 0.1s;
    margin-bottom: 16px;
}
.voice-design-promptbox:focus-within { border-color: var(--accent); }
.voice-design-promptinput {
    width: 100%;
    /* Generous minimum height to encourage specificity — users are more
       likely to write age + register + accent + pace + tone when the box
       visually invites a paragraph, not a line. */
    min-height: 140px;
    padding: 12px;
    color: var(--text);
    /* Matches the Generate drawer's prompt (`.gen-panel .prompt-input`)
       so the modal feels like the same material. */
    font-size: 13px;
    font-family: inherit;
    outline: none;
    white-space: pre-wrap;
    word-break: break-word;
}
/* Preview-line is a single paragraph, shorter box — distinct from the
   description which is where we want the user to expand. */
.voice-design-promptinput-single {
    min-height: 64px;
    padding: 10px 12px;
}
/* Contenteditable placeholder — shows the `data-placeholder` attr when
   the element is empty (no text nodes, no <br>). */
.voice-design-promptinput:empty::before {
    content: attr(data-placeholder);
    color: var(--text-dim);
    pointer-events: none;
}

.voice-design-previews {
    margin: 4px 0 16px;
    display: flex;
    flex-direction: column;
    gap: 12px;
}
.voice-design-previews.hidden { display: none; }
.voice-design-preview {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 10px 12px;
    background: var(--bg-input);
    transition: border-color 0.12s, background 0.12s;
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.voice-design-preview.is-picked {
    border-color: var(--accent);
    background: rgba(131, 149, 111, 0.12);
}
.voice-design-preview-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
}
.voice-design-preview-label {
    font-size: 13px;
    color: var(--text);
    font-weight: 500;
    /* When rendered as a button (.voice-design-preview-playlabel) the
       label becomes clickable — strip the default button chrome so it
       reads as text, add a subtle hover to telegraph interactivity. */
    background: transparent;
    border: none;
    padding: 0;
    cursor: pointer;
    font-family: inherit;
    text-align: left;
    transition: color 0.12s;
}
.voice-design-preview-playlabel:hover {
    color: var(--accent);
}
/* Waveform tile — same palette as the audio tile + modal editor. Click
   anywhere on it to toggle play / pause (loops until you tap again).
   No play button; the tile itself is the control. */
.voice-design-preview-wave {
    position: relative;
    height: 56px;
    width: 100%;
    background: rgba(255, 255, 255, 0.02);
    border-radius: 4px;
    cursor: pointer;
    overflow: hidden;
    transition: background 0.12s;
}
.voice-design-preview-wave:hover {
    background: rgba(255, 255, 255, 0.05);
}
.voice-design-preview.is-playing .voice-design-preview-wave {
    background: rgba(131, 149, 111, 0.08);
}
.voice-design-preview-svg {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    display: block;
}
.voice-design-preview-path { fill: rgba(255, 255, 255, 0.45); }
.voice-design-preview.is-playing .voice-design-preview-path {
    fill: rgba(255, 255, 255, 0.82);
}
/* White-line playhead — mirrors `.audio-playhead` in the lightbox audio
   editor. Hidden by default; revealed + animated when the tile is
   playing (position driven by RAF in JS, wraps at loop boundaries). */
.voice-design-preview-playhead {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: 2px;
    background: rgba(255, 255, 255, 0.9);
    border-radius: 1px;
    transform: translateX(-1px);
    opacity: 0;
    pointer-events: none;
}
.voice-design-preview.is-playing .voice-design-preview-playhead,
.voice-design-preview.is-paused .voice-design-preview-playhead {
    opacity: 1;
}
/* Pending (placeholder) state. Two visual modes inside the same tile:
   • idle → green accent star, centred (shown on modal open).
   • is-generating → spinner + "Generating" label, matching the
     image/video gallery tile's generating placeholder.
   JS flips `.is-generating` on the `.voice-design-preview-wave` to swap
   without a DOM rebuild. */
.voice-design-preview.is-pending { opacity: 0.7; }
.voice-design-preview-wave.is-pending {
    cursor: default;
    display: flex;
    align-items: center;
    justify-content: center;
}
.voice-design-preview-wave.is-pending:hover { background: rgba(255, 255, 255, 0.02); }
.voice-design-preview-idle-star {
    /* Same glyph + colour as the drawer-rail stars at the left of the
       page so the modal's "empty slot" affordance reads like the rest
       of the app. */
    color: var(--accent);
    font-size: 20px;
    line-height: 1;
}
.voice-design-preview-generating {
    display: none;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 6px;
    color: var(--text-dim);
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
}
.voice-design-preview-wave.is-generating .voice-design-preview-idle-star { display: none; }
.voice-design-preview-wave.is-generating .voice-design-preview-generating { display: flex; }
.voice-design-preview-generating-label { color: var(--text-dim); }
/* Scale the shared .spinner down to sit comfortably inside the 56px
   tile — same glyph/animation as the image/video placeholder tiles. */
.voice-design-preview-generating .spinner {
    width: 18px;
    height: 18px;
    border-width: 2px;
}

/* Generate button under preview line — full-width green, same look as
   the main Generate button (`.generate-btn`). Just adds breathing room. */
.voice-design-generate-btn {
    margin-top: 4px;
    margin-bottom: 16px;
}
.voice-design-preview.is-playing .voice-design-preview-wave { /* legacy no-op */ }
.voice-design-pick { flex-shrink: 0; }
/* Per-tile action slot — hosts the Select button, then swaps to a Name
   input + Save button on Select click, and to a "Saved" indicator after
   save. All three states live in the same container so the header never
   changes width abruptly. */
.voice-design-preview-action {
    display: inline-flex;
    align-items: center;
    justify-content: flex-end;
    gap: 6px;
    flex-shrink: 0;
    /* Fixed width + height so the header never shifts — horizontally or
       vertically — as the slot walks through its states (Select button →
       Name+Save → Saved). The naming state is the widest; all states
       right-align within this 240×32 ceiling. */
    width: 240px;
    height: 32px;
}
/* Match the Select button's height to the fixed action-slot height so
   the initial state doesn't stand taller than the Name+Save pair and
   nudge the tile a couple of pixels when Select is clicked. */
.voice-design-pick {
    height: 32px;
    display: inline-flex;
    align-items: center;
}
.voice-design-name-inline {
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text);
    font-size: 13px;
    padding: 4px 8px;
    height: 32px;
    width: 160px;
    outline: none;
    font-family: inherit;
    transition: border-color 0.12s;
}
.voice-design-name-inline:focus { border-color: var(--accent); }
.voice-design-name-inline:disabled { opacity: 0.6; }
.voice-design-name-inline.is-saved {
    /* Keep the typed name legible after save but read as locked. */
    border-color: rgba(131, 149, 111, 0.5);
    background: rgba(131, 149, 111, 0.08);
    color: var(--text);
    cursor: default;
    opacity: 1;
}
.voice-design-save-inline {
    height: 32px;
    padding: 0 12px;
    font-size: 13px;
    line-height: 1;
    display: inline-flex;
    align-items: center;
}
/* Small × that cancels the naming state and returns the slot to the
   idle Select button. Subtle text button — same feel as the other
   inline affordances in the row, not drawing attention. */
.voice-design-name-cancel {
    background: transparent;
    border: none;
    color: var(--text-dim);
    font-size: 16px;
    line-height: 1;
    cursor: pointer;
    padding: 0 4px;
    height: 32px;
    display: inline-flex;
    align-items: center;
    transition: color 0.12s;
}
.voice-design-name-cancel:hover { color: var(--text); }
.voice-design-save-inline.is-saved {
    /* Olive-filled confirmation — stays in place of the Save button so
       the header width doesn't jump. Disabled so re-saves can't fire. */
    background: var(--accent);
    border-color: var(--accent);
    color: var(--bg);
    opacity: 1;
    cursor: default;
}
.voice-design-saved-indicator {
    color: var(--accent);
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    padding: 4px 8px;
}
/* No global .hidden rule in this stylesheet — scope it explicitly for
   the modal's action button so it can toggle visibility cleanly. */
#voice-design-run.hidden { display: none; }

/* ---------- Voice tiles (ASSETS → Audio tab) --------------------------
   Backed by the `voices` table, not `assets`. Designed + stock voices
   both render. ELV + VOICE badges olive; STOCK badge grey. Envelope
   area is clickable → opens play-only modal. Drag starts an
   application/x-ref-asset payload (type=voice) that the Select-voice
   slot accepts. */
.gallery-card.voice-card .audio-waveform-container.voice-preview-trigger {
    cursor: pointer;
}
.gallery-card.voice-card .audio-tile-path {
    /* Slightly darker envelope than plain audio tiles so voice reads as
       its own "family" — consistent with ELV olive badge tone. */
    fill: rgba(131, 149, 111, 0.55);
}

/* ---------- Voice preview modal (play-only, audio-editor shell) -----
   Reuses the audio-editor's `.audio-editor` + `.audio-wave-*` +
   `.audio-playhead` + `.audio-editor-toolbar` + `.audio-play-btn`
   classes so the modal looks identical to the regular audio lightbox —
   same envelope, same played-path sweep, same white playhead, same
   toolbar typography. The only dialog-specific rules below are the
   outer dialog box (width, padding, flow). Toolbar controls we don't
   want (trim/zoom/reset/handles/minimap/IN-OUT labels) are simply not
   rendered in the modal HTML. */
.voice-preview-dialog {
    width: min(720px, 92vw);
    max-width: min(720px, 92vw);
    padding: 16px;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: 12px;
}
.voice-preview-dialog h3.voice-preview-title {
    margin: 0;
    font-size: 15px;
    font-weight: 600;
    color: var(--text);
    padding-right: 24px;  /* clear the × */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* Voice modal sits in the modal-overlay centre; the normal audio editor
   lives inside the lightbox shell and has the lightbox scroll chrome
   around it. Add a thin border so the editor reads as a card in the
   modal. */
.voice-preview-dialog .voice-preview-editor {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 12px;
    background: rgba(255, 255, 255, 0.02);
}
/* Clicks on the waveform seek the preview — pointer affords this. */
.voice-preview-dialog .audio-wave-viewport { cursor: pointer; }
/* Hidden-by-default Re-run button (toggled by JS when the open voice
   has a source_prompt). Extra class-level hide so a CSS-only load
   without JS doesn't flash the button in. */
.voice-preview-rerun.hidden { display: none; }

/* ---------- Responsive ---------- */
@media (max-width: 600px) {
    .container { padding: 12px; }
    .controls-row { flex-wrap: wrap; }
    .frame-uploads { flex-direction: column; }
    .gallery-grid { grid-template-columns: 1fr; }
    .project-layout { flex-direction: column; }
    .project-sidebar { width: 100%; border-right: none; border-bottom: 1px solid var(--border); }
}
