/* =========================================================
   GEXI Studio v2 — stylesheet

   Visual direction (from build #1, change here):
     "2028 not 2026" — high contrast, electric accent,
     sharp small radius, oversized typography on idle.

   Layout direction:
     Stable canvas. The canvas frame holds its size when
     selection / overlay editing happens. ONLY a deep-edit
     sheet (typing text, picking sounds) overlays the
     bottom region. Everything else snaps into the ribbon
     contextually without resizing the canvas.
   ========================================================= */

:root {
  /* PALETTE — change here.
     v2.0.4 — deeper grays per user feedback. CapCut-class blacks. */
  --bg:           #06070b;
  --surface:      #0d0f15;
  --surface-2:    #14171f;
  --surface-3:    #1d2029;
  --border:       #1d2030;
  --border-2:     #2a2e3d;

  --text:         #f7f8fa;
  --text-2:       #b1b6c2;
  --text-3:       #6c7280;

  /* contrasting accents — pick one when you see it */
  --accent:       #a3ff12;   /* electric green */
  --accent-2:     #ff2bd6;   /* hot magenta */
  --on-accent:    #0a0b10;   /* text/icon ON the accent */

  --warn:         #ffb020;
  --error:        #ff4564;

  /* spacing scale (4px base) */
  --s1: 4px;  --s2: 8px;  --s3: 12px; --s4: 16px;
  --s5: 24px; --s6: 32px; --s7: 48px;

  /* radius — sharper on small, generous on big */
  --r-xs: 4px;  --r-sm: 8px; --r-md: 12px; --r-lg: 18px; --r-pill: 999px;

  /* timing */
  --t-fast: 140ms;
  --t-base: 220ms;
  --t-slow: 360ms;
  --ease:     cubic-bezier(.2, .8, .2, 1);
  --ease-out: cubic-bezier(.16, 1, .3, 1);

  /* shadows */
  --sh-1: 0 1px 2px rgba(0,0,0,0.30);
  --sh-2: 0 6px 16px rgba(0,0,0,0.40);
  --sh-3: 0 18px 48px rgba(0,0,0,0.55);

  /* layout — STABLE row sizes, all reads of these are layout-pinned.
     v2.0.19: tightened header / controls / ribbon to give canvas and
     timeline more breathing room per user feedback. */
  --header-h:   50px;        /* was 56 — top chrome thinner */
  --controls-h: 42px;        /* was 48 — preview controls thinner */
  --tl-h:       184px;       /* was 172 — gets +12 from ribbon */
  --ribbon-h:   64px;        /* was 76 — bottom strip thinner */
  --tap:        44px;        /* min tap target */
  --canvas-max: 62dvh;        /* was 56dvh — uses freed top/bottom chrome */

  /* z stack — canonical, never inline-override */
  --z-canvas:   1;
  --z-overlay:  10;
  --z-controls: 20;
  --z-timeline: 30;
  --z-ribbon:   40;
  --z-sheet:    60;
  --z-modal:    80;
  --z-toast:    100;
}

/* =========================================================
   Base + reset
   ========================================================= */
*, *::before, *::after { box-sizing: border-box; }
html, body {
  margin: 0; padding: 0;
  height: 100%; height: 100dvh;
  overflow: hidden;
  background: var(--bg); color: var(--text);
  font: 500 15px/1.4 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
  -webkit-touch-callout: none;
  -webkit-user-select: none; user-select: none;
  overscroll-behavior: none;
}
/* All interactive elements: remove double-tap zoom delay on iOS */
button, [role="button"], a, label, input, select, textarea, .v2-tl-bar, .v2-canvas-text, .v2-canvas-sticker {
  -webkit-tap-highlight-color: transparent;
}
input, textarea, [contenteditable] { -webkit-user-select: text; user-select: text; }
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
/* v2.0.263 — Universal active-press feedback. Every button gets a
   subtle scale-down on :active that overrides nothing (transform is
   not normally set on buttons). The :not() guards exclude elements
   that drive their own transform via inline style — drag handles,
   canvas overlays, sheet heads — so press feedback never fights
   user-driven transforms. CapCut's whole interface feels tactile
   because EVERY tap registers visually within 1 frame; we now match. */
button:not(.v2-canvas-text):not(.v2-canvas-sticker):not(.v2-modal-head):not(.v2-tl-bar):not(.v2-ribbon-item):active {
  transform: scale(0.96);
  transition: transform 90ms cubic-bezier(.2, .8, .2, 1);
}

/* =========================================================
   v2.0.263 — Toaster. Long-missing UI for the toast() helper.
   Stack at top-right, max 3 visible. Slide-in from right with a
   small overshoot, sit for the duration, fade out. Tap to dismiss.
   ========================================================= */
.v2-toaster {
  position: fixed;
  top: calc(env(safe-area-inset-top, 0px) + 12px);
  right: 12px;
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 8px;
  pointer-events: none;       /* let clicks through gaps */
  max-width: calc(100vw - 24px);
}
.v2-toast {
  pointer-events: auto;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px 10px 12px;
  background: rgba(20, 22, 28, 0.94);
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 999px;
  color: var(--text);
  font: 600 13px/1.2 'Inter', system-ui, sans-serif;
  letter-spacing: 0.01em;
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45),
              0 1px 0 rgba(255, 255, 255, 0.04) inset;
  max-width: min(420px, calc(100vw - 24px));
  animation: v2-toast-in 280ms cubic-bezier(.16, 1.2, .4, 1);
  transition: opacity 160ms var(--ease), transform 160ms var(--ease);
  text-align: left;
  white-space: normal;
  word-break: break-word;
}
.v2-toast:active { transform: scale(0.97); }
.v2-toast-dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
  box-shadow: 0 0 6px currentColor;
}
.v2-toast-msg {
  color: var(--text);
  /* Cap line count so a runaway error message doesn't stretch
     across the whole viewport — 3 lines is plenty. */
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* Kind-specific accents on the dot + a tinted border. The text color
   stays neutral white so the message reads clearly; the chip identity
   reads from the color of the dot + the subtle border tint. */
.v2-toast--info    { color: #7dd3fc; border-color: rgba(125, 211, 252, 0.22); }
.v2-toast--success { color: var(--accent); border-color: rgba(163, 255, 18, 0.28); }
.v2-toast--warn    { color: #fbbf24; border-color: rgba(251, 191, 36, 0.28); }
.v2-toast--error   { color: #ff6b6b; border-color: rgba(255, 107, 107, 0.32); }

@keyframes v2-toast-in {
  0%   { transform: translateX(40%) scale(0.9); opacity: 0; }
  60%  { transform: translateX(-6%) scale(1.02); opacity: 1; }
  100% { transform: translateX(0)    scale(1);   opacity: 1; }
}
img, svg { display: block; max-width: 100%; }

#v2-app { height: 100dvh; display: contents; }

/* =========================================================
   Editor — the heart. CSS Grid with FIXED row sizes so
   nothing pushes the canvas around.
   ========================================================= */
.v2-edit {
  position: fixed; inset: 0;
  display: grid;
  /* Add safe-area insets so the header clears the notch and the
     ribbon clears the home indicator on iPhone, plus any cutouts
     on Android. The row heights stay the same — only the OUTSIDE
     edges get padding. */
  padding-top:    env(safe-area-inset-top, 0px);
  padding-bottom: env(safe-area-inset-bottom, 0px);
  padding-left:   env(safe-area-inset-left, 0px);
  padding-right:  env(safe-area-inset-right, 0px);
  grid-template-rows:
    var(--header-h)
    1fr
    var(--controls-h)
    var(--tl-h)
    var(--ribbon-h);
  height: 100dvh;
  background: var(--bg);
  overflow: hidden;
}

/* =========================================================
   v2.0.493 — IMMERSIVE TOOL MODE (CapCut-style).
   While a bottom tool sheet is open, the top chrome (X / search / title /
   Export) isn't needed — collapse it and hand the space to the canvas so the
   preview reads closer to full-screen. The canvas stays fully interactive
   (tap-subject, eyedropper, brush, drag PiP all still work).

   --header-h is registered as an animatable <length> so the grid's header row
   collapses 50px→0 SMOOTHLY (one motion), and the 1fr canvas row grows with it
   instead of snapping. --canvas-max is bumped past the freed space so the
   available row height (not the cap) is what bounds the canvas — it grows
   continuously as the row animates. Toggled by the `is-tool-immersive` class
   (main.js effect on the 8 bottom-sheet signals). Reversible: closing the sheet
   animates the header straight back. */
@property --header-h {
  syntax: '<length>';
  inherits: true;
  initial-value: 50px;
}
.v2-edit {
  transition: --header-h 300ms cubic-bezier(.2, .8, .2, 1);
}
.v2-edit.is-tool-immersive {
  --header-h:   0px;
  --canvas-max: 74dvh;   /* non-binding cap; row height drives the growth */
}
.v2-edit.is-tool-immersive .v2-header {
  opacity: 0;
  transform: translateY(-100%);
  pointer-events: none;
  border-bottom-color: transparent;
}

/* ----- Header ----- */
.v2-header {
  grid-row: 1;
  display: flex; align-items: center; gap: var(--s2);
  padding: 0 var(--s3);
  background: var(--bg);
  border-bottom: 1px solid var(--border);
  /* Smoothly fade + slide away in immersive mode (the grid row collapse is
     animated separately via --header-h). overflow:hidden so the 50px of
     content clips cleanly as the row shrinks to 0. */
  overflow: hidden;
  transition: opacity 200ms ease, transform 300ms cubic-bezier(.2, .8, .2, 1),
              border-bottom-color 200ms ease;
}
.v2-header-left, .v2-header-right { display: flex; align-items: center; gap: var(--s2); }
.v2-header-mid { flex: 1; display: flex; justify-content: center; align-items: center; gap: var(--s2); min-width: 0; }
.v2-header-title {
  font: 700 13px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--text-2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* v2.0.141 — Inline rename. The button variant shows a pencil hint
   icon and presses to scale. The input variant strips chrome so the
   typing feels like editing the title in place, not entering a form. */
.v2-header-title--btn {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 8px;
  margin: -4px -8px;
  background: transparent;
  border: 0;
  cursor: text;
  border-radius: 4px;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.v2-header-title--btn:hover,
.v2-header-title--btn:focus-visible {
  background: rgba(255,255,255,0.04);
  color: var(--text);
}
.v2-header-title--btn:active { background: rgba(255,255,255,0.08); }
.v2-header-rename-hint {
  display: inline-flex;
  color: var(--text-3);
  opacity: 0.55;
}
.v2-header-title--edit {
  font: 700 13px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text);
  background: rgba(163, 255, 18, 0.08);
  border: 1px solid rgba(163, 255, 18, 0.45);
  border-radius: 4px;
  padding: 5px 9px;
  margin: -5px -1px;
  outline: none;
  min-width: 0; max-width: 240px;
  text-align: center;
  caret-color: var(--accent);
}
.v2-header-title--edit:focus {
  box-shadow: 0 0 0 2px rgba(163, 255, 18, 0.22);
}
.v2-header-saved {
  font: 700 9px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent);
  background: rgba(163, 255, 18, 0.10);
  border: 1px solid rgba(163, 255, 18, 0.32);
  padding: 4px 6px;
  border-radius: var(--r-xs);
  animation: v2-header-saved-in 220ms var(--ease) 1;
}
@keyframes v2-header-saved-in {
  from { opacity: 0; transform: scale(0.85); }
  to   { opacity: 1; transform: scale(1); }
}

/* v2.0.385 — Server-overlay autosave chip. Renders inline in
   .v2-header-left next to the project name. Shares the .v2-header-
   saved geometry but has state-specific colors. */
.v2-sync-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font: 700 9px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  padding: 4px 6px;
  border-radius: var(--r-xs);
  margin-left: 6px;
  /* v2.0.386 — Audit Style #3: cap width on narrow viewports so the
     longer 'conflict' label doesn't blow past the export button on
     320px-wide phones. Truncate with ellipsis if the chip + title
     + buttons can't all fit. */
  max-width: 50vw;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  animation: v2-header-saved-in 220ms var(--ease) 1;
}
.v2-sync-chip.is-saving {
  color:      var(--text-2, #9ca3af);
  background: rgba(255, 255, 255, 0.04);
  border:     1px solid rgba(255, 255, 255, 0.10);
}
.v2-sync-chip.is-saving::before {
  /* tiny spinner dot to signal in-flight */
  content: '';
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--text-2, #9ca3af);
  animation: v2-sync-pulse 900ms ease-in-out infinite;
}
@keyframes v2-sync-pulse {
  0%, 100% { opacity: 0.35; }
  50%      { opacity: 1; }
}
/* v2.0.386 — Audit Style #3: respect prefers-reduced-motion. The
   pulse is a nice-to-have visual confirmation, not a status that
   the user needs to perceive via motion (the "Saving…" text label
   is the actual status). Vestibular-disorder users get a static
   dim dot. */
@media (prefers-reduced-motion: reduce) {
  .v2-sync-chip.is-saving::before {
    animation: none;
    opacity: 0.7;
  }
}
.v2-sync-chip.is-saved {
  color:      var(--accent);
  background: rgba(163, 255, 18, 0.10);
  border:     1px solid rgba(163, 255, 18, 0.32);
}
.v2-sync-chip.is-offline {
  color:      #fbbf24;
  background: rgba(251, 191, 36, 0.08);
  border:     1px solid rgba(251, 191, 36, 0.32);
}
.v2-sync-chip.is-conflict {
  /* v2.0.386 — Audit Style #3: brightened from #f87171 to #fca5a5
     so the 9px text against dark BG passes WCAG-AAA (~7:1
     contrast — pre-386 #f87171 was ~5.5:1, AA-normal only). */
  color:      #fca5a5;
  background: rgba(248, 113, 113, 0.08);
  border:     1px solid rgba(248, 113, 113, 0.32);
}

.v2-header-version {
  font: 800 9px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.12em;
  background: var(--accent); color: var(--on-accent);
  padding: 4px 6px; border-radius: var(--r-xs);
}
.v2-iconbtn {
  width: 44px; height: 44px;     /* iOS minimum tap target */
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--text); border-radius: var(--r-sm);
  transition: background var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
  touch-action: manipulation;    /* remove 300ms double-tap zoom delay */
}
.v2-iconbtn:active { transform: scale(0.94); }
.v2-iconbtn:active { background: var(--surface-2); }
.v2-iconbtn.is-disabled,
.v2-iconbtn:disabled {
  opacity: 0.32;
  cursor: default;
}
.v2-iconbtn.is-disabled:active,
.v2-iconbtn:disabled:active { background: transparent; transform: none; }
.v2-export {
  height: 32px; padding: 0 14px; border-radius: var(--r-pill);
  background: var(--accent); color: var(--on-accent);
  font: 700 12px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.02em;
  display: inline-flex; align-items: center; justify-content: center;
}
.v2-export:active { transform: scale(0.96); }

/* ----- Canvas frame (the stable one) ----- */
.v2-canvas-wrap {
  grid-row: 2;
  position: relative;
  display: flex; align-items: center; justify-content: center;
  background: var(--bg);
  overflow: hidden;
  z-index: var(--z-canvas);
}
.v2-canvas-frame {
  /* aspect-ratio + max-height + max-width alone gives a 0×0 element
     because the flex parent doesn't stretch the cross-axis. We need
     ONE explicit dimension for aspect-ratio to anchor against. height
     fills wrap (clamped by max-height: 56dvh), then aspect-ratio
     computes width. max-width: 100% prevents overflow on narrow phones
     where the aspect-derived width would otherwise exceed the viewport. */
  aspect-ratio: var(--canvas-aspect, 9 / 16);
  height: 100%;
  max-height: var(--canvas-max, 56dvh);
  max-width: 100%;
  background: #000;
  border-radius: var(--r-md);
  position: relative;
  overflow: hidden;
  /* v2.0.365 — Removed `box-shadow: 0 8px 32px rgba(0,0,0,0.5)`. User:
     "get rid of that preview shadow." The deep shadow made the preview
     read as a floating card; CapCut keeps the preview surface flat
     against the dark UI background, no shadow chrome — the preview IS
     the focus, not a container. */
  /* v2.0.381 — Audit regression R3: on AMOLED phones the preview
     frame's #000 fill is indistinguishable from the dark UI surround
     — the canvas edge is invisible until something paints. A 1px
     subtle border restores the edge without re-introducing the v365
     floating-card feel. Color uses surface-tinted accent so light-
     theme users also see it. */
  border: 1px solid rgba(255, 255, 255, 0.06);
  /* v2.0.335 — iOS Safari interprets quick double taps on the canvas
     as zoom-in; the documented double-tap-to-enter-inline-edit gesture
     (main.js ~1742) was being hijacked. `manipulation` also kills the
     300ms tap delay across all browsers.
     v2.0.357 — Changed from `manipulation` to `none`. `manipulation`
     means "browser handles pinch-zoom" — which ate the SECOND finger
     of any two-finger gesture on the canvas, so our pinch+rotate
     handler (line ~983) never fired for text/sticker/PiP scale+rotate.
     User report: "i can move things with one finger but i cant use
     any of the 2 finger gestures." `none` disables ALL browser touch
     gestures within the canvas → our pointer-event handlers see every
     finger. Also covers double-tap-to-zoom (the v335 concern). Page
     scrolling outside the canvas is unaffected (touch-action scopes
     to the element). */
  touch-action: none;
}
/* Background is applied directly to .v2-canvas-frame inline now —
   simpler than the var-based child div, and one fewer thing to debug. */

/* Real media (video / image) rendered on canvas. One element per
   scene with a url, all pre-mounted, only the active one is opaque.
   Path A.2 — replaces the placeholder scene-color background as the
   primary visual when media is present.
   v2.0.57 — DEPRECATED. The canvas renderer (.v2-canvas-paint) now
   paints all media. Keeping the rule so old projects don't break
   if any inline <video class="v2-canvas-media"> remains, but new
   code should not produce these. */
.v2-canvas-media {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  opacity: 0;
  z-index: 1;
  pointer-events: none;
  background: #000;
  transition: opacity 60ms linear;
}
.v2-canvas-media.is-active {
  opacity: 1;
  z-index: 2;
}

/* v2.0.57 — the canvas renderer. ONE <canvas> paints every visible
   thing per frame: scene bg, active video/image (with filter),
   text overlays, stickers, animations, transitions. Same renderFrame
   function fires offscreen at export time. CapCut principle: preview
   IS render. */
.v2-canvas-paint {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  display: block;
  z-index: 1;
  pointer-events: none;   /* hit-targets live on the DOM overlay layer above */
  background: transparent;
}

/* The DOM text/sticker elements below the canvas paint are now
   INPUT-ONLY: they capture drag/pinch + hold the selection halo +
   delete chip + inline-edit contenteditable. Their visual text
   content is invisible — the canvas renderer paints the real text
   on top at the same coordinates. */
.v2-canvas-text:not(.is-editing) { color: transparent !important; }
.v2-canvas-text:not(.is-editing) .v2-canvas-text-content {
  background: transparent !important;
  /* v2.0.65 — kill backdrop-filter on the input-only hit-target. The
     DOM div sits ON TOP of the canvas (z-index 10 vs canvas 1); leaving
     backdrop-filter:blur(3px) active here literally blurs the
     canvas-painted text underneath. backdrop-filter restored during
     inline edit (when the DOM actually paints text). */
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
  text-shadow: none !important;
  -webkit-text-stroke: none !important;
}
/* Hide stickers' DOM emoji — canvas paints them. Halo + delete stay. */
.v2-canvas-sticker .v2-canvas-sticker-content {
  color: transparent !important;
}

/* Active scene label chip — small pill in canvas top-left so the
   user can SEE which scene is active at playhead. Placeholder
   until real video frame rendering. */
.v2-canvas-scene-tag {
  position: absolute;
  top: 12px; left: 12px;
  padding: 5px 10px;
  border-radius: 999px;
  background: rgba(0,0,0,0.45);
  backdrop-filter: blur(6px);
  color: #fff;
  font: 700 10px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  pointer-events: none;
  z-index: 4;
}

/* ---- Canvas text overlay (v2.0.24 — halo, not box) ---- */
/* The selected state is a soft accent halo via box-shadow + tiny dot
   handles, not a hard rectangle border with corner squares. Reads as
   "this is alive and grabbable" not "this is in a desktop dialog." */
.v2-canvas-text {
  position: absolute;
  display: inline-block;
  /* font, color, stroke, shadow now set via inline style from overlay
     fields (v2.0.46 + v2.0.48). The line-height + font-weight defaults
     apply when no overlay has been styled. */
  font-weight: 800;
  line-height: 1.2;
  /* v2.0.260 — Removed `white-space: nowrap`. The canvas-rendered text
     now wraps multi-line via wrapTextToLines in renderer.js; the DOM
     hit-target must wrap too so what the user TYPES in inline-edit
     mode matches what they see on commit. `pre-wrap` preserves
     user-typed spaces (collapsed in normal whitespace handling would
     drop trailing spaces in a way users don't expect when editing
     captions). `overflow-wrap: anywhere` breaks long unbroken strings
     (URLs, hashtags) so the box doesn't blow past the canvas edge —
     matches the char-level break fallback in wrapTextToLines. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  word-break: break-word;
  /* Cap the DOM box width so wrapping kicks in without escaping the
     canvas frame. 90vw works for portrait mobile; the canvas-frame
     parent is also constrained, so this is a defense-in-depth limit
     against accidental world-wide hit targets. */
  max-width: 90vw;
  text-align: center;
  user-select: none;
  cursor: grab;
  z-index: 10;
  will-change: transform;
  /* Smooth settle when drag/pinch releases — element transitions from
     lifted/offset state back to its base render. Disabled during active
     drag (.is-dragging / .is-transforming) so per-frame updates are instant. */
  transition: transform 140ms cubic-bezier(.2, .8, .2, 1),
              left      140ms cubic-bezier(.2, .8, .2, 1),
              top       140ms cubic-bezier(.2, .8, .2, 1);
}
.v2-canvas-text.is-dragging,
.v2-canvas-text.is-transforming,
/* v2.0.335 — Also disable during playback. The 140ms transition was
   chasing 60Hz signal writes from keyframed transforms — DOM lagged
   behind canvas paint, so handles and bbox drifted off the visible text. */
.v2-canvas-frame.is-playing .v2-canvas-text { transition: none; }
.v2-canvas-text:active { cursor: grabbing; }
.v2-canvas-text.is-dragging,
.v2-canvas-text.is-transforming {
  z-index: 30;
  cursor: grabbing;
}
.v2-canvas-text-content {
  display: inline-block;
  pointer-events: none;
  padding: 4px 8px;
  border-radius: 6px;
  background: rgba(0,0,0,0.22);
  backdrop-filter: blur(3px);
  transition: box-shadow 180ms var(--ease), background 180ms var(--ease);
  outline: none;
  caret-color: var(--accent);
}
/* BG variants (v2.0.46) — controlled by .v2-canvas-text--bg-{value} on parent.
   "pill" = the default rounded pill (above). "box" = square corners. "none"
   = no background at all (relies on text-shadow for contrast). */
.v2-canvas-text--bg-box .v2-canvas-text-content {
  border-radius: 2px;
}
.v2-canvas-text--bg-none .v2-canvas-text-content {
  background: transparent;
  backdrop-filter: none;
  padding: 4px 6px;
}
/* v2.0.363 — Selection chrome on text overlays REMOVED. User reported
   "invisible weird box outline that shifts shape from different size
   squares to different size rectangles as i move my finger around the
   screen" — that was this box-shadow ring (a pill border on the
   .v2-canvas-text-content) which is sized to the text wrapper (changes
   width/height as content/scale changes). Pre-v363 it was meant as
   "subtle selection cue" but read as a phantom box on canvas. CapCut
   model: selection cue lives in the floating action toolbar and the
   tool sheet that opens; the overlay itself stays visually clean.
   Inline-edit (.is-editing) still gets a backdrop so the caret is
   visible — see rules at .v2-canvas-text.is-editing below (kept). */
/* Inline-editing state: text becomes contenteditable, ring gets a
   subtle dark backdrop so the caret is visible against any video. */
.v2-canvas-text.is-editing { z-index: 40; cursor: text; }
.v2-canvas-text.is-editing .v2-canvas-text-content {
  pointer-events: auto;          /* re-enable so taps reach contenteditable */
  user-select: text;
  -webkit-user-select: text;
  background: rgba(0, 0, 0, 0.45);
  box-shadow:
    0 0 0 1.5px rgba(255, 255, 255, 0.90),
    0 6px 18px rgba(0, 0, 0, 0.50);
}

/* ---- Canvas sticker ---- */
/* Same positioning model as text, but content is an emoji/glyph.
   No text-shadow, no background pill — the emoji speaks for itself.
   Halo on selection via drop-shadow filter. */
.v2-canvas-sticker {
  position: absolute;
  display: inline-block;
  user-select: none;
  cursor: grab;
  z-index: 11;
  will-change: transform;
  transition: transform 140ms cubic-bezier(.2, .8, .2, 1),
              left      140ms cubic-bezier(.2, .8, .2, 1),
              top       140ms cubic-bezier(.2, .8, .2, 1);
}
.v2-canvas-sticker.is-dragging,
.v2-canvas-sticker.is-transforming,
/* v2.0.335 — see comment on .v2-canvas-text playback gate above. */
.v2-canvas-frame.is-playing .v2-canvas-sticker { transition: none; }
.v2-canvas-sticker:active { cursor: grabbing; }
.v2-canvas-sticker.is-dragging,
.v2-canvas-sticker.is-transforming {
  z-index: 30;
  cursor: grabbing;
}
.v2-canvas-sticker-content {
  display: inline-block;
  pointer-events: none;
  /* v2.0.335 — Was 32px. renderer.js paints emoji at 64px
     (drawStickerOverlay line ~1363), so the DOM hit-target was half the
     visible size — selection bbox + corner handles sat well inside the
     painted sticker, making resize feel detached. Match 64px. */
  font: normal 64px/1 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', sans-serif;
  filter: drop-shadow(0 2px 6px rgba(0,0,0,0.45));
}
/* v2.0.363 — Sticker selection chrome removed (same reason as text
   above). Pre-v363 the 1px white drop-shadow ring outlined the
   STICKER WRAPPER (DOM size, not the painted emoji at 64px) — those
   sizes don't match, so the ring read as a phantom misaligned box.
   Selection cue moves to the action toolbar / tool sheet, same as
   text. The base drop-shadow stays for contrast against video. */

/* ---- Selection chrome — CapCut-style (v2.0.209) ----
   Was: invisible bbox + bright lime dot handles + red/cyan/lime
   chips. Now: subtle white bounding box + 11px white circle handles
   with translucent dark rings + small dark-translucent action chips
   with white icons. The selection chrome defers to the content;
   the content is the star. */
.v2-canvas-bbox {
  position: absolute;
  inset: -10px;
  pointer-events: none;
  z-index: 1;
  opacity: 1;
  /* v2.0.356 — CapCut gesture model: one finger anywhere = move,
     two fingers anywhere = scale + rotate. Two-finger pinch+rotate
     was already wired at the canvas frame since v286, but the visible
     bbox + corner scale handles + rotation handle were catching
     one-finger drags (finger lands on a corner handle, drag becomes
     a scale-by-handle gesture). User feedback: "moving with one
     finger manipulates not just movement but sizing." Plus the
     visible outline was off-center relative to text glyphs (descender
     padding asymmetry) → confusing. Fix: drop the visible border,
     drop scale + rotate handles. Selection indicator is now the
     floating action toolbar (delete/dup/keyframe) above the overlay,
     same as CapCut/iMovie/Reels. */
  border: none;
  box-shadow: none;
  animation: none;
}
@keyframes v2-bbox-in {
  from { opacity: 0; transform: scale(0.96); }
  to   { opacity: 1; transform: scale(1); }
}
.v2-canvas-handle {
  position: absolute;
  pointer-events: auto;
  z-index: 2;
  touch-action: none;
}
/* v2.0.356 — Hide single-finger scale + rotate handles in favor of the
   2-finger pinch+rotate gesture (CapCut model). One-finger drag now
   reliably means "move." Action chips (delete/dup/keyframe/edit) at
   the top stay visible; those are control buttons, not gestures. */
.v2-canvas-handle--scale,
.v2-canvas-handle--rotate {
  display: none !important;
}
/* v2.0.356 — Invisible halo. Expands the touch hit-area ~14px around
   text/sticker/PiP wrappers so finger taps NEAR the glyphs (not
   pixel-on-glyph) still register as a drag start. ::after is a child
   of the wrapper, so pointerdown on it bubbles up to the wrapper's
   drag handler. z-index:-1 keeps it BEHIND the visible content (text
   selection still works on the glyphs themselves). Replaces the
   visible bbox border the user complained about as "always offset." */
.v2-canvas-text::after,
.v2-canvas-sticker::after,
.v2-canvas-pip::after {
  content: '';
  position: absolute;
  inset: -14px;
  z-index: -1;
  pointer-events: auto;
}
/* Corner scale handles — small white circles with a translucent dark
   ring for contrast on bright video frames. v2.0.209 — 14px → 11px,
   lime fill → white. */
.v2-canvas-handle--scale {
  width: 11px; height: 11px;
  background: #ffffff;
  border-radius: 50%;
  box-shadow:
    0 0 0 1.5px rgba(0, 0, 0, 0.55),
    0 1px 3px rgba(0, 0, 0, 0.40);
  transition: transform 120ms var(--ease);
}
/* v2.0.335 — Invisible hit pad expands the touch target to 43×43px
   (Apple HIG 44min). Without this, the 11px visual circle was also the
   click region — thumbs missed the corner repeatedly. Mirrors the
   pattern used by the tracker bbox handles (v2.0.321). */
.v2-canvas-handle--scale::after {
  content: '';
  position: absolute;
  inset: -16px;
}
.v2-canvas-handle--scale:active,
.v2-canvas-text.is-transforming .v2-canvas-handle--scale,
.v2-canvas-sticker.is-transforming .v2-canvas-handle--scale {
  transform: scale(1.25);
  background: var(--accent);     /* brief lime confirm on the handle being dragged */
}
.v2-canvas-handle--tl { top: -6px;  left: -6px;  cursor: nwse-resize; }
.v2-canvas-handle--tr { top: -6px;  right: -6px; cursor: nesw-resize; }
.v2-canvas-handle--br { bottom: -6px; right: -6px; cursor: nwse-resize; }
.v2-canvas-handle--bl { bottom: -6px; left: -6px; cursor: nesw-resize; }
/* Rotate handle — small white circle on a thin stalk above the bbox.
   v2.0.209 — 16px → 13px, lime → white, stem thinner.
   v2.0.336 — Restored proper visual after v335 regression. The v335
   attempt to redraw the arrow via inset box-shadows produced solid
   concentric rings (no arc), so the rotate handle was indistinguishable
   from scale handles. Now: host element IS the visible 13×13 disk +
   curved-arrow icon (background-image SVG, gap rendered correctly),
   ::after is the invisible hit pad extending around the disk + stem,
   ::before is the stem. Hit pad: 28×42 (was 38×42 — narrowed so it
   doesn't overlap the delete chip on small overlays per audit A3). */
.v2-canvas-handle--rotate {
  /* Visible disk, restored to ~pre-v335 dimensions. */
  top: -28px; left: 50%;
  transform: translateX(-50%);
  width: 13px; height: 13px;
  border-radius: 50%;
  background: #ffffff;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'><path d='M3 6a3 3 0 1 1 .9 2.1' fill='none' stroke='%230a0b10' stroke-width='1.5' stroke-linecap='round'/><path d='M3 4.2v2H5' fill='none' stroke='%230a0b10' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/></svg>");
  background-size: 100% 100%;
  background-repeat: no-repeat;
  background-position: center;
  background-color: #ffffff;
  box-shadow:
    0 0 0 1.5px rgba(0, 0, 0, 0.55),
    0 1px 3px rgba(0, 0, 0, 0.40);
  cursor: grab;
  transition: transform 120ms var(--ease), background-color 120ms var(--ease);
}
.v2-canvas-handle--rotate::before {
  /* Stem: 1px-wide line from below the disk down to the bbox top edge.
     Was ::after pre-v335; ::after is now the hit pad. */
  content: '';
  position: absolute;
  top: 100%; left: 50%;
  transform: translateX(-50%);
  width: 1px; height: 14px;
  background: rgba(255, 255, 255, 0.65);
  border-radius: 1px;
}
.v2-canvas-handle--rotate::after {
  /* Invisible hit pad: 28×42 centered on the 13×13 disk, extending
     down to cover the stem area. Apple HIG-ish touch target without
     blowing up the visible footprint. Narrowed from v335's 38px so
     it doesn't overlap the delete chip on narrow overlays. */
  content: '';
  position: absolute;
  top: -10px; left: 50%;
  transform: translate(-50%, 0);
  width: 28px; height: 42px;
}
.v2-canvas-handle--rotate:active {
  transform: translateX(-50%) scale(1.20);
  background-color: var(--accent);
  cursor: grabbing;
}
/* v2.0.209 — Action chips (delete / duplicate / edit) all share the
   same subtle dark-translucent treatment. Was: red / cyan / lime
   colored badges screaming for attention. Now: 22px circles with a
   blurred dark backdrop + white icon, just visible enough to tap.
   Active state briefly tints lime (delete = red) for the confirmation
   feedback CapCut also does. */
.v2-canvas-handle--delete,
.v2-canvas-handle--keyframe {
  width: 22px; height: 22px;
  background: rgba(10, 11, 16, 0.78);
  color: #ffffff;
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0;
  border: 1px solid rgba(255, 255, 255, 0.30);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45);
  touch-action: manipulation;
  position: absolute;  /* needed for ::after hit pad below */
  transition: transform var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
/* v2.0.339 — Hit-pad expansion. Visible 22px chip but ~44px touch
   target (Apple HIG). Mirrors the v335 fix for scale/rotate handles.
   Action chips sit close to each other (right cluster at -11/17/45) so
   the pad is asymmetric — extending more inward (away from edges) than
   outward, to avoid stealing taps from the neighbour. */
.v2-canvas-handle--delete::after,
.v2-canvas-handle--keyframe::after {
  content: '';
  position: absolute;
  inset: -10px;
}
.v2-canvas-handle--delete   { top: -28px; right: -11px; }
/* v2.0.478 — removed dead --dup / --edit rules (their canvas chips were removed
   in v472; Duplicate now lives in the ribbon, Edit was never wired). */
/* v2.0.213 — Keyframe pin sits left of duplicate so the three
   right-side chips (keyframe / duplicate / delete) read as a
   coherent group, with keyframe — the most-used animation tool —
   nearest the bbox content. */
.v2-canvas-handle--keyframe { top: -28px; right: 45px;  }
.v2-canvas-handle--delete:active {
  transform: scale(0.92);
  background: rgba(239, 68, 68, 0.92);   /* red flash to confirm destructive intent */
}
.v2-canvas-handle--keyframe:active {
  transform: scale(0.92);
  background: rgba(163, 255, 18, 0.92);  /* lime flash for safe actions */
  color: #0a0b10;
}
/* v2.0.213 — Active keyframe pin: filled lime diamond when the
   overlay already has at least one keyframe, so the user can tell
   at a glance whether they're animating this overlay. Matches the
   universal NLE convention (CapCut, Premiere, AE — filled = key
   present, hollow = none).
   v2.0.221 — Polish: gradient fill + dual-ring glow for premium feel. */
.v2-canvas-handle--keyframe.is-active {
  background: linear-gradient(180deg, #c5ff4a, #a3ff12);
  color: #0a0b10;
  border-color: rgba(163, 255, 18, 0.85);
  box-shadow:
    0 2px 8px rgba(0, 0, 0, 0.45),
    0 0 0 1px rgba(163, 255, 18, 0.35),
    0 0 14px rgba(163, 255, 18, 0.55);
}
/* Bounding-box accent when keyframed. Subtle lime double-stroke so
   it doesn't dominate the selection chrome but is unmistakable. */
.v2-canvas-text.is-keyframed.is-selected .v2-canvas-bbox,
.v2-canvas-sticker.is-keyframed.is-selected .v2-canvas-bbox,
.v2-canvas-pip.is-keyframed.is-selected .v2-canvas-bbox {
  /* v2.0.292 — PiP joins the lime inset stroke on the bbox when
     the overlay is keyframed. Mirrors text + sticker exactly so
     the visual language is identical across all three kinds. */
  box-shadow: inset 0 0 0 1px rgba(163, 255, 18, 0.55);
}
/* Hide handles while inline-editing (typing) so chrome doesn't
   steal attention from the text. */
.v2-canvas-text.is-editing .v2-canvas-bbox { display: none; }

/* =========================================================
   v2.0.243 — Custom motion-path SVG overlay on canvas.
   Fills the canvas frame, no pointer-events so it doesn't block
   the underlying overlay drag/select gestures. Rendered above the
   canvas paint but below the selection chrome (z-index 8 vs
   chrome's 10+). */
.v2-motion-path-overlay {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  pointer-events: none;
  z-index: 8;
  animation: v2-motion-path-in 220ms ease-out;
}
@keyframes v2-motion-path-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
/* v2.0.244 — In edit mode the overlay becomes interactive. Bumped
   z-index above selection chrome (which is at 10) + drag handles
   (which are at 2). Cursor crosshair for canvas-bg taps. */
.v2-motion-path-overlay.is-editing {
  pointer-events: auto;
  z-index: 40;
  cursor: crosshair;
  /* Subtle backdrop dim so the path + handles read against busy
     video. Inset boxshadow vignette gives "you're in edit mode"
     visual context without obscuring underlying content. */
  background: radial-gradient(closest-side at 50% 50%,
    rgba(0, 0, 0, 0) 0%,
    rgba(0, 0, 0, 0.20) 100%);
}
.v2-motion-path-overlay.is-editing circle {
  /* Override pointer-events: none from the parent rule (circles in
     non-edit svg inherit). In edit mode, anchors/handles need to
     receive pointerdown. */
  pointer-events: auto;
}

/* Floating path-edit toolbar — top of canvas, glass-blur, lime
   accent. Same 2026 polish language as multi-select bar.
   v2.0.253 — max-width + horizontal scroll so the toolbar never
   gets clipped on narrow portrait phone canvases (~220px wide).
   With 6-8 buttons the toolbar exceeds canvas width — swipe-scroll
   keeps them all reachable. Snap-pad on Done so it stays visible. */
.v2-path-edit-bar {
  position: absolute;
  top: 8px; left: 50%;
  transform: translateX(-50%);
  display: inline-flex; align-items: center; gap: 6px;
  padding: 7px 8px 7px 6px;
  max-width: calc(100% - 16px);
  overflow-x: auto;
  scrollbar-width: none;
  -ms-overflow-style: none;
  background: linear-gradient(180deg, rgba(34, 38, 50, 0.94), rgba(18, 20, 28, 0.94));
  border: 1px solid rgba(163, 255, 18, 0.55);
  border-radius: 999px;
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  box-shadow: 0 6px 22px rgba(0, 0, 0, 0.55), 0 0 18px rgba(163, 255, 18, 0.25);
  z-index: 45;
  animation: v2-path-edit-bar-in 220ms cubic-bezier(.2, .7, .2, 1);
}
.v2-path-edit-bar::-webkit-scrollbar { display: none; }
/* Done button + tool buttons shouldn't shrink in the flex scroll */
.v2-path-edit-done,
.v2-path-edit-tool,
.v2-path-edit-divider,
.v2-path-edit-count { flex: 0 0 auto; }
@keyframes v2-path-edit-bar-in {
  from { transform: translateX(-50%) translateY(-8px); opacity: 0; }
  to   { transform: translateX(-50%) translateY(0);    opacity: 1; }
}
.v2-path-edit-done {
  display: inline-flex; align-items: center;
  padding: 7px 16px;
  background: linear-gradient(180deg, #c5ff4a, #a3ff12);
  border: none;
  color: #0a0b10;
  font: 800 12px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  border-radius: 999px;
  cursor: pointer;
  touch-action: manipulation;
  box-shadow: 0 0 12px rgba(163, 255, 18, 0.55);
  transition: transform var(--t-fast) var(--ease);
}
.v2-path-edit-done:hover  { transform: scale(1.03); }
.v2-path-edit-done:active { transform: scale(0.94); }
.v2-path-edit-tools {
  display: inline-flex; align-items: center; gap: 4px;
}
.v2-path-edit-tool {
  display: inline-flex; align-items: center;
  padding: 6px 11px;
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.16);
  color: #f7f8fa;
  font: 700 11px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  border-radius: 999px;
  cursor: pointer;
  touch-action: manipulation;
  transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.v2-path-edit-tool:hover  { background: rgba(255, 255, 255, 0.14); border-color: rgba(255, 255, 255, 0.28); }
.v2-path-edit-tool:active { transform: scale(0.94); background: linear-gradient(180deg, #c5ff4a, #a3ff12); color: #0a0b10; border-color: rgba(163, 255, 18, 0.85); }
.v2-path-edit-tool.is-on  { background: linear-gradient(180deg, rgba(163, 255, 18, 0.30), rgba(163, 255, 18, 0.16)); border-color: rgba(163, 255, 18, 0.65); color: #a3ff12; }
/* v2.0.246 — Vertical divider between core tools + contextual
   per-anchor tools. Subtle. */
.v2-path-edit-divider {
  display: inline-block;
  width: 1px; height: 18px;
  background: rgba(255, 255, 255, 0.15);
  margin: 0 2px;
}
/* v2.0.246 — Anchor type pill. Distinguished from regular tools by
   an indicator color that flips: smooth = lime tint, corner =
   amber/orange tint. Helps the user see at a glance what kind of
   anchor they're editing. */
.v2-path-edit-tool--type {
  background: linear-gradient(180deg, rgba(163, 255, 18, 0.22), rgba(163, 255, 18, 0.10));
  border-color: rgba(163, 255, 18, 0.55);
  color: #a3ff12;
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  letter-spacing: 0;
}
.v2-path-edit-tool--type.is-corner {
  background: linear-gradient(180deg, rgba(255, 178, 41, 0.22), rgba(255, 178, 41, 0.10));
  border-color: rgba(255, 178, 41, 0.55);
  color: #ffb229;
}
.v2-path-edit-tool--type:active {
  transform: scale(0.94);
}

/* v2.0.247 — Smooth button with long-press progress fill. Tap fires
   light smooth; holding 500ms fires strong smooth. During the hold,
   a lime overlay fills L→R inside the button showing how close the
   long-press is to firing. Releasing mid-fill = tap behavior (light). */
.v2-path-edit-tool--smooth {
  position: relative;
  overflow: hidden;
  /* isolated for the ::before fill to clip cleanly inside border-radius */
  isolation: isolate;
}
.v2-path-edit-tool--smooth::before {
  content: '';
  position: absolute;
  top: 0; bottom: 0; left: 0;
  width: 0;
  background: linear-gradient(180deg, rgba(163, 255, 18, 0.55), rgba(163, 255, 18, 0.28));
  z-index: -1;
  pointer-events: none;
  transition: width 60ms linear;
}
.v2-path-edit-tool--smooth.is-holding::before {
  width: 100%;
  transition: width 500ms linear;
}
.v2-path-edit-tool--smooth.is-holding {
  color: #ffffff;
  border-color: rgba(163, 255, 18, 0.85);
}

/* v2.0.245 — Path-edit anchor-count badge + templates popover. */
.v2-path-edit-count {
  display: inline-flex; align-items: center;
  padding: 3px 8px;
  margin-left: 2px;
  background: rgba(0, 0, 0, 0.40);
  border: 1px solid rgba(255, 255, 255, 0.10);
  color: #cdd2dc;
  font: 700 10px/1 'JetBrains Mono', ui-monospace, monospace;
  border-radius: 999px;
  font-variant-numeric: tabular-nums;
}
.v2-path-templates-popover {
  position: absolute;
  top: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%);
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 8px;
  padding: 10px;
  background: linear-gradient(180deg, rgba(34, 38, 50, 0.96), rgba(18, 20, 28, 0.96));
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 14px;
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.65);
  z-index: 50;
  /* v2.0.253 — Fit inside narrow phone canvas (canvas-frame has
     overflow:hidden which would clip a too-wide popover). max-width
     is relative to the closest positioned ancestor — canvas-frame
     is `position: relative`, so 100% = canvas-frame width. The 16px
     subtract leaves 8px gap on each side. Internal scrollbar if
     more templates ever cause vertical overflow. */
  max-width: calc(100% - 16px);
  max-height: 60dvh;
  overflow-y: auto;
  width: max-content;
}
/* On narrow canvases, drop to a 3-col layout so chips stay readable.
   Triggered by container width via narrow phones; matches the px
   threshold where 4-col chips would compress past 48px width. */
@media (max-width: 360px) {
  .v2-path-templates-popover { grid-template-columns: repeat(3, 1fr); }
}
@keyframes v2-path-tpl-in {
  from { transform: translateX(-50%) translateY(-6px); opacity: 0; }
  to   { transform: translateX(-50%) translateY(0);    opacity: 1; }
}
.v2-path-template-chip {
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  padding: 6px 4px;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.10);
  border-radius: 10px;
  color: #f7f8fa;
  cursor: pointer;
  touch-action: manipulation;
  transition: background var(--t-fast) var(--ease), transform var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
}
.v2-path-template-chip:hover {
  background: rgba(255, 255, 255, 0.10);
  border-color: rgba(255, 255, 255, 0.22);
  transform: translateY(-1px);
}
.v2-path-template-chip:active {
  transform: scale(0.94);
  background: linear-gradient(180deg, rgba(163, 255, 18, 0.20), rgba(163, 255, 18, 0.08));
  border-color: rgba(163, 255, 18, 0.55);
}
.v2-path-template-svg {
  width: 48px; height: 48px;
  background: rgba(0, 0, 0, 0.40);
  border-radius: 6px;
  border: 1px solid rgba(255, 255, 255, 0.05);
}
.v2-path-template-lbl {
  font: 700 10px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.02em;
  color: #e7eaf0;
  text-align: center;
}

/* v2.0.244 — "Edit on canvas" button inside the Motion panel's
   expanded path-custom detail row. Distinct from sliders + dropdowns:
   primary lime action that takes the user OUT of the panel and INTO
   the canvas-edit experience. */
.v2-motion-edit-path-btn {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 11px 6px 9px;
  background: linear-gradient(180deg, rgba(163, 255, 18, 0.22), rgba(163, 255, 18, 0.12));
  border: 1px solid rgba(163, 255, 18, 0.55);
  color: #a3ff12;
  font: 700 11px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.02em;
  border-radius: 999px;
  cursor: pointer;
  touch-action: manipulation;
  transition: background var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.v2-motion-edit-path-btn:hover  { background: linear-gradient(180deg, rgba(163, 255, 18, 0.32), rgba(163, 255, 18, 0.18)); }
.v2-motion-edit-path-btn:active { transform: scale(0.94); background: linear-gradient(180deg, #c5ff4a, #a3ff12); color: #0a0b10; }

/* =========================================================
   v2.0.241 — Gesture-based multi-select chrome.

   Each multi-selected overlay gets a 2026-style "marching ants"
   dashed border + a corner checkmark badge. SVG-style dashes
   animate around the overlay's bbox via background-image
   conic-gradient + animated background-position (no per-pixel
   redraw, GPU-friendly).

   Also includes the floating multi-select action bar at the top
   of the canvas frame: count + Done button. Slide-in animation +
   glass blur for the 2026 polished feel.
   ========================================================= */
.v2-canvas-text.is-multi-selected,
.v2-canvas-sticker.is-multi-selected,
.v2-canvas-pip.is-multi-selected {
  /* Background dashed border via animated background-image.
     Using outline so the dash sits OUTSIDE the bbox content
     without affecting layout. Animation runs even when the
     overlay is otherwise static.
     v2.0.292 — PiP joins the marching-ants treatment so the
     long-press multi-select gesture has visible feedback on
     PiP overlays. */
  outline: 2px dashed #a3ff12;
  outline-offset: 4px;
  animation: v2-multi-march 1.2s linear infinite;
  border-radius: 6px;
}
@keyframes v2-multi-march {
  /* The "marching ants" effect — animate the outline by cycling
     its dasharray-equivalent (here we use a slight scale pulse on
     a ::before instead since outline doesn't animate dash offset).
     Simulated via subtle scale-pulse for a kinetic feel that
     reads as "active selection" without being noisy. */
  0%, 100% { outline-color: #a3ff12; }
  50%      { outline-color: rgba(163, 255, 18, 0.55); }
}
.v2-canvas-text.is-multi-selected::after,
.v2-canvas-sticker.is-multi-selected::after,
.v2-canvas-pip.is-multi-selected::after {
  /* Checkmark badge in the top-left corner — visual proof the
     overlay is multi-selected. Pulse-in on each add (the parent
     class is added/removed reactively). */
  content: '✓';
  position: absolute;
  top: -10px; left: -10px;
  width: 20px; height: 20px;
  display: flex; align-items: center; justify-content: center;
  background: linear-gradient(180deg, #c5ff4a, #a3ff12);
  color: #0a0b10;
  border: 1.5px solid #0a0b10;
  border-radius: 50%;
  font: 800 11px/1 'Inter', system-ui, sans-serif;
  box-shadow: 0 0 10px rgba(163, 255, 18, 0.65), 0 2px 4px rgba(0, 0, 0, 0.55);
  z-index: 25;
  pointer-events: none;
  animation: v2-multi-check-in 320ms cubic-bezier(.34, 1.56, .64, 1);
}
@keyframes v2-multi-check-in {
  0%   { transform: scale(0);    opacity: 0; }
  60%  { transform: scale(1.25); opacity: 1; }
  100% { transform: scale(1);    opacity: 1; }
}

/* Floating multi-select action bar — top of canvas frame. Glass
   blur + slight slide-in + lime accent on the count badge. */
.v2-multi-bar {
  position: absolute;
  top: 8px; left: 50%;
  transform: translateX(-50%);
  display: inline-flex; align-items: center; gap: 12px;
  padding: 8px 10px 8px 16px;
  background: linear-gradient(180deg, rgba(34, 38, 50, 0.94), rgba(18, 20, 28, 0.94));
  border: 1px solid rgba(163, 255, 18, 0.45);
  border-radius: 999px;
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  box-shadow: 0 6px 22px rgba(0, 0, 0, 0.55), 0 0 18px rgba(163, 255, 18, 0.25);
  z-index: 30;
  animation: v2-multi-bar-in 240ms cubic-bezier(.2, .7, .2, 1);
}
@keyframes v2-multi-bar-in {
  from { transform: translateX(-50%) translateY(-8px); opacity: 0; }
  to   { transform: translateX(-50%) translateY(0);    opacity: 1; }
}
.v2-multi-bar-count {
  display: inline-flex; align-items: center; gap: 5px;
  color: #e7eaf0;
  font: 700 12px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.02em;
}
.v2-multi-bar-num {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 22px; height: 22px;
  padding: 0 6px;
  background: linear-gradient(180deg, #c5ff4a, #a3ff12);
  color: #0a0b10;
  font: 800 12px/1 'Inter', system-ui, sans-serif;
  border-radius: 999px;
  box-shadow: 0 0 8px rgba(163, 255, 18, 0.65);
  font-variant-numeric: tabular-nums;
}
.v2-multi-bar-label {
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 10px;
  color: #cdd2dc;
}
.v2-multi-bar-btn {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 6px 14px;
  background: rgba(255, 255, 255, 0.10);
  border: 1px solid rgba(255, 255, 255, 0.18);
  color: #f7f8fa;
  border-radius: 999px;
  font: 700 11px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  cursor: pointer;
  touch-action: manipulation;
  transition: background var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.v2-multi-bar-btn:hover  { background: rgba(255, 255, 255, 0.18); }
.v2-multi-bar-btn:active { transform: scale(0.94); background: linear-gradient(180deg, #c5ff4a, #a3ff12); color: #0a0b10; border-color: rgba(163, 255, 18, 0.85); }

/* v2.0.136 (Polish I) — Alignment guide lines. Visible only during
   an overlay drag when the position has snapped to center / edge.
   Pink accent (matches existing snap-overlay color) so it doesn't
   blend with the lime selection chrome. Animated in via opacity. */
.v2-canvas-align-guides {
  position: absolute; inset: 0;
  pointer-events: none;
  z-index: 5;
}
.v2-canvas-align-line {
  position: absolute;
  background: var(--accent-2);
  box-shadow:
    0 0 8px rgba(255, 43, 214, 0.75),
    0 0 24px rgba(255, 43, 214, 0.35);
  animation: v2-canvas-guide-in 160ms var(--ease) 1;
}
@keyframes v2-canvas-guide-in {
  from { opacity: 0; }
  to   { opacity: 0.9; }
}
.v2-canvas-align-line--v { width: 1px; top: 0; bottom: 0; }
.v2-canvas-align-line--h { height: 1px; left: 0; right: 0; }
.v2-canvas-align-line--center.v2-canvas-align-line--v { left: 50%; transform: translateX(-0.5px); }
.v2-canvas-align-line--left   { left: 5%; }
.v2-canvas-align-line--right  { left: 95%; transform: translateX(-1px); }
.v2-canvas-align-line--center.v2-canvas-align-line--h { top: 50%; transform: translateY(-0.5px); }
.v2-canvas-align-line--top    { top: 5%; }
.v2-canvas-align-line--bottom { top: 95%; transform: translateY(-1px); }

/* ----- Timeline bar delete chip (any kind) -----
   v2.0.153 — Polish: rounded into a circle to match the canvas
   delete chip language, lifted with a drop shadow so it visually
   floats above the bar instead of clipping into the rail, plus a
   subtle hover/active scale animation. */
.v2-tl-bar-delete {
  position: absolute;
  top: -28px; right: -2px;
  width: 24px; height: 24px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--error);
  color: #fff;
  border: 2px solid var(--bg);
  border-radius: 50%;
  cursor: pointer;
  z-index: 13;
  touch-action: manipulation;
  font: 700 14px/1 'Inter', system-ui, sans-serif;
  padding: 0;
  box-shadow: 0 2px 6px rgba(0,0,0,0.5);
  transition: transform 140ms cubic-bezier(0.2, 1.0, 0.4, 1), background var(--t-fast) var(--ease);
}
.v2-tl-bar-delete:active {
  transform: scale(0.88);
  background: #ef4444;
}

/* =========================================================
   Modal — bottom sheet for inline editors (text content, etc.)
   ========================================================= */
.v2-modal-backdrop {
  position: fixed; inset: 0;
  background: rgba(8, 9, 13, 0.55);
  backdrop-filter: blur(18px) saturate(140%);
  -webkit-backdrop-filter: blur(18px) saturate(140%);
  z-index: var(--z-modal);
  display: flex; align-items: flex-end; justify-content: center;
  animation: v2-fade-in var(--t-fast) var(--ease);
  /* v2.0.336 — Lift the backdrop above the iOS virtual keyboard. The
     --keyboard-h var is set by main.js via a visualViewport listener
     (defaults to 0 when no keyboard is open). Without this, the sheet
     was anchored to the layout viewport and the iOS keyboard covered
     the entire text editor — users typed blind into overlays. */
  padding-bottom: var(--keyboard-h, 0px);
  transition: padding-bottom 120ms var(--ease);
}
@keyframes v2-fade-in { from { opacity: 0; } to { opacity: 1; } }

.v2-modal {
  background: rgba(20, 22, 28, 0.88);
  backdrop-filter: blur(28px) saturate(160%);
  -webkit-backdrop-filter: blur(28px) saturate(160%);
  border: 1px solid rgba(255, 255, 255, 0.06);
  border-bottom: none;
  width: 100%;
  max-width: 720px;
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  padding: var(--s4);
  /* Add home-indicator clearance on iPhone so the Done button isn't
     hidden by the bottom system bar. */
  padding-bottom: calc(var(--s4) + env(safe-area-inset-bottom, 0px));
  box-shadow: 0 -24px 64px rgba(0,0,0,0.55), 0 -1px 0 rgba(255,255,255,0.04) inset;
  position: relative;
}
/* Sheet drag-handle pill (iOS pattern) — telegraphs "this can be
   dismissed by swiping down". Pure visual for now. */
.v2-modal::before {
  content: '';
  position: absolute;
  top: 8px; left: 50%;
  transform: translateX(-50%);
  width: 36px; height: 4px;
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.18);
}
.v2-modal--bottom {
  animation: v2-slide-up var(--t-base) var(--ease-out);
}
@keyframes v2-slide-up {
  from { transform: translateY(20%); opacity: 0; }
  to   { transform: translateY(0);   opacity: 1; }
}
.v2-modal-head {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: var(--s3);
  margin-top: var(--s2);                /* clear the drag-handle pill above */
  cursor: grab;
  touch-action: none;                   /* let JS own the gesture for swipe-down */
  user-select: none;
  -webkit-user-select: none;
}
.v2-modal-head:active { cursor: grabbing; }
.v2-modal-title {
  font: 700 11px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-2);
}
.v2-modal-done {
  height: 30px; padding: 0 14px;
  background: var(--accent); color: var(--on-accent);
  border-radius: var(--r-pill);
  font: 700 12px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.02em;
}
.v2-modal-done:active { transform: scale(0.96); }
.v2-modal-textarea {
  display: block;
  width: 100%;
  min-height: 80px;
  padding: 12px;
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--border-2);
  border-radius: var(--r-sm);
  font: 600 16px/1.4 'Inter', system-ui, sans-serif;
  resize: vertical;
  outline: 0;
  -webkit-user-select: text; user-select: text;
}
.v2-modal-textarea:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px rgba(163, 255, 18, 0.18);
}
.v2-modal-hint {
  margin-top: var(--s2);
  font: 500 11px/1.3 'Inter', system-ui, sans-serif;
  color: var(--text-3);
}

/* ---- Audio editor sheet ---- */
.v2-modal-input {
  display: block;
  width: 100%;
  padding: 12px;
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--border-2);
  border-radius: var(--r-sm);
  font: 700 16px/1.2 'Inter', system-ui, sans-serif;
  outline: 0;
  margin-bottom: var(--s3);
  -webkit-user-select: text; user-select: text;
}
.v2-modal-input:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px rgba(163, 255, 18, 0.18);
}
.v2-modal-row {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: var(--s2);
  margin-bottom: var(--s3);
}
.v2-modal-meta {
  display: flex; flex-direction: column;
  padding: 10px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border-2);
  border-radius: var(--r-sm);
  gap: 4px;
}
.v2-modal-meta-lbl {
  font: 700 9px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--text-3);
}
.v2-modal-meta-val {
  font: 600 14px/1 'JetBrains Mono', ui-monospace, monospace;
  color: var(--text);
}
.v2-modal-actions {
  display: flex; gap: var(--s2); flex-wrap: wrap;
  margin-bottom: var(--s2);
}
.v2-modal-action {
  flex: 1 1 auto;
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  height: 38px; padding: 0 14px;
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--border-2);
  border-radius: var(--r-pill);
  font: 700 12px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.02em;
  transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
  touch-action: manipulation;
}
.v2-modal-action:active { transform: scale(0.97); }
.v2-modal-action.is-on {
  background: rgba(163, 255, 18, 0.12);
  border-color: var(--accent);
  color: var(--accent);
}
.v2-modal-action--danger {
  color: #f97777;
  border-color: rgba(249, 119, 119, 0.35);
}
.v2-modal-action--danger:active {
  background: rgba(249, 119, 119, 0.12);
}

/* ---- Export modal ---- */
/* Centered rather than bottom-sheet — feels more like a "publish" action
   than a settings tray. Glass card on the dim backdrop. v2.0.58. */
.v2-modal--export {
  align-self: center;
  width: min(94vw, 460px);
  max-height: 92dvh;
  overflow-y: auto;
  border-radius: var(--r-lg);
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
  display: flex; flex-direction: column;
  gap: var(--s3);
}
.v2-modal--export::before { display: none; }   /* no drag-handle pill — not a sheet */
.v2-modal-close {
  width: 28px; height: 28px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 50%;
  background: rgba(255,255,255,0.06);
  color: var(--text-2);
  border: 1px solid var(--border-2);
  touch-action: manipulation;
}
.v2-modal-close:active { transform: scale(0.92); }

.v2-export-group {
  display: flex; flex-direction: column; gap: 6px;
}
.v2-export-lbl {
  font: 700 10px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--text-3);
}
.v2-export-chips {
  display: flex; gap: 6px; flex-wrap: wrap;
}

/* v2.0.112 (B-export-presets) — Platform preset chips at the top of the
   export modal. Single tap pre-fills aspect + resolution + fps for the
   target platform. Each chip carries a brand-tinted dot so the strip
   reads at a glance: pink = TikTok, magenta = Reels, red = Shorts/YT,
   purple = Instagram Square. The active chip gets a colored border
   matching its dot via --preset-color CSS var. */
.v2-export-presets {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
  gap: 8px;
}
.v2-export-preset {
  display: flex; flex-direction: column; align-items: flex-start; gap: 4px;
  padding: 10px 12px;
  min-height: 60px;
  background: var(--surface);
  border: 1px solid var(--border-2);
  border-radius: var(--r-md);
  color: var(--text-1);
  text-align: left;
  touch-action: manipulation;
  cursor: pointer;
  transition:
    border-color var(--t-fast) var(--ease),
    background var(--t-fast) var(--ease),
    transform 140ms cubic-bezier(0.2, 1.0, 0.4, 1),
    box-shadow var(--t-fast) var(--ease);
}
.v2-export-preset:active { transform: scale(0.97); }
.v2-export-preset.is-on {
  border-color: var(--preset-color, var(--accent));
  background: color-mix(in srgb, var(--preset-color, var(--accent)) 10%, transparent);
  box-shadow:
    0 0 0 2px color-mix(in srgb, var(--preset-color, var(--accent)) 26%, transparent),
    0 0 12px color-mix(in srgb, var(--preset-color, var(--accent)) 24%, transparent);
}
.v2-export-preset-dot {
  width: 10px; height: 10px;
  border-radius: 50%;
  box-shadow: 0 0 6px currentColor;
}
.v2-export-preset-lbl {
  font: 700 12px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.02em;
}
.v2-export-preset-meta {
  font: 600 10px/1 'JetBrains Mono', ui-monospace, monospace;
  color: var(--text-2);
  letter-spacing: 0.02em;
}
/* v2.0.167 — Hero summary card. Cover thumb + 1-line config summary
   + length/size + Edit toggle. Always visible above the advanced
   disclosure so the user sees at a glance what's about to export,
   without opening anything. Replaces the prior layout where Cover,
   Length/size, and the config sections all stacked separately
   (300+ vertical pixels of chrome). */
.v2-export-hero {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border-2);
  border-radius: var(--r-md);
}
.v2-export-hero-cover {
  flex: 0 0 auto;
  width: 56px; height: 56px;
  border-radius: 10px;
  overflow: hidden;
  background: #0a0b10;
  display: flex; align-items: center; justify-content: center;
  color: var(--text-2);
  border: 1px solid var(--border-2);
  padding: 0;
  cursor: pointer;
  transition: transform 140ms cubic-bezier(0.2,1,0.4,1), border-color var(--t-fast) var(--ease);
}
.v2-export-hero-cover img {
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
}
.v2-export-hero-cover:active { transform: scale(0.95); }
.v2-export-hero-cover:hover  { border-color: var(--accent); }
.v2-export-hero-info {
  flex: 1 1 auto; min-width: 0;
  display: flex; flex-direction: column; gap: 4px;
}
.v2-export-hero-summary {
  font: 700 13px/1.2 'Inter', system-ui, sans-serif;
  color: var(--text-1);
  letter-spacing: 0.01em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.v2-export-hero-meta {
  font: 600 11px/1 'JetBrains Mono', ui-monospace, monospace;
  color: var(--text-2);
  letter-spacing: 0.02em;
}
.v2-export-edit-toggle {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; gap: 4px;
  padding: 8px 12px;
  background: var(--surface);
  color: var(--text-1);
  border: 1px solid var(--border-2);
  border-radius: 999px;
  font: 700 11px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  cursor: pointer;
  touch-action: manipulation;
  transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.v2-export-edit-toggle:active { transform: scale(0.94); }
.v2-export-edit-toggle.is-open {
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  border-color: var(--accent);
  color: var(--accent);
}
.v2-export-advanced {
  display: flex; flex-direction: column; gap: 16px;
  padding: 14px;
  background: var(--surface-2);
  border: 1px solid var(--border-2);
  border-radius: var(--r-md);
  animation: v2-export-advanced-in 240ms cubic-bezier(0.2, 1.0, 0.4, 1);
}
@keyframes v2-export-advanced-in {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: translateY(0); }
}
/* Range slider sits between hero card and Export button — give it a
   subtle label-less container that flows visually with the hero. */
.v2-export-group--range { padding-top: 4px; }

.v2-export-warn {
  margin-top: 4px;
  padding: 10px 12px;
  background: rgba(249, 119, 119, 0.10);
  border: 1px solid rgba(249, 119, 119, 0.32);
  border-radius: var(--r-sm);
  color: #ffb4b4;
  font: 600 11px/1.4 'Inter', system-ui, sans-serif;
  letter-spacing: 0.01em;
  display: flex; align-items: center; gap: 10px;
}
.v2-export-warn-msg { flex: 1 1 auto; min-width: 0; }
.v2-export-warn-action {
  flex: 0 0 auto;
  padding: 7px 12px;
  border-radius: 999px;
  background: rgba(249, 119, 119, 0.18);
  color: #fff;
  border: 1px solid rgba(249, 119, 119, 0.55);
  font: 700 11px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  touch-action: manipulation;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.v2-export-warn-action:active {
  background: rgba(249, 119, 119, 0.30);
  transform: scale(0.96);
}

/* v2.0.119 — Cover frame section in the export modal. Thumb on the
   left, action button + hint stacked on the right. Thumb shows the
   captured frame at 96×128 (9:16 portrait-ish; aspect adapts via
   object-fit when the actual frame is a different ratio). */
.v2-export-cover {
  display: flex; gap: 12px;
  padding: 10px;
  background: var(--surface-2);
  border: 1px solid var(--border-2);
  border-radius: var(--r-sm);
}
.v2-export-cover-thumb {
  flex: 0 0 auto;
  width: 80px;
  height: 100px;
  border-radius: 6px;
  overflow: hidden;
  background: #0a0b10;
  display: flex; align-items: center; justify-content: center;
}
.v2-export-cover-thumb img {
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
}
.v2-export-cover-empty {
  color: var(--text-3);
  font: 600 10px/1.2 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  text-align: center;
  padding: 0 6px;
}
.v2-export-cover-actions {
  flex: 1 1 auto;
  display: flex; flex-direction: column; gap: 8px;
  justify-content: center;
}
.v2-export-cover-btn {
  align-self: flex-start;
  padding: 8px 14px;
  border-radius: 999px;
  background: rgba(163, 255, 18, 0.10);
  color: var(--accent);
  border: 1px solid rgba(163, 255, 18, 0.32);
  font: 700 11px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  touch-action: manipulation;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.v2-export-cover-btn:active { background: rgba(163, 255, 18, 0.22); transform: scale(0.96); }
/* v2.0.129 — Clear-cover pill. Sits next to the primary "Change cover"
   button when a cover is already set. */
.v2-export-cover-clear {
  align-self: flex-start;
  padding: 8px 12px;
  border-radius: 999px;
  background: transparent;
  color: var(--text-3);
  border: 1px solid var(--border-2);
  font: 700 11px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  touch-action: manipulation;
  cursor: pointer;
}
.v2-export-cover-clear:active { background: rgba(255,255,255,0.04); transform: scale(0.96); }

/* v2.0.129 — Cover picker sub-modal. The main preview canvas is
   visible above this bottom-sheet; the sheet hosts the scrubber +
   nudge buttons + "Set as cover" commit. */
.v2-cover-modal { min-height: 240px; }
.v2-cover-body {
  display: flex; flex-direction: column; gap: 14px;
  padding: 6px 18px 22px;
}
.v2-cover-hint {
  font: 500 11px/1.4 'Inter', system-ui, sans-serif;
  color: var(--text-2);
  letter-spacing: 0.01em;
  text-align: center;
}
.v2-cover-scrubber {
  display: flex; align-items: center;
}
.v2-cover-slider { flex: 1 1 auto; }
.v2-cover-time-row {
  display: flex; align-items: center; justify-content: center; gap: 6px;
}
.v2-cover-time {
  flex: 1 1 auto;
  text-align: center;
  font: 700 12px/1 'JetBrains Mono', ui-monospace, monospace;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}
.v2-cover-nudge {
  flex: 0 0 auto;
  padding: 7px 11px;
  border-radius: 999px;
  background: rgba(255,255,255,0.04);
  color: var(--text-2);
  border: 1px solid var(--border-2);
  font: 700 11px/1 'JetBrains Mono', ui-monospace, monospace;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  touch-action: manipulation;
  cursor: pointer;
}
.v2-cover-nudge:active { background: rgba(255,255,255,0.10); transform: scale(0.94); }
.v2-cover-set {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  height: 46px;
  border-radius: 999px;
  background: var(--accent);
  color: var(--on-accent);
  border: 0;
  font: 800 13px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  touch-action: manipulation;
  cursor: pointer;
  transition: transform var(--t-fast) var(--ease);
}
.v2-cover-set:active { transform: scale(0.97); }

/* v2.0.130 — Silence-trim preview sheet. List of per-clip proposals
   with a check-state row + Apply button. Skipped rows show as muted
   text + empty check box. Apply is the same lime CTA the Cover sheet
   uses (consistent commit shape across sub-modals). */
.v2-silence-modal { min-height: 280px; }
.v2-silence-body {
  display: flex; flex-direction: column; gap: 14px;
  padding: 4px 18px 22px;
}
.v2-silence-hint {
  font: 500 11px/1.4 'Inter', system-ui, sans-serif;
  color: var(--text-2);
  letter-spacing: 0.01em;
}
.v2-silence-list {
  display: flex; flex-direction: column; gap: 6px;
  max-height: 38dvh;
  overflow-y: auto;
}
.v2-silence-row {
  display: grid;
  grid-template-columns: 24px 1fr auto auto;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  background: var(--surface);
  border: 1px solid var(--border-2);
  border-radius: var(--r-md);
  color: var(--text-1);
  font: 600 12px/1 'Inter', system-ui, sans-serif;
  touch-action: manipulation;
  text-align: left;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.v2-silence-row:active { transform: scale(0.98); }
.v2-silence-row.is-kept {
  background: rgba(163, 255, 18, 0.08);
  border-color: rgba(163, 255, 18, 0.40);
}
.v2-silence-row.is-skipped {
  opacity: 0.5;
}
.v2-silence-check {
  width: 22px; height: 22px;
  border-radius: 50%;
  background: transparent;
  border: 1.5px solid var(--border-2);
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--accent);
}
.v2-silence-row.is-kept .v2-silence-check {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}
.v2-silence-row-lbl {
  min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.v2-silence-row-meta {
  font: 600 11px/1 'JetBrains Mono', ui-monospace, monospace;
  font-variant-numeric: tabular-nums;
  color: var(--text-2);
  letter-spacing: 0.02em;
}
.v2-silence-new { color: var(--accent); }
.v2-silence-row-save {
  font: 700 11px/1 'JetBrains Mono', ui-monospace, monospace;
  color: #f97777;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
}
.v2-silence-summary {
  display: flex; align-items: center; gap: 10px;
  padding-top: 6px;
  border-top: 1px solid var(--border-2);
}
.v2-silence-summary-lbl {
  flex: 1 1 auto;
  color: var(--accent);
  font: 700 12px/1 'JetBrains Mono', ui-monospace, monospace;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
}
.v2-silence-apply {
  flex: 0 0 auto;
  height: 44px;
  padding: 0 22px;
  border-radius: 999px;
  background: var(--accent);
  color: var(--on-accent);
  border: 0;
  font: 800 13px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  touch-action: manipulation;
  cursor: pointer;
  transition: transform var(--t-fast) var(--ease);
}
.v2-silence-apply:disabled { opacity: 0.4; pointer-events: none; }
.v2-silence-apply:active:not(:disabled) { transform: scale(0.97); }
.v2-export-cover-hint {
  font: 500 11px/1.3 'Inter', system-ui, sans-serif;
  color: var(--text-3);
  letter-spacing: 0.01em;
}

/* Tertiary export action — same shape as secondary, paler tint. */
.v2-export-action--tertiary {
  background: rgba(255, 255, 255, 0.04);
  color: var(--text);
  border: 1px solid var(--border-2);
}

/* v2.0.120 — Smart cleanup row. Single-tap "fix obvious problems"
   actions. Layout: pill button on the left, hint text below. Pale
   surface so it doesn't compete with the primary Export button. */
.v2-export-smart {
  display: flex; flex-direction: column; gap: 4px;
}
.v2-export-smart-btn {
  align-self: flex-start;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 8px 14px;
  border-radius: 999px;
  background: rgba(163, 255, 18, 0.08);
  color: var(--accent);
  border: 1px solid rgba(163, 255, 18, 0.28);
  font: 700 11px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  touch-action: manipulation;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.v2-export-smart-btn:active { background: rgba(163, 255, 18, 0.20); transform: scale(0.96); }
.v2-export-smart-hint {
  font: 500 11px/1.3 'Inter', system-ui, sans-serif;
  color: var(--text-3);
  letter-spacing: 0.01em;
}

/* v2.0.118 — Saved success state on the export done-screen. Reuses the
   captions sheet's pop-in check animation. Visible for ~4 seconds after
   a successful share/download, then the screen reverts to the video
   preview so the user can re-save in a different format if needed. */
.v2-export-saved-state {
  display: flex; flex-direction: column; align-items: center; gap: 14px;
  padding: 24px 16px 28px;
  text-align: center;
}
.v2-export-saved-msg {
  font: 700 14px/1.2 'Inter', system-ui, sans-serif;
  letter-spacing: 0.02em;
  color: var(--text);
}

/* v2.0.115 — disabled chip state for 4K-on-portrait. Keep the chip
   visible (the user should see 4K exists; show why it's not available
   via the title tooltip). */
.v2-chip.is-disabled,
.v2-chip:disabled {
  opacity: 0.34;
  pointer-events: auto;          /* let the title tooltip show on hover */
  cursor: not-allowed;
}

/* v2.0.128 — Range slider with thumbnail filmstrip. Replaces the
   v2.0.125 text-input row. Three layers stacked in a relative
   container:
     1. .v2-range-strip — thumbnail strip across the full width
     2. .v2-range-mask  — dimmed overlays outside the selected band
     3. .v2-range-band  — accent border around the selected band
     4. .v2-range-handle — two draggable handles + time labels
   touch-action:none on the track + handles so pointer events aren't
   eaten by browser scroll heuristics. */
.v2-range {
  display: flex; flex-direction: column; gap: 8px;
}
.v2-range-track {
  position: relative;
  height: 56px;
  border-radius: 8px;
  overflow: hidden;
  background: #0a0b10;
  touch-action: none;
  user-select: none;
}
.v2-range-strip {
  position: absolute; inset: 0;
  display: flex;
  pointer-events: none;
}
.v2-range-strip-clip {
  height: 100%;
  display: flex;
  overflow: hidden;
  border-right: 1px solid rgba(255,255,255,0.04);
}
.v2-range-strip-clip:last-child { border-right: 0; }
.v2-range-strip-clip img {
  height: 100%;
  width: auto;
  object-fit: cover;
  flex: 1 1 0;
  min-width: 0;
}
.v2-range-strip-empty {
  flex: 1 1 0;
  background: repeating-linear-gradient(45deg, #1f2937 0 6px, #14181f 6px 12px);
}
.v2-range-mask {
  position: absolute; top: 0; bottom: 0;
  background: rgba(10, 11, 16, 0.62);
  pointer-events: none;
}
.v2-range-mask--start { left: 0; }
.v2-range-mask--end   { right: 0; }
.v2-range-band {
  position: absolute; top: 0; bottom: 0;
  border-top:    2px solid var(--accent);
  border-bottom: 2px solid var(--accent);
  box-shadow: 0 0 12px rgba(163, 255, 18, 0.18);
  pointer-events: none;
}
.v2-range-handle {
  position: absolute;
  top: -4px; bottom: -4px;
  width: 18px;
  margin-left: -9px;
  touch-action: none;
  cursor: ew-resize;
  z-index: 4;
}
.v2-range-handle-grip {
  position: absolute;
  top: 4px; bottom: 4px;
  left: 6px; right: 6px;
  background: var(--accent);
  border-radius: 4px;
  box-shadow: 0 0 0 2px #0a0b10, 0 0 8px rgba(163, 255, 18, 0.55);
}
.v2-range-handle-grip::before {
  content: '';
  position: absolute;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  width: 2px; height: 18px;
  background: rgba(10, 11, 16, 0.6);
  border-radius: 1px;
  box-shadow: 4px 0 0 rgba(10, 11, 16, 0.6), -4px 0 0 rgba(10, 11, 16, 0.6);
}
.v2-range-handle:active .v2-range-handle-grip { transform: scaleX(1.18); }
.v2-range-handle-label {
  position: absolute;
  bottom: calc(100% + 4px);
  left: 50%; transform: translateX(-50%);
  padding: 3px 6px;
  background: var(--accent);
  color: var(--on-accent);
  border-radius: 4px;
  font: 700 10px/1 'JetBrains Mono', ui-monospace, monospace;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0.95;
}
.v2-range-handle--end .v2-range-handle-label {
  bottom: auto;
  top: calc(100% + 4px);
}

/* Actions row below the slider. */
.v2-range-actions {
  display: flex; align-items: center; gap: 6px;
  flex-wrap: wrap;
}
.v2-range-action {
  flex: 0 0 auto;
  padding: 6px 10px;
  border-radius: 999px;
  background: rgba(163, 255, 18, 0.08);
  color: var(--accent);
  border: 1px solid rgba(163, 255, 18, 0.28);
  font: 700 10px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  touch-action: manipulation;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.v2-range-action:active { background: rgba(163, 255, 18, 0.20); transform: scale(0.96); }
.v2-range-dur {
  flex: 1 1 auto;
  text-align: center;
  color: var(--accent);
  font: 700 12px/1 'JetBrains Mono', ui-monospace, monospace;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
}
.v2-range-reset {
  flex: 0 0 auto;
  padding: 6px 10px;
  border-radius: 999px;
  background: transparent;
  color: var(--text-3);
  border: 1px solid var(--border-2);
  font: 700 10px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  touch-action: manipulation;
  cursor: pointer;
}
.v2-range-reset:active { background: rgba(255,255,255,0.04); transform: scale(0.96); }

/* v2.0.126 — Format selection hint below the format chips. Shows what
   the GIF output specs are so the user knows what they're getting. */
.v2-export-format-hint {
  font: 500 11px/1.4 'Inter', system-ui, sans-serif;
  color: var(--text-3);
  letter-spacing: 0.01em;
  padding: 4px 2px 0;
}

/* v2.0.131 — GIF options block when Format=GIF is selected. Three
   labeled rows (FPS / Size / Loop) — first two are sliders + readout,
   third is a chip pair. */
.v2-export-gifopts {
  display: flex; flex-direction: column; gap: 8px;
  margin-top: 8px;
  padding: 10px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border-2);
  border-radius: var(--r-sm);
}
.v2-export-gifopts-row {
  display: flex; align-items: center; gap: 10px;
}
.v2-export-gifopts-lbl {
  flex: 0 0 36px;
  color: var(--text-3);
  font: 700 10px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.v2-export-gifopts-val {
  flex: 0 0 auto;
  min-width: 46px;
  text-align: right;
  color: var(--accent);
  font: 700 11px/1 'JetBrains Mono', ui-monospace, monospace;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
}
.v2-export-gifopts-chips {
  flex: 1 1 auto;
  display: flex; gap: 6px;
}

/* v2.0.124 — Custom resolution input row. Shows below the chip strip
   when "Custom" is active. Number input + unit + computed dims readout. */
.v2-export-custom-row {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 4px 2px;
  font: 600 11px/1 'Inter', system-ui, sans-serif;
}
.v2-export-custom-lbl {
  color: var(--text-3);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-weight: 700;
  font-size: 10px;
}
.v2-export-custom-input {
  width: 72px;
  padding: 6px 8px;
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--border-2);
  border-radius: 6px;
  font: 700 12px/1 'JetBrains Mono', ui-monospace, monospace;
  font-variant-numeric: tabular-nums;
  text-align: right;
}
.v2-export-custom-input:focus {
  outline: 0;
  border-color: rgba(163, 255, 18, 0.55);
  box-shadow: 0 0 0 2px rgba(163, 255, 18, 0.22);
}
.v2-export-custom-unit {
  color: var(--text-3);
  font-weight: 600;
}
.v2-export-custom-dims {
  margin-left: auto;
  color: var(--accent);
  font: 700 11px/1 'JetBrains Mono', ui-monospace, monospace;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
}

/* v2.0.132 — Quick-pick chips for common long-side resolutions.
   Sit below the custom input row so the user can one-tap to 540 /
   720 / 900 / 1080 / 1440 / 2160 instead of typing. Active chip when
   the input is within 5 of the chip's value. */
.v2-export-custom-picks {
  display: flex; flex-wrap: wrap; gap: 4px;
  padding: 2px 0;
}
.v2-export-custom-pick {
  flex: 0 0 auto;
  padding: 5px 10px;
  border-radius: 999px;
  background: transparent;
  color: var(--text-3);
  border: 1px solid var(--border-2);
  font: 700 10px/1 'JetBrains Mono', ui-monospace, monospace;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  touch-action: manipulation;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.v2-export-custom-pick:active { transform: scale(0.94); }
.v2-export-custom-pick.is-on {
  background: rgba(163, 255, 18, 0.12);
  color: var(--accent);
  border-color: rgba(163, 255, 18, 0.55);
}
/* v2.0.115 — Meta row now grid (label, value | label, value) so we can
   show Length AND Est. size side-by-side without text overflow. */
.v2-export-meta {
  display: grid;
  grid-template-columns: 1fr auto 1fr auto;
  align-items: center;
  gap: 6px 14px;
  font: 600 12px/1.2 'JetBrains Mono', ui-monospace, monospace;
  color: var(--text-2);
  padding: 10px 14px;
  background: var(--surface-2);
  border: 1px solid var(--border-2);
  border-radius: var(--r-sm);
}
.v2-export-meta > div:nth-child(even) {
  color: var(--text);
  text-align: right;
  font-weight: 700;
}
.v2-export-action {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  height: 48px; padding: 0 18px;
  background: var(--accent); color: var(--on-accent);
  border-radius: var(--r-pill);
  font: 800 14px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.02em;
  transition: transform var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
  touch-action: manipulation;
}
.v2-export-action:active { transform: scale(0.97); }
.v2-export-action--cancel {
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--border-2);
}
/* Secondary action — used for the Download fallback alongside the
   primary Save-to-Photos button. Less prominent, accent border, no
   fill. v2.0.62. */
.v2-export-action--secondary {
  background: transparent;
  color: var(--accent);
  border: 1px solid rgba(163, 255, 18, 0.45);
  font-weight: 700;
  height: 42px;
}
.v2-export-action--secondary:active {
  background: rgba(163, 255, 18, 0.06);
}

.v2-export-progress-big {
  display: flex; flex-direction: column; align-items: center; gap: 12px;
  padding: var(--s5) var(--s3) var(--s3);
}
.v2-export-progress-pct {
  font: 900 56px/1 'Inter', system-ui, sans-serif;
  letter-spacing: -0.04em;
  color: var(--text);
  background: linear-gradient(180deg, #ffffff 0%, var(--accent) 100%);
  -webkit-background-clip: text;
          background-clip: text;
  -webkit-text-fill-color: transparent;
}
.v2-export-progress-bar {
  width: 100%;
  height: 6px;
  background: var(--surface-2);
  border-radius: 3px;
  overflow: hidden;
}
.v2-export-progress-fill {
  height: 100%;
  background: linear-gradient(90deg, var(--accent), #d6ff66);
  border-radius: 3px;
  transition: width 80ms linear;
}
.v2-export-progress-meta {
  font: 600 12px/1 'JetBrains Mono', ui-monospace, monospace;
  letter-spacing: 0.06em;
  color: var(--text-3);
  text-transform: uppercase;
}

.v2-export-preview {
  width: 100%;
  max-height: 50dvh;
  background: #000;
  border-radius: var(--r-md);
  display: block;
}
.v2-export-error {
  padding: var(--s3);
  background: rgba(249, 119, 119, 0.08);
  border: 1px solid rgba(249, 119, 119, 0.30);
  border-radius: var(--r-sm);
  color: #ffb4b4;
  font: 600 13px/1.4 'Inter', system-ui, sans-serif;
}

/* ---- Aspect picker grid ----
   v2.0.148 — Cards rebuilt around a phone-silhouette preview. The
   aspect rectangle sits inside a stylized phone outline so the user
   immediately understands "this is the cropped frame inside a phone
   screen." Active card lights lime + glow. */
.v2-aspect-grid {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: var(--s3);
}
.v2-aspect-btn {
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  padding: 16px 10px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  color: var(--text);
  cursor: pointer;
  touch-action: manipulation;
  transition:
    background var(--t-fast) var(--ease),
    border-color var(--t-fast) var(--ease),
    transform var(--t-fast) var(--ease),
    box-shadow var(--t-fast) var(--ease);
}
.v2-aspect-btn.is-active {
  background: rgba(163, 255, 18, 0.10);
  border-color: var(--accent);
  box-shadow: 0 0 0 2px rgba(163, 255, 18, 0.22), 0 0 14px rgba(163, 255, 18, 0.18);
}
.v2-aspect-btn:active { transform: scale(0.97); }

/* Phone silhouette: portrait rounded rect with a notch dot at the top. */
.v2-aspect-phone {
  position: relative;
  width: 56px; height: 92px;
  background: #0a0b10;
  border: 2px solid var(--border-2);
  border-radius: 11px;
  display: flex; align-items: center; justify-content: center;
  transition: border-color var(--t-fast) var(--ease);
}
.v2-aspect-btn.is-active .v2-aspect-phone {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px rgba(163, 255, 18, 0.35);
}
.v2-aspect-phone-notch {
  position: absolute;
  top: 3px; left: 50%;
  transform: translateX(-50%);
  width: 14px; height: 3px;
  background: var(--border-2);
  border-radius: 1.5px;
}

.v2-aspect-preview {
  background: var(--surface-3);
  border-radius: 3px;
  transition: background var(--t-fast) var(--ease);
}
/* Per-aspect sizing inside the 56×92 phone. Portrait 9:16 fills the
   whole phone screen area; 1:1 sits centered; 16:9 sits horizontally
   with letterboxes top/bottom. */
.v2-aspect-preview--9-16 { width: 44px; height: 78px; }
.v2-aspect-preview--1-1  { width: 44px; height: 44px; }
.v2-aspect-preview--16-9 { width: 44px; height: 25px; }

.v2-aspect-btn.is-active .v2-aspect-preview {
  background: linear-gradient(135deg, rgba(163, 255, 18, 0.95) 0%, rgba(163, 255, 18, 0.55) 100%);
  box-shadow: 0 0 8px rgba(163, 255, 18, 0.45);
}

.v2-aspect-lbl {
  font: 800 14px/1 'JetBrains Mono', ui-monospace, monospace;
  letter-spacing: 0.02em;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.v2-aspect-name {
  font: 700 10px/1.2 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-2);
  text-align: center;
}
.v2-aspect-btn.is-active .v2-aspect-name { color: var(--accent); }
.v2-aspect-desc {
  font: 500 9px/1.2 'Inter', system-ui, sans-serif;
  color: var(--text-3);
  letter-spacing: 0.02em;
  text-align: center;
  max-width: 100%;
  white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}

/* ---- Background color grid ----
   v2.0.149 — 4-column grid (was 6) gives each swatch more area to
   read as a real color sample, not a postage-stamp. Active swatch
   gets a checkmark glyph + outer accent glow ring + slight lift. */
.v2-color-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 10px;
}
.v2-color-swatch {
  aspect-ratio: 1 / 1;
  border-radius: 12px;
  border: 2px solid rgba(255,255,255,0.10);
  padding: 0;
  position: relative;
  cursor: pointer;
  touch-action: manipulation;
  transition:
    transform 140ms cubic-bezier(0.2, 1.0, 0.4, 1),
    border-color var(--t-fast) var(--ease),
    box-shadow var(--t-fast) var(--ease);
}
.v2-color-swatch:active { transform: scale(0.92); }
.v2-color-swatch.is-active {
  border-color: var(--accent);
  box-shadow:
    0 0 0 3px rgba(163, 255, 18, 0.30),
    0 0 14px rgba(163, 255, 18, 0.25);
  transform: scale(1.04);
}
/* Checkmark glyph appears on the active swatch. Color flips based on
   the swatch's luminance — for light swatches (white, lime, etc.) we
   show a dark checkmark; for dark swatches a light one. Both via a
   solid-color check with a contrasting drop-shadow halo, which works
   on any background. */
.v2-color-swatch.is-active::after {
  content: '';
  position: absolute;
  inset: 0;
  display: block;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round'><path d='M5 12l5 5L20 7'/></svg>");
  background-size: 22px 22px;
  background-position: center;
  background-repeat: no-repeat;
  filter: drop-shadow(0 1px 2px rgba(0,0,0,0.6));
  pointer-events: none;
}
/* v2.0.139 (Polish L) — Empty-canvas hero. Replaces the gray
   "No clips yet · Add a clip from the timeline" text card with a big
   tap target that opens the media picker directly. Mirrors CapCut's
   onboarding pattern: the user's first interaction is centered on the
   canvas, not buried in the timeline toolbar. */
.v2-canvas-empty {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 14px;
  color: var(--text-3);
  cursor: pointer;
  padding: 24px;
  background:
    radial-gradient(80% 60% at 50% 45%, rgba(163, 255, 18, 0.05) 0%, transparent 70%),
    var(--bg);
}
.v2-canvas-empty-cta {
  width: 96px; height: 96px;
  border-radius: 50%;
  background: var(--accent);
  color: var(--on-accent);
  border: 0;
  display: inline-flex; align-items: center; justify-content: center;
  box-shadow:
    0 8px 26px rgba(163, 255, 18, 0.45),
    0 0 0 6px rgba(163, 255, 18, 0.12),
    0 0 0 12px rgba(163, 255, 18, 0.06);
  cursor: pointer;
  touch-action: manipulation;
  transition: transform 220ms cubic-bezier(0.34, 1.56, 0.64, 1);
  animation: v2-empty-pulse 2400ms ease-in-out infinite;
}
.v2-canvas-empty-cta:active { transform: scale(0.94); }
@keyframes v2-empty-pulse {
  0%, 100% { box-shadow: 0 8px 26px rgba(163, 255, 18, 0.45), 0 0 0 6px rgba(163, 255, 18, 0.12), 0 0 0 12px rgba(163, 255, 18, 0.06); }
  50%      { box-shadow: 0 8px 30px rgba(163, 255, 18, 0.55), 0 0 0 10px rgba(163, 255, 18, 0.16), 0 0 0 20px rgba(163, 255, 18, 0.08); }
}
.v2-canvas-empty-title {
  font: 800 18px/1.2 'Inter', system-ui, sans-serif;
  letter-spacing: -0.01em;
  color: var(--text);
  text-align: center;
  margin-top: 4px;
}
.v2-canvas-empty-sub {
  font: 700 11px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
}
.v2-canvas-empty-tip {
  margin-top: 12px;
  font: 500 11px/1.4 'Inter', system-ui, sans-serif;
  color: var(--text-3);
  max-width: 280px;
  text-align: center;
  letter-spacing: 0.01em;
}

/* ----- v2.0.214 Keyframe strip -----
   Floats above PreviewControls when the selected overlay is keyframed.
   Reads like a mini-timeline: dark rounded rail with diamond markers
   and a playhead cursor. Pin (+) on left, clear (trash) on right. */
.v2-keyframe-strip {
  /* v2.0.236 — Strip is now rendered INSIDE the Motion panel's
     Keyframes tab instead of as a canvas overlay. Static positioning
     so it flows with the panel body; the --in-panel variant below
     handles the relative-position context for the ease picker
     popover. Old canvas-overlay positioning kept commented out at
     the bottom of this rule in case we ever want to re-enable a
     dual-mount (we don't expect to). */
  position: relative;
  box-sizing: border-box;
  overflow: visible;       /* let the ease picker pop out above */
  /* min-width:0 still required so the inner overflow-x:auto rail can
     activate (12 preset chips have min-content ~600px otherwise). */
  min-width: 0;
  display: flex; align-items: center; gap: 8px;
  padding: 7px 10px;
  /* v2.0.221 — Polish: refined glass background with subtle vertical
     gradient (was flat rgba). Saturate filter punches up underlying
     video color for a more premium glass feel — CapCut's overlay
     controls use the same trick. Inset top hairline + deeper shadow
     give the strip clearer separation from canvas content beneath. */
  background:
    linear-gradient(180deg, rgba(34, 38, 50, 0.92), rgba(18, 20, 28, 0.92));
  border: 1px solid rgba(255, 255, 255, 0.10);
  border-radius: 14px;
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  box-shadow:
    0 6px 24px rgba(0, 0, 0, 0.55),
    inset 0 1px 0 rgba(255, 255, 255, 0.06);
  /* Slide-in only when the strip first mounts (per selection change). */
  animation: v2-keyframe-strip-in 260ms cubic-bezier(.2,.7,.2,1);
}
@keyframes v2-keyframe-strip-in {
  from { transform: translateY(8px); opacity: 0; }
  to   { transform: translateY(0);   opacity: 1; }
}
/* v2.0.221 — Polish: 28×28 instead of 26 for finger-friendly touch
   target (mobile guideline = 44pt absolute min; on a typical phone
   28 CSS px ≈ 42pt). Pin button shows a subtle lime ring when
   keyframes already exist (the strip is in diamond mode), reinforcing
   "tap me to add more". Clear button gets a danger-red glow on hover. */
.v2-keyframe-strip-pin,
.v2-keyframe-strip-clear {
  width: 28px; height: 28px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.12);
  color: #f7f8fa;
  flex: 0 0 auto;
  touch-action: manipulation;
  transition:
    transform var(--t-fast) var(--ease),
    background var(--t-fast) var(--ease),
    border-color var(--t-fast) var(--ease),
    box-shadow var(--t-fast) var(--ease);
}
.v2-keyframe-strip-pin {
  /* Subtle lime ring so users associate this button with adding
     keyframes (matches the active state on the bbox diamond chip). */
  border-color: rgba(163, 255, 18, 0.35);
  box-shadow: 0 0 0 1px rgba(163, 255, 18, 0.10);
}
.v2-keyframe-strip-pin:active {
  transform: scale(0.92);
  background: linear-gradient(180deg, #c5ff4a, #a3ff12);
  border-color: rgba(163, 255, 18, 0.85);
  color: #0a0b10;
  box-shadow: 0 0 16px rgba(163, 255, 18, 0.55);
}
.v2-keyframe-strip-clear:active {
  transform: scale(0.92);
  background: linear-gradient(180deg, #ff6b6b, #ef4444);
  border-color: rgba(239, 68, 68, 0.85);
  color: #ffffff;
  box-shadow: 0 0 16px rgba(239, 68, 68, 0.55);
}
.v2-keyframe-strip-rail {
  position: relative;
  flex: 1 1 auto;
  /* v2.0.218 — min-width:0 lets the rail shrink to fit narrow screens
     instead of taking its diamond markers' min-content width. The
     diamonds reposition by left% so they always fit. */
  min-width: 0;
  /* v2.0.221 — Polish: taller rail (26px) gives diamond markers
     vertical breathing room + bigger touch slop. Inset shadow gives
     the rail a "carved into the surface" feel rather than a flat panel. */
  height: 26px;
  border-radius: 13px;
  background: rgba(0, 0, 0, 0.30);
  border: 1px solid rgba(255, 255, 255, 0.06);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.30);
  overflow: visible;
  touch-action: manipulation;
  cursor: pointer;
}
/* Progress fill — visualizes how far through the overlay's duration
   the playhead currently is. Lime to match the rest of the in-flight
   chrome. */
.v2-keyframe-strip-fill {
  position: absolute; top: 0; bottom: 0; left: 0;
  /* v2.0.221 — Polish: deeper gradient with subtle top-edge highlight
     so the fill reads as "filled glass" not a flat color wash. */
  background: linear-gradient(180deg,
    rgba(163, 255, 18, 0.32) 0%,
    rgba(163, 255, 18, 0.16) 60%,
    rgba(163, 255, 18, 0.12) 100%);
  border-radius: 13px 0 0 13px;
  pointer-events: none;
  /* Smooth fill animation while the playhead moves. */
  transition: width 80ms linear;
}
/* Playhead cursor — lime line with a small triangle indicator at the
   top so it reads as a proper playhead, not just a divider. */
.v2-keyframe-strip-cursor {
  position: absolute; top: -3px; bottom: -3px;
  width: 2px;
  background: #a3ff12;
  border-radius: 1px;
  transform: translateX(-1px);
  box-shadow: 0 0 8px rgba(163, 255, 18, 0.75);
  pointer-events: none;
  transition: left 80ms linear;
}
.v2-keyframe-strip-cursor::before {
  /* Tiny downward triangle at the top — reinforces "this is the
     playhead position" reading vs just a vertical separator. */
  content: '';
  position: absolute;
  top: -4px; left: 50%;
  transform: translateX(-50%);
  width: 0; height: 0;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 5px solid #a3ff12;
}
/* Diamond marker — rotated square, lime gradient with dark stroke.
   v2.0.221 polish: larger (18px instead of 14) for finger accuracy,
   gradient fill instead of flat, soft outer glow that intensifies
   when at the playhead. Touch slop is larger than the visual via
   a transparent ::before pseudo-element so taps register reliably. */
.v2-keyframe-mark {
  position: absolute; top: 50%; left: 0;
  width: 16px; height: 16px;
  transform: translate(-50%, -50%) rotate(45deg);
  background: linear-gradient(135deg, #c5ff4a 0%, #a3ff12 60%, #84e000 100%);
  border: 1.5px solid #0a0b10;
  border-radius: 3px;
  padding: 0;
  box-shadow:
    0 1px 3px rgba(0, 0, 0, 0.55),
    0 0 6px rgba(163, 255, 18, 0.35);
  cursor: pointer;
  touch-action: manipulation;
  /* Mount animation — each new diamond scale-pulses in. */
  animation: v2-keyframe-mark-in 220ms cubic-bezier(.34, 1.56, .64, 1);
  transition:
    transform var(--t-fast) var(--ease),
    box-shadow var(--t-fast) var(--ease),
    background var(--t-fast) var(--ease);
}
/* Invisible touch-slop expansion. Pseudo-element sits ON TOP of the
   diamond but extends 8px past on every side, capturing taps that
   miss the 16px diamond. */
.v2-keyframe-mark::before {
  content: '';
  position: absolute;
  inset: -8px;
  background: transparent;
}
@keyframes v2-keyframe-mark-in {
  0%   { transform: translate(-50%, -50%) rotate(45deg) scale(0.30); opacity: 0; }
  60%  { transform: translate(-50%, -50%) rotate(45deg) scale(1.20); opacity: 1; }
  100% { transform: translate(-50%, -50%) rotate(45deg) scale(1.00); opacity: 1; }
}
.v2-keyframe-mark:active {
  transform: translate(-50%, -50%) rotate(45deg) scale(1.25);
  box-shadow:
    0 1px 3px rgba(0, 0, 0, 0.55),
    0 0 18px rgba(163, 255, 18, 0.85);
}
/* v2.0.224 — drag state. The marker scales up + glows brighter
   while being dragged along the rail so the user gets a strong
   "I'm holding this" affordance. We also disable the mount
   animation during drag (would jitter the move). */
.v2-keyframe-mark.is-dragging {
  transform: translate(-50%, -50%) rotate(45deg) scale(1.35);
  box-shadow:
    0 2px 6px rgba(0, 0, 0, 0.60),
    0 0 22px rgba(163, 255, 18, 0.95);
  animation: none;
  z-index: 5;   /* above other markers if they overlap during snap */
}
/* The keyframe AT the playhead reads brighter + larger glow so the
   user gets confident "I'm exactly on a key" feedback during scrub. */
.v2-keyframe-mark.is-here {
  background: linear-gradient(135deg, #ffffff 0%, #f0f0f0 100%);
  box-shadow:
    0 1px 3px rgba(0, 0, 0, 0.55),
    0 0 14px rgba(255, 255, 255, 0.80),
    0 0 22px rgba(163, 255, 18, 0.50);
}
/* v2.0.223 — Per-keyframe ease picker. Floats above the strip when
   a keyframe is at the playhead. 7 mini-buttons in a row, each
   showing an SVG of the ease curve (drawn from the actual easing
   math in keyframes.js — guaranteed to match how the playback
   interpolates). Pointer-events: auto so taps land on the buttons;
   the wrapper passes through clicks so the canvas underneath stays
   tappable when the picker doesn't cover the tap target. */
.v2-keyframe-ease-picker {
  position: absolute;
  left: 50%;
  bottom: calc(100% + 8px);
  transform: translateX(-50%);
  /* v2.0.236 — In-panel variant flips the picker BELOW the strip
     (so it doesn't get clipped by the ToolSheet body's overflow-y
     when the strip is near the top of the panel). The triangle tail
     also flips. See .v2-keyframe-strip--in-panel rules below. */
  display: flex; gap: 4px;
  padding: 6px 8px;
  background: linear-gradient(180deg, rgba(34, 38, 50, 0.96), rgba(18, 20, 28, 0.96));
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 12px;
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  box-shadow:
    0 8px 24px rgba(0, 0, 0, 0.55),
    inset 0 1px 0 rgba(255, 255, 255, 0.06);
  z-index: 16;
  animation: v2-keyframe-ease-in 180ms cubic-bezier(.2, .7, .2, 1);
  pointer-events: auto;
}
@keyframes v2-keyframe-ease-in {
  from { transform: translateX(-50%) translateY(4px); opacity: 0; }
  to   { transform: translateX(-50%) translateY(0);   opacity: 1; }
}
/* Little downward triangle tying the picker to the strip below — same
   convention as a chat-bubble tail, makes the picker feel attached
   to the strip rather than floating arbitrarily. */
.v2-keyframe-ease-picker::after {
  content: '';
  position: absolute;
  top: 100%; left: 50%;
  transform: translateX(-50%);
  width: 0; height: 0;
  border-left: 6px solid transparent;
  border-right: 6px solid transparent;
  border-top: 6px solid rgba(18, 20, 28, 0.96);
  filter: drop-shadow(0 1px 0 rgba(255, 255, 255, 0.04));
}
.v2-keyframe-ease-btn {
  width: 32px; height: 24px;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 3px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.10);
  border-radius: 7px;
  color: rgba(255, 255, 255, 0.65);
  cursor: pointer;
  touch-action: manipulation;
  transition:
    transform var(--t-fast) var(--ease),
    background var(--t-fast) var(--ease),
    border-color var(--t-fast) var(--ease),
    color var(--t-fast) var(--ease);
}
.v2-keyframe-ease-btn:hover {
  background: rgba(255, 255, 255, 0.08);
  border-color: rgba(255, 255, 255, 0.20);
  color: #ffffff;
}
.v2-keyframe-ease-btn:active {
  transform: scale(0.92);
  background: linear-gradient(180deg, #c5ff4a, #a3ff12);
  color: #0a0b10;
  border-color: rgba(163, 255, 18, 0.85);
}
.v2-keyframe-ease-btn.is-on {
  background: linear-gradient(180deg, rgba(163, 255, 18, 0.30), rgba(163, 255, 18, 0.18));
  color: #a3ff12;
  border-color: rgba(163, 255, 18, 0.55);
  box-shadow: 0 0 10px rgba(163, 255, 18, 0.35);
}

/* v2.0.236 — Strip in-panel variants. Ease picker pops BELOW the
   strip (instead of above) so it doesn't get clipped by the panel
   body's scroll edge. Triangle tail flips accordingly. */
.v2-keyframe-strip--in-panel {
  padding-bottom: 7px;
  margin-bottom: 70px;   /* room for ease picker to render below */
}
.v2-keyframe-strip--in-panel .v2-keyframe-ease-picker {
  bottom: auto;
  top: calc(100% + 8px);
}
.v2-keyframe-strip--in-panel .v2-keyframe-ease-picker::after {
  top: auto;
  bottom: 100%;
  border-top: none;
  border-bottom: 6px solid rgba(18, 20, 28, 0.96);
  filter: drop-shadow(0 -1px 0 rgba(255, 255, 255, 0.04));
}
/* v2.0.215 — preset mode of the keyframe strip. When the selected
   overlay has NO keyframes, the strip shows a horizontal scrolling
   row of preset chips ("Ken Burns", "Fade In/Out", "Pulse" etc.)
   instead of the diamond timeline. Same outer container styling so
   the transition between modes feels seamless. */
.v2-keyframe-strip--presets {
  /* v2.0.221 — match strip's main border-radius now that they share
     the same panel look. */
  border-radius: 14px;
  padding: 7px 10px;
}
.v2-keyframe-presets-label {
  display: inline-flex; align-items: center; gap: 5px;
  /* v2.0.221 — slightly bigger + bolder so the call-to-action reads.
     The label IS the discoverability hint for new users; needs to
     pop without being shouty. */
  font: 800 11px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.10em; text-transform: uppercase;
  color: #e7eaf0;
  padding: 0 8px 0 4px;
  flex: 0 0 auto;
  /* Soft pulse on the diamond icon to draw the eye when the strip
     first appears for an un-animated overlay. Subtle — fades out
     so it doesn't become noise after the user has seen it. */
}
.v2-keyframe-presets-label svg {
  color: #a3ff12;
  filter: drop-shadow(0 0 4px rgba(163, 255, 18, 0.55));
  animation: v2-keyframe-label-pulse 2.4s ease-in-out infinite;
}
@keyframes v2-keyframe-label-pulse {
  0%, 100% { transform: scale(1);    opacity: 1;   }
  50%      { transform: scale(1.12); opacity: 0.85; }
}
.v2-keyframe-presets-row {
  flex: 1 1 auto;
  /* v2.0.218 — see .v2-keyframe-strip-rail; same rationale.
     Without min-width:0, the row's flex item refuses to be
     narrower than the sum of its chips (~600px) and pushes the
     parent strip past the viewport. */
  min-width: 0;
  display: flex; align-items: center; gap: 6px;
  overflow-x: auto; overflow-y: hidden;
  scrollbar-width: none;
  -ms-overflow-style: none;
  /* Hide native scrollbar — chips swipe with momentum on mobile. */
  scroll-snap-type: x proximity;
  padding: 2px 4px 4px;
  /* Edge-fade so the row tells the user "there's more to scroll".
     Mask both ends in a thin gradient to transparent. */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 14px, #000 calc(100% - 14px), transparent);
          mask-image: linear-gradient(90deg, transparent, #000 14px, #000 calc(100% - 14px), transparent);
}
.v2-keyframe-presets-row::-webkit-scrollbar { display: none; }
.v2-keyframe-preset-chip {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; gap: 6px;
  /* v2.0.221 — bigger padding for better finger targets + comfier
     visual weight. CapCut's preset chips are visibly chunky for
     thumb-tappability. */
  padding: 8px 13px 8px 11px;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.04));
  color: #f7f8fa;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 999px;
  font: 700 12px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.005em;
  white-space: nowrap;
  scroll-snap-align: start;
  cursor: pointer;
  touch-action: manipulation;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.05);
  transition:
    transform var(--t-fast) var(--ease),
    background var(--t-fast) var(--ease),
    border-color var(--t-fast) var(--ease),
    box-shadow var(--t-fast) var(--ease);
}
.v2-keyframe-preset-chip svg { color: #a3ff12; opacity: 0.95; }
.v2-keyframe-preset-chip:hover {
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.06));
  border-color: rgba(255, 255, 255, 0.22);
  transform: translateY(-1px);
  box-shadow: 0 3px 8px rgba(0, 0, 0, 0.40), inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
.v2-keyframe-preset-chip:active {
  transform: scale(0.94);
  background: linear-gradient(180deg, #c5ff4a, #a3ff12);
  color: #0a0b10;
  border-color: rgba(163, 255, 18, 0.85);
  box-shadow: 0 0 18px rgba(163, 255, 18, 0.55);
}
.v2-keyframe-preset-chip:active svg { color: #0a0b10; opacity: 1; }

/* =========================================================
   v2.0.225 — Live mini-previews on preset chips.

   Each chip replaces the generic icon with a tiny animated glyph
   that mirrors the preset's actual motion. The glyph is a small
   lime square inside a 16×16 viewport (overflow:hidden on the
   wrapper so slide-out motions clip cleanly at the edge instead
   of escaping the chip). Animations are GPU-cheap (transform +
   opacity only) and loop continuously at a leisurely cadence so
   the chip rail doesn't feel hectic.
   ========================================================= */
.v2-preset-preview {
  width: 16px; height: 16px;
  display: inline-flex; align-items: center; justify-content: center;
  position: relative;
  flex: 0 0 auto;
  overflow: hidden;
  border-radius: 3px;
  background: rgba(255, 255, 255, 0.06);
  margin-right: -1px;
}
.v2-preset-preview-glyph {
  width: 9px; height: 9px;
  background: #a3ff12;
  border-radius: 2px;
  box-shadow: 0 0 4px rgba(163, 255, 18, 0.55);
  /* Per-chip animation drives transform/opacity. */
  transform-origin: center;
}
/* Active state — chip becomes lime, glyph swaps to dark so it
   stays readable against the chip's new bg. */
.v2-keyframe-preset-chip:active .v2-preset-preview-glyph {
  background: #0a0b10;
  box-shadow: none;
}

/* ----- Per-preset animations ----- */

/* Ken Burns in: gentle scale-up + return loop. Real preset scales
   from 1.0 to 1.18 over the clip; preview goes 1.0 → 1.18 → 1.0 so
   the user sees both directions of the motion in one cycle. */
.v2-preset-chip--ken-burns-in .v2-preset-preview-glyph {
  animation: v2-pp-kbi 2.4s ease-in-out infinite;
}
@keyframes v2-pp-kbi {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.55); }
}

/* Ken Burns out: same but starts zoomed in. */
.v2-preset-chip--ken-burns-out .v2-preset-preview-glyph {
  animation: v2-pp-kbo 2.4s ease-in-out infinite;
}
@keyframes v2-pp-kbo {
  0%, 100% { transform: scale(1.55); }
  50%      { transform: scale(1); }
}

/* Fade in/out: opacity arc with hold in the middle. */
.v2-preset-chip--fade-in-out .v2-preset-preview-glyph {
  animation: v2-pp-fadeio 2.4s ease-in-out infinite;
}
@keyframes v2-pp-fadeio {
  0%, 100% { opacity: 0; }
  20%, 80% { opacity: 1; }
}

/* Fade in: 0 → 1 then hold. */
.v2-preset-chip--fade-in .v2-preset-preview-glyph {
  animation: v2-pp-fadein 2.4s ease-out infinite;
}
@keyframes v2-pp-fadein {
  0%   { opacity: 0; }
  40%  { opacity: 1; }
  100% { opacity: 1; }
}

/* Slide directions: glyph enters from outside the preview window
   (overflow:hidden clips the entry frame cleanly). */
.v2-preset-chip--slide-in-left .v2-preset-preview-glyph {
  animation: v2-pp-slideL 2.0s cubic-bezier(.2, .8, .2, 1) infinite;
}
@keyframes v2-pp-slideL {
  0%       { transform: translateX(-14px); opacity: 0; }
  30%      { transform: translateX(0);      opacity: 1; }
  100%     { transform: translateX(0);      opacity: 1; }
}
.v2-preset-chip--slide-in-right .v2-preset-preview-glyph {
  animation: v2-pp-slideR 2.0s cubic-bezier(.2, .8, .2, 1) infinite;
}
@keyframes v2-pp-slideR {
  0%       { transform: translateX(14px);  opacity: 0; }
  30%      { transform: translateX(0);      opacity: 1; }
  100%     { transform: translateX(0);      opacity: 1; }
}
.v2-preset-chip--slide-in-up .v2-preset-preview-glyph {
  animation: v2-pp-slideU 2.0s cubic-bezier(.2, .8, .2, 1) infinite;
}
@keyframes v2-pp-slideU {
  0%       { transform: translateY(14px);  opacity: 0; }
  30%      { transform: translateY(0);      opacity: 1; }
  100%     { transform: translateY(0);      opacity: 1; }
}
.v2-preset-chip--slide-in-down .v2-preset-preview-glyph {
  animation: v2-pp-slideD 2.0s cubic-bezier(.2, .8, .2, 1) infinite;
}
@keyframes v2-pp-slideD {
  0%       { transform: translateY(-14px); opacity: 0; }
  30%      { transform: translateY(0);      opacity: 1; }
  100%     { transform: translateY(0);      opacity: 1; }
}

/* Pop in: scale from 0 with overshoot (matches the preset's
   'overshoot' ease). */
.v2-preset-chip--pop-in .v2-preset-preview-glyph {
  animation: v2-pp-popin 2.0s cubic-bezier(.34, 1.56, .64, 1) infinite;
}
@keyframes v2-pp-popin {
  0%       { transform: scale(0);    opacity: 0; }
  60%      { transform: scale(1.20); opacity: 1; }
  80%, 100% { transform: scale(1);   opacity: 1; }
}

/* Pulse: continuous heartbeat scale. Matches the preset's
   per-cycle scale oscillation. */
.v2-preset-chip--pulse .v2-preset-preview-glyph {
  animation: v2-pp-pulse 0.9s ease-in-out infinite;
}
@keyframes v2-pp-pulse {
  0%, 100% { transform: scale(1);    }
  50%      { transform: scale(1.30); }
}

/* Spin in: rotate from -180° to 0° + scale up. */
.v2-preset-chip--spin-in .v2-preset-preview-glyph {
  animation: v2-pp-spin 2.2s ease-out infinite;
}
@keyframes v2-pp-spin {
  0%       { transform: rotate(-180deg) scale(0.5); opacity: 0; }
  50%      { transform: rotate(0deg)    scale(1);   opacity: 1; }
  100%     { transform: rotate(0deg)    scale(1);   opacity: 1; }
}

/* Wiggle: continuous side-to-side rotation. Matches the preset's
   ±6° shake (slightly amplified here so it reads in 16px). */
.v2-preset-chip--wiggle .v2-preset-preview-glyph {
  animation: v2-pp-wiggle 0.7s ease-in-out infinite;
}
@keyframes v2-pp-wiggle {
  0%, 100% { transform: rotate(-18deg); }
  50%      { transform: rotate(18deg);  }
}

/* =========================================================
   v2.0.226 — Live "Aa" mini-previews on the Chars tab chips.

   Each chip in TextStylePanel's character-animation tab shows a
   tiny rendering of the per-char effect: two letters "A" and "a"
   with a per-char animation delay so the stagger is visible. Same
   GPU-cheap transform+opacity approach as the preset previews.

   The "None" chip shows static Aa (no animation) so the visual
   rhythm of the chip row stays consistent.
   ========================================================= */
.v2-char-preview {
  display: inline-flex;
  font: 800 13px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.06em;
  margin-right: 5px;
  color: inherit;
  /* Reserve vertical room so the wave animation doesn't change
     chip height as letters bob up and down. */
  min-height: 16px; align-items: center;
}
.v2-char-preview > span {
  display: inline-block;
  transform-origin: center;
  will-change: transform, opacity;
}

/* Type-on: hard step from 0 → 1 in sequence (typewriter look). */
.v2-char-chip--type-on .v2-char-preview > span:nth-child(1) { animation: v2-cp-type 2.4s steps(1) infinite; animation-delay: 0s; }
.v2-char-chip--type-on .v2-char-preview > span:nth-child(2) { animation: v2-cp-type 2.4s steps(1) infinite; animation-delay: 0.20s; }
@keyframes v2-cp-type {
  0%, 5%    { opacity: 0; }
  10%, 88%  { opacity: 1; }
  95%, 100% { opacity: 0; }
}

/* Fade-each: smooth per-char fade-in. */
.v2-char-chip--fade-each .v2-char-preview > span:nth-child(1) { animation: v2-cp-fade 2.4s ease-out infinite; animation-delay: 0s; }
.v2-char-chip--fade-each .v2-char-preview > span:nth-child(2) { animation: v2-cp-fade 2.4s ease-out infinite; animation-delay: 0.15s; }
@keyframes v2-cp-fade {
  0%, 4%   { opacity: 0; }
  22%, 88% { opacity: 1; }
  100%     { opacity: 0; }
}

/* Pop-each: scale-from-tiny with subtle overshoot. */
.v2-char-chip--pop-each .v2-char-preview > span:nth-child(1) { animation: v2-cp-pop 2.4s cubic-bezier(.34,1.56,.64,1) infinite; animation-delay: 0s; }
.v2-char-chip--pop-each .v2-char-preview > span:nth-child(2) { animation: v2-cp-pop 2.4s cubic-bezier(.34,1.56,.64,1) infinite; animation-delay: 0.15s; }
@keyframes v2-cp-pop {
  0%, 4%   { opacity: 0; transform: scale(0.30); }
  22%      { opacity: 1; transform: scale(1.20); }
  35%, 88% { opacity: 1; transform: scale(1);    }
  100%     { opacity: 0; transform: scale(0.30); }
}

/* Cascade-in: slide in from the left. */
.v2-char-chip--cascade-in .v2-char-preview > span:nth-child(1) { animation: v2-cp-cascade 2.4s cubic-bezier(.2,.8,.2,1) infinite; animation-delay: 0s; }
.v2-char-chip--cascade-in .v2-char-preview > span:nth-child(2) { animation: v2-cp-cascade 2.4s cubic-bezier(.2,.8,.2,1) infinite; animation-delay: 0.15s; }
@keyframes v2-cp-cascade {
  0%, 4%   { opacity: 0; transform: translateX(-6px); }
  22%, 88% { opacity: 1; transform: translateX(0);    }
  100%     { opacity: 0; transform: translateX(-6px); }
}

/* Reveal-up: slide up into place. */
.v2-char-chip--reveal-up .v2-char-preview > span:nth-child(1) { animation: v2-cp-reveal 2.4s cubic-bezier(.2,.8,.2,1) infinite; animation-delay: 0s; }
.v2-char-chip--reveal-up .v2-char-preview > span:nth-child(2) { animation: v2-cp-reveal 2.4s cubic-bezier(.2,.8,.2,1) infinite; animation-delay: 0.15s; }
@keyframes v2-cp-reveal {
  0%, 4%   { opacity: 0; transform: translateY(6px); }
  22%, 88% { opacity: 1; transform: translateY(0);   }
  100%     { opacity: 0; transform: translateY(6px); }
}

/* Wave: continuous vertical oscillation, phase-offset per char so
   the two letters wobble out of sync (the signature wave look). */
.v2-char-chip--wave .v2-char-preview > span:nth-child(1) { animation: v2-cp-wave 1.2s ease-in-out infinite; animation-delay: 0s;    }
.v2-char-chip--wave .v2-char-preview > span:nth-child(2) { animation: v2-cp-wave 1.2s ease-in-out infinite; animation-delay: 0.15s; }
@keyframes v2-cp-wave {
  0%, 100% { transform: translateY(0);    }
  50%      { transform: translateY(-3px); }
}

/* =========================================================
   v2.0.227 — Live blend-mode preview swatches.

   Each blend chip in TextStylePanel / StickerStylePanel shows a
   tiny swatch demonstrating the mode in action. The swatch's
   base is a colorful gradient (simulating underlying video); the
   ::after layer is a translucent blue with the chip's mix-blend-mode
   applied — so the swatch IS the blend in action, not a metaphor.

   Uses CSS mix-blend-mode rather than canvas globalCompositeOperation
   for cheapness (no canvas paint per chip). The two are visually
   identical for these 9 modes — CSS and canvas implement the W3C
   compositing spec the same way.
   ========================================================= */
.v2-blend-preview {
  width: 18px; height: 18px;
  position: relative;
  border-radius: 4px;
  overflow: hidden;
  flex: 0 0 auto;
  margin-right: 5px;
  /* Base "video" — three-color gradient with warm + cool stops so
     the blend modes visibly do different things. Bordered so the
     swatch reads cleanly against the chip's own bg. */
  background: linear-gradient(135deg, #ff6b6b 0%, #4ecdc4 50%, #ffe66d 100%);
  border: 1px solid rgba(255, 255, 255, 0.15);
}
.v2-blend-preview::after {
  content: '';
  position: absolute; inset: 0;
  /* "Overlay sticker" — translucent blue with full alpha to keep
     each blend mode's effect strong + readable. */
  background: rgba(60, 110, 255, 0.85);
  /* mix-blend-mode overridden per-chip below. */
  mix-blend-mode: normal;
}

.v2-blend-chip--source-over .v2-blend-preview::after { mix-blend-mode: normal; }
.v2-blend-chip--multiply    .v2-blend-preview::after { mix-blend-mode: multiply; }
.v2-blend-chip--screen      .v2-blend-preview::after { mix-blend-mode: screen; }
.v2-blend-chip--overlay     .v2-blend-preview::after { mix-blend-mode: overlay; }
.v2-blend-chip--soft-light  .v2-blend-preview::after { mix-blend-mode: soft-light; }
.v2-blend-chip--hard-light  .v2-blend-preview::after { mix-blend-mode: hard-light; }
.v2-blend-chip--lighten     .v2-blend-preview::after { mix-blend-mode: lighten; }
.v2-blend-chip--darken      .v2-blend-preview::after { mix-blend-mode: darken; }
.v2-blend-chip--difference  .v2-blend-preview::after { mix-blend-mode: difference; }

/* =========================================================
   v2.0.229 — Motion panel.

   Unified animation surface that replaces the legacy Anim/Chars/
   KeyframeStrip-presets fragmentation. Lives in the SecondaryToolbar
   as the 'motion' tool. Layout:
     1. Category tabs at top (Recent / Entrance / Exit / Loop / Special)
        — handled by the existing ToolSheet tab strip
     2. Effect chip grid: each chip is a 76×70 card with a 36×32 live
        preview area + a small label
     3. Applied section: list of currently-applied effects with name +
        time range + ✕ remove button
   ========================================================= */
/* v2.0.239 — Apply-to-all toggle banner. Sits at the top of the
   Motion panel body. Default state: muted (off, hinting "tap me to
   batch your changes"). On state: lime fill + check icon + bold
   target count. Banner-width hit area = comfy tap target. */
.v2-motion-apply-all {
  display: flex; align-items: center; gap: 10px;
  width: 100%;
  padding: 9px 12px;
  margin-bottom: 8px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 10px;
  color: #cdd2dc;
  font: 600 12px/1.3 'Inter', system-ui, sans-serif;
  text-align: left;
  cursor: pointer;
  touch-action: manipulation;
  transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.v2-motion-apply-all:hover {
  background: rgba(255, 255, 255, 0.07);
  border-color: rgba(255, 255, 255, 0.16);
  color: #ffffff;
}
.v2-motion-apply-all.is-on {
  background: linear-gradient(180deg, rgba(163, 255, 18, 0.22), rgba(163, 255, 18, 0.12));
  border-color: rgba(163, 255, 18, 0.55);
  color: #e7eaf0;
  box-shadow: 0 0 12px rgba(163, 255, 18, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.v2-motion-apply-all-check {
  width: 18px; height: 18px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1.5px solid rgba(255, 255, 255, 0.25);
  border-radius: 5px;
  background: rgba(0, 0, 0, 0.25);
  color: #0a0b10;
  flex: 0 0 auto;
  transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
}
.v2-motion-apply-all.is-on .v2-motion-apply-all-check {
  background: linear-gradient(180deg, #c5ff4a, #a3ff12);
  border-color: rgba(163, 255, 18, 0.85);
}
.v2-motion-apply-all-text strong {
  color: #a3ff12;
  font-weight: 800;
}
.v2-motion-apply-all.is-on .v2-motion-apply-all-text strong {
  color: #ffffff;
}

/* =========================================================
   v2.0.249 — Detected-BPM chip in Motion → Beat tab.
   Shows the auto-analyzer's result for the project's audio with
   confidence-tinted colors. Magenta accent (matches Beat category).
   Tap "Use" applies to all existing tempo-* effects on the
   selected overlay.
   ========================================================= */
.v2-bpm-detected-chip {
  display: flex; align-items: center; gap: 10px;
  width: 100%;
  padding: 9px 12px;
  margin-bottom: 10px;
  background: linear-gradient(180deg, rgba(255, 43, 214, 0.22), rgba(255, 43, 214, 0.10));
  border: 1px solid rgba(255, 43, 214, 0.55);
  border-radius: 12px;
  color: #ffffff;
  font: 600 12px/1.3 'Inter', system-ui, sans-serif;
  cursor: pointer;
  text-align: left;
  touch-action: manipulation;
  box-shadow: 0 0 14px rgba(255, 43, 214, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.06);
  transition: background var(--t-fast) var(--ease), transform var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
  /* Subtle pulse animation on first appearance to invite the eye. */
  animation: v2-bpm-chip-in 280ms cubic-bezier(.2, .7, .2, 1);
}
@keyframes v2-bpm-chip-in {
  from { transform: translateY(-4px); opacity: 0; box-shadow: 0 0 0 rgba(255, 43, 214, 0); }
  to   { transform: translateY(0);    opacity: 1; box-shadow: 0 0 14px rgba(255, 43, 214, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.06); }
}
.v2-bpm-detected-chip:hover {
  background: linear-gradient(180deg, rgba(255, 43, 214, 0.30), rgba(255, 43, 214, 0.15));
  border-color: rgba(255, 43, 214, 0.85);
  box-shadow: 0 0 20px rgba(255, 43, 214, 0.40), inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
.v2-bpm-detected-chip:active {
  transform: scale(0.97);
  background: linear-gradient(180deg, #ff5be6, #ff2bd6);
  color: #0a0b10;
}
.v2-bpm-chip-icon {
  display: inline-flex; align-items: center; justify-content: center;
  flex: 0 0 auto;
  color: #ff2bd6;
  /* Subtle pulse on the music note — draws the eye gently. */
  animation: v2-bpm-icon-pulse 1.8s ease-in-out infinite;
}
@keyframes v2-bpm-icon-pulse {
  0%, 100% { transform: scale(1);    opacity: 1;   }
  50%      { transform: scale(1.18); opacity: 0.80; }
}
.v2-bpm-chip-text {
  flex: 1 1 auto;
  font: 600 12px/1.3 'Inter', system-ui, sans-serif;
}
.v2-bpm-chip-text strong {
  color: #ffffff;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}
.v2-bpm-chip-conf {
  margin-left: 4px;
  color: rgba(255, 255, 255, 0.60);
  font-variant-numeric: tabular-nums;
}
.v2-bpm-chip-use {
  flex: 0 0 auto;
  display: inline-flex; align-items: center;
  padding: 5px 12px;
  background: rgba(255, 255, 255, 0.15);
  border: 1px solid rgba(255, 255, 255, 0.30);
  color: #ffffff;
  font: 800 11px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  border-radius: 999px;
}
/* Confidence color tints — high (lime), mid (default magenta),
   low (amber warning). Border gets bolder for high; muted for low. */
.v2-bpm-detected-chip.is-high {
  border-color: rgba(163, 255, 18, 0.60);
  box-shadow: 0 0 14px rgba(163, 255, 18, 0.30), inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.v2-bpm-detected-chip.is-high .v2-bpm-chip-icon { color: #a3ff12; }
.v2-bpm-detected-chip.is-low {
  border-color: rgba(255, 178, 41, 0.50);
  box-shadow: 0 0 14px rgba(255, 178, 41, 0.20), inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.v2-bpm-detected-chip.is-low .v2-bpm-chip-icon { color: #ffb229; animation: none; }
.v2-bpm-detected-chip.is-low .v2-bpm-chip-conf::after {
  content: ' · low confidence';
  color: #ffb229;
}

/* Analyzing variant — muted, with a tiny spinning dot to indicate
   work in progress. Non-interactive. */
.v2-bpm-detected-chip--analyzing {
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.03));
  border-color: rgba(255, 255, 255, 0.14);
  color: #cdd2dc;
  cursor: default;
  animation: none;
}
.v2-bpm-detected-chip--analyzing:hover {
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.03));
  border-color: rgba(255, 255, 255, 0.14);
  box-shadow: none;
}
.v2-bpm-chip-spinner {
  width: 14px; height: 14px;
  flex: 0 0 auto;
  border: 2px solid rgba(255, 255, 255, 0.15);
  border-top-color: #ff2bd6;
  border-radius: 50%;
  animation: v2-bpm-spinner 800ms linear infinite;
}
@keyframes v2-bpm-spinner {
  to { transform: rotate(360deg); }
}

.v2-tool-motion .v2-motion-chip-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 4px 0 8px;
}
.v2-motion-chip {
  flex: 0 0 auto;
  width: 76px;
  display: flex; flex-direction: column; align-items: center;
  padding: 6px 4px 7px;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.03));
  border: 1px solid rgba(255, 255, 255, 0.10);
  border-radius: 10px;
  gap: 5px;
  color: #f7f8fa;
  cursor: pointer;
  touch-action: manipulation;
  transition:
    transform var(--t-fast) var(--ease),
    background var(--t-fast) var(--ease),
    border-color var(--t-fast) var(--ease),
    box-shadow var(--t-fast) var(--ease);
}
.v2-motion-chip:hover {
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.10), rgba(255, 255, 255, 0.05));
  border-color: rgba(255, 255, 255, 0.22);
  transform: translateY(-1px);
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.35);
}
.v2-motion-chip:active {
  transform: scale(0.95);
  background: linear-gradient(180deg, rgba(163, 255, 18, 0.20), rgba(163, 255, 18, 0.10));
  border-color: rgba(163, 255, 18, 0.55);
  box-shadow: 0 0 12px rgba(163, 255, 18, 0.35);
}
/* Preview area — small dark window with rounded corners. Each
   per-effect class targets its inner .v2-motion-chip-glyph with a
   CSS animation that mirrors the effect's signature motion (same
   approach as the v225 keyframe-preset chip previews). */
.v2-motion-chip-preview {
  width: 56px; height: 32px;
  background: linear-gradient(135deg, #1a1d2e 0%, #0a0b10 100%);
  border-radius: 6px;
  position: relative;
  overflow: hidden;
  flex: 0 0 auto;
  border: 1px solid rgba(255, 255, 255, 0.04);
}
.v2-motion-chip-glyph {
  position: absolute;
  top: 50%; left: 50%;
  width: 12px; height: 12px;
  margin: -6px 0 0 -6px;
  background: #a3ff12;
  border-radius: 2px;
  box-shadow: 0 0 5px rgba(163, 255, 18, 0.6);
  transform-origin: center;
  will-change: transform, opacity;
}
.v2-motion-chip-label {
  font: 700 10px/1.1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.005em;
  color: #e7eaf0;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}

/* Applied effects section — sits below the chip grid. */
.v2-motion-applied-section {
  margin-top: 4px;
  padding: 10px 0 6px;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.v2-motion-applied-head {
  display: flex; align-items: center;
  margin-bottom: 8px;
  padding: 0 4px;
}
.v2-motion-applied-title {
  font: 700 10px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  color: #cdd2dc;
  flex: 1 1 auto;
}
/* v2.0.232 — Save preset button. Inline next to the section title;
   subtle by default so it doesn't compete with the section content,
   pops on hover. */
.v2-motion-save-btn {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 5px 10px 5px 8px;
  background: rgba(163, 255, 18, 0.12);
  border: 1px solid rgba(163, 255, 18, 0.30);
  border-radius: 999px;
  color: #a3ff12;
  font: 700 11px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.02em;
  cursor: pointer;
  touch-action: manipulation;
  transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.v2-motion-save-btn:hover {
  background: rgba(163, 255, 18, 0.20);
  border-color: rgba(163, 255, 18, 0.55);
}
.v2-motion-save-btn:active {
  transform: scale(0.94);
  background: linear-gradient(180deg, #c5ff4a, #a3ff12);
  color: #0a0b10;
  border-color: rgba(163, 255, 18, 0.85);
  box-shadow: 0 0 12px rgba(163, 255, 18, 0.55);
}
.v2-motion-applied-empty,
.v2-motion-empty {
  font: 500 12px/1.4 'Inter', system-ui, sans-serif;
  color: #9ca3af;
  padding: 12px 8px;
  text-align: center;
  display: flex; flex-direction: column; align-items: center; gap: 8px;
}
.v2-motion-empty svg { color: #a3ff12; opacity: 0.7; }
.v2-motion-applied-list {
  display: flex; flex-direction: column; gap: 6px;
}
.v2-motion-applied-row {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 10px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 10px;
}
.v2-motion-applied-dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  flex: 0 0 auto;
}
.v2-motion-applied-dot--entrance { background: #a3ff12; box-shadow: 0 0 6px rgba(163, 255, 18, 0.55); }
.v2-motion-applied-dot--exit     { background: #ff6b6b; box-shadow: 0 0 6px rgba(239, 68, 68, 0.55); }
.v2-motion-applied-dot--loop     { background: #60a5fa; box-shadow: 0 0 6px rgba(96, 165, 250, 0.55); }
.v2-motion-applied-dot--special  { background: #ffe66d; box-shadow: 0 0 6px rgba(255, 230, 109, 0.55); }
/* v2.0.235 — Beat category gets a pink/magenta dot to read
   distinctly from the lime (entrance) / red (exit) / blue (loop) /
   yellow (special) palette. */
.v2-motion-applied-dot--beat     { background: #ff2bd6; box-shadow: 0 0 6px rgba(255, 43, 214, 0.55); }
/* v2.0.240 — Path category gets purple to fill out the 6-category
   color palette (lime / red / blue / yellow / pink / purple). */
.v2-motion-applied-dot--path     { background: #a78bfa; box-shadow: 0 0 6px rgba(167, 139, 250, 0.55); }
.v2-motion-applied-dot--other    { background: #cdd2dc; }
.v2-motion-applied-name {
  flex: 1 1 auto;
  font: 700 12px/1.2 'Inter', system-ui, sans-serif;
  color: #f7f8fa;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* v2.0.233 — Toggle button wraps the dot + name + chevron so the
   whole leading area is one comfy tap target for expand/collapse. */
.v2-motion-applied-toggle {
  display: inline-flex; align-items: center; gap: 8px;
  background: transparent;
  border: none;
  padding: 0 4px 0 0;
  cursor: pointer;
  color: inherit;
  touch-action: manipulation;
  min-width: 0;       /* allow name to truncate */
  max-width: 130px;   /* leave room for track + remove */
}
.v2-motion-applied-toggle:hover .v2-motion-applied-name { color: #ffffff; }
.v2-motion-applied-chev {
  display: inline-flex; align-items: center; justify-content: center;
  color: #9ca3af;
  transition: transform var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.v2-motion-applied-chev.is-expanded {
  transform: rotate(180deg);
  color: #a3ff12;
}
.v2-motion-applied-row.is-expanded {
  border-color: rgba(163, 255, 18, 0.30);
  box-shadow: 0 0 0 1px rgba(163, 255, 18, 0.20);
}
/* Expanded detail panel — sits below its row visually, indented
   slightly so the parent-child relationship reads clearly. */
.v2-motion-applied-detail {
  margin: -2px 0 6px 16px;
  padding: 8px 10px;
  background: rgba(0, 0, 0, 0.30);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-top: none;
  border-radius: 0 0 10px 10px;
  display: flex; flex-direction: column; gap: 8px;
  animation: v2-motion-detail-in 180ms cubic-bezier(.2, .7, .2, 1);
}
@keyframes v2-motion-detail-in {
  from { opacity: 0; transform: translateY(-4px); max-height: 0; }
  to   { opacity: 1; transform: translateY(0);   max-height: 200px; }
}
.v2-motion-detail-row {
  display: flex; align-items: center; gap: 10px;
}
.v2-motion-detail-row--time {
  font-variant-numeric: tabular-nums;
}
.v2-motion-detail-label {
  flex: 0 0 auto;
  width: 56px;
  font: 700 10px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #9ca3af;
}
.v2-motion-detail-value {
  flex: 1 1 auto;
  font: 600 12px/1.2 'Inter', system-ui, sans-serif;
  color: #f7f8fa;
}
.v2-motion-detail-value--mono {
  flex: 0 0 auto;
  width: 56px;
  text-align: right;
  font: 600 11px/1 'JetBrains Mono', ui-monospace, monospace;
  font-variant-numeric: tabular-nums;
  color: #cdd2dc;
}
.v2-motion-detail-sub {
  font: 500 10px/1 'Inter', system-ui, sans-serif;
  color: #6b7280;
  margin-left: 6px;
}
.v2-motion-detail-slider-wrap {
  flex: 1 1 auto;
  display: flex; align-items: center; gap: 10px;
}
.v2-motion-detail-slider {
  flex: 1 1 auto;
  margin: 0;
}
.v2-motion-detail-actions {
  flex: 1 1 auto;
  display: inline-flex; align-items: center; gap: 4px;
}
.v2-motion-detail-btn {
  width: 28px; height: 28px;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.10);
  color: #f7f8fa;
  border-radius: 7px;
  cursor: pointer;
  touch-action: manipulation;
  transition: background var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.v2-motion-detail-btn:hover    { background: rgba(255, 255, 255, 0.12); }
.v2-motion-detail-btn:active   { transform: scale(0.92); background: linear-gradient(180deg, #c5ff4a, #a3ff12); color: #0a0b10; }
.v2-motion-detail-btn.is-disabled { opacity: 0.35; cursor: not-allowed; }
.v2-motion-detail-btn.is-disabled:hover  { background: rgba(255, 255, 255, 0.06); }
.v2-motion-detail-btn.is-disabled:active { transform: none; background: rgba(255, 255, 255, 0.06); color: #f7f8fa; }

/* v2.0.237 — Tap-tempo button. Lives next to the BPM slider readout.
   Each tap fires a 280ms lime-flash animation via the is-pulsing
   class (re-added per tap so the animation runs from frame 0 every
   time). Big enough touch target for finger tapping (32×28 with
   padding for thumb comfort). */
.v2-motion-tap-tempo-btn {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; gap: 4px;
  padding: 5px 9px 5px 7px;
  background: rgba(255, 43, 214, 0.18);
  border: 1px solid rgba(255, 43, 214, 0.45);
  color: #ff2bd6;
  border-radius: 999px;
  font: 700 11px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.02em;
  cursor: pointer;
  touch-action: manipulation;
  user-select: none;
  -webkit-user-select: none;
  transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.v2-motion-tap-tempo-btn:hover {
  background: rgba(255, 43, 214, 0.30);
  border-color: rgba(255, 43, 214, 0.75);
}
.v2-motion-tap-tempo-btn:active {
  transform: scale(0.92);
  background: linear-gradient(180deg, #ff5be6, #ff2bd6);
  color: #0a0b10;
  border-color: rgba(255, 43, 214, 0.95);
}
/* is-pulsing class added imperatively per tap to play the flash
   animation. CSS animation guaranteed to restart by class-remove
   then class-add with a reflow in between (handled in JS). */
.v2-motion-tap-tempo-btn.is-pulsing {
  animation: v2-tap-flash 280ms cubic-bezier(.2, .7, .2, 1);
}
@keyframes v2-tap-flash {
  0%   { background: rgba(255, 43, 214, 0.18); transform: scale(1); box-shadow: 0 0 0 rgba(255, 43, 214, 0); }
  20%  { background: linear-gradient(180deg, #ff5be6, #ff2bd6); transform: scale(1.10); box-shadow: 0 0 18px rgba(255, 43, 214, 0.85); color: #0a0b10; }
  100% { background: rgba(255, 43, 214, 0.18); transform: scale(1); box-shadow: 0 0 0 rgba(255, 43, 214, 0); }
}
.v2-motion-applied-time {
  font: 600 11px/1 'JetBrains Mono', ui-monospace, monospace;
  color: #9ca3af;
  flex: 0 0 auto;
  font-variant-numeric: tabular-nums;
}
/* v2.0.230 — Track bar visualization for an applied effect.
   The track is the row's "timeline" of the overlay duration; the
   colored bar inside represents the effect's window (start_pct +
   duration_pct). Drag the bar body to retime; drag the right-edge
   handle to lengthen/shorten. The lime cursor sweeps across as
   playback progresses. */
.v2-motion-track {
  position: relative;
  flex: 1 1 auto;
  min-width: 60px;
  height: 22px;
  border-radius: 11px;
  background: rgba(0, 0, 0, 0.32);
  border: 1px solid rgba(255, 255, 255, 0.06);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.35);
  overflow: visible;
  touch-action: none;
}
.v2-motion-track-cursor {
  position: absolute;
  top: -3px; bottom: -3px;
  width: 2px;
  background: #ffffff;
  border-radius: 1px;
  transform: translateX(-1px);
  box-shadow: 0 0 6px rgba(255, 255, 255, 0.6);
  pointer-events: none;
  z-index: 2;
}
.v2-motion-track-bar {
  position: absolute;
  top: 2px; bottom: 2px;
  border-radius: 9px;
  cursor: grab;
  display: flex; align-items: stretch;
  justify-content: flex-end;
  touch-action: none;
  transition: filter var(--t-fast) var(--ease);
}
.v2-motion-track-bar:active { cursor: grabbing; filter: brightness(1.15); }
/* Category-color fills, matching the dot pellets in the row label. */
.v2-motion-track-bar--entrance {
  background: linear-gradient(180deg, rgba(163, 255, 18, 0.65), rgba(163, 255, 18, 0.40));
  border: 1px solid rgba(163, 255, 18, 0.65);
  box-shadow: 0 1px 4px rgba(163, 255, 18, 0.25);
}
.v2-motion-track-bar--exit {
  background: linear-gradient(180deg, rgba(239, 68, 68, 0.65), rgba(239, 68, 68, 0.40));
  border: 1px solid rgba(239, 68, 68, 0.65);
  box-shadow: 0 1px 4px rgba(239, 68, 68, 0.25);
}
.v2-motion-track-bar--loop {
  background: linear-gradient(180deg, rgba(96, 165, 250, 0.65), rgba(96, 165, 250, 0.40));
  border: 1px solid rgba(96, 165, 250, 0.65);
  box-shadow: 0 1px 4px rgba(96, 165, 250, 0.25);
}
.v2-motion-track-bar--special {
  background: linear-gradient(180deg, rgba(255, 230, 109, 0.65), rgba(255, 230, 109, 0.40));
  border: 1px solid rgba(255, 230, 109, 0.65);
  box-shadow: 0 1px 4px rgba(255, 230, 109, 0.25);
}
.v2-motion-track-bar--beat {
  background: linear-gradient(180deg, rgba(255, 43, 214, 0.65), rgba(255, 43, 214, 0.40));
  border: 1px solid rgba(255, 43, 214, 0.65);
  box-shadow: 0 1px 4px rgba(255, 43, 214, 0.25);
}
.v2-motion-track-bar--path {
  background: linear-gradient(180deg, rgba(167, 139, 250, 0.65), rgba(167, 139, 250, 0.40));
  border: 1px solid rgba(167, 139, 250, 0.65);
  box-shadow: 0 1px 4px rgba(167, 139, 250, 0.25);
}
.v2-motion-track-bar--other {
  background: rgba(205, 210, 220, 0.45);
  border: 1px solid rgba(205, 210, 220, 0.55);
}
/* Right-edge resize handle. Wider invisible touch area via padding;
   the visible gripper line is just 4px wide. */
.v2-motion-track-handle--right {
  position: relative;
  width: 10px;
  flex: 0 0 auto;
  cursor: ew-resize;
  touch-action: none;
}
.v2-motion-track-handle--right::after {
  content: '';
  position: absolute;
  right: 2px;
  top: 25%;
  bottom: 25%;
  width: 2px;
  background: rgba(255, 255, 255, 0.75);
  border-radius: 1px;
}
.v2-motion-applied-remove {
  width: 22px; height: 22px;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.10);
  color: #cdd2dc;
  border-radius: 50%;
  cursor: pointer;
  touch-action: manipulation;
  flex: 0 0 auto;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.v2-motion-applied-remove:active {
  background: rgba(239, 68, 68, 0.92);
  color: #ffffff;
  transform: scale(0.92);
}

/* =========================================================
   v2.0.229 — Per-effect chip live previews. Each chip's glyph
   animates with the effect's signature motion. Reuses the v225/226
   timing language (2.0-2.4s loops, ease-out-cubic / overshoot
   curves) so the picker rail has consistent rhythm.

   ENTRANCE effects fade-in or slide-in then hold then loop back to
   start. EXIT effects start at rest, then exit. LOOP effects
   continuously oscillate.
   ========================================================= */

/* Common: shorter loop period for entrance/exit (so users see the
   full motion in 2-3 reps); longer for loops. */

/* ----- ENTRANCE ----- */
.v2-motion-chip--fade-in  .v2-motion-chip-glyph { animation: v2-mp-fadein 2.0s ease-out infinite; }
@keyframes v2-mp-fadein  { 0% { opacity: 0; } 40% { opacity: 1; } 100% { opacity: 1; } }

.v2-motion-chip--pop-in .v2-motion-chip-glyph { animation: v2-mp-popin 2.2s cubic-bezier(.34,1.56,.64,1) infinite; }
@keyframes v2-mp-popin {
  0%       { transform: scale(0);    opacity: 0; }
  40%      { transform: scale(1.25); opacity: 1; }
  60%, 100% { transform: scale(1);   opacity: 1; }
}

.v2-motion-chip--slide-in-left  .v2-motion-chip-glyph { animation: v2-mp-slL 2.0s cubic-bezier(.2,.8,.2,1) infinite; }
@keyframes v2-mp-slL { 0% { transform: translateX(-22px); opacity: 0; } 40% { transform: translateX(0); opacity: 1; } 100% { transform: translateX(0); opacity: 1; } }

.v2-motion-chip--slide-in-right .v2-motion-chip-glyph { animation: v2-mp-slR 2.0s cubic-bezier(.2,.8,.2,1) infinite; }
@keyframes v2-mp-slR { 0% { transform: translateX(22px); opacity: 0; } 40% { transform: translateX(0); opacity: 1; } 100% { transform: translateX(0); opacity: 1; } }

.v2-motion-chip--slide-in-up    .v2-motion-chip-glyph { animation: v2-mp-slU 2.0s cubic-bezier(.2,.8,.2,1) infinite; }
@keyframes v2-mp-slU { 0% { transform: translateY(14px); opacity: 0; } 40% { transform: translateY(0); opacity: 1; } 100% { transform: translateY(0); opacity: 1; } }

.v2-motion-chip--slide-in-down  .v2-motion-chip-glyph { animation: v2-mp-slD 2.0s cubic-bezier(.2,.8,.2,1) infinite; }
@keyframes v2-mp-slD { 0% { transform: translateY(-14px); opacity: 0; } 40% { transform: translateY(0); opacity: 1; } 100% { transform: translateY(0); opacity: 1; } }

.v2-motion-chip--spin-in .v2-motion-chip-glyph { animation: v2-mp-spinIn 2.2s ease-out infinite; }
@keyframes v2-mp-spinIn { 0% { transform: rotate(-180deg) scale(0.4); opacity: 0; } 50% { transform: rotate(0) scale(1); opacity: 1; } 100% { transform: rotate(0) scale(1); opacity: 1; } }

.v2-motion-chip--zoom-in .v2-motion-chip-glyph { animation: v2-mp-zoomIn 2.0s ease-out infinite; }
@keyframes v2-mp-zoomIn { 0% { transform: scale(0.2); opacity: 0; } 45% { transform: scale(1); opacity: 1; } 100% { transform: scale(1); opacity: 1; } }

.v2-motion-chip--bounce-in .v2-motion-chip-glyph { animation: v2-mp-bounceIn 2.2s ease-out infinite; }
@keyframes v2-mp-bounceIn {
  0%   { transform: scale(0.6); opacity: 0; }
  20%  { transform: scale(1.20); opacity: 1; }
  35%  { transform: scale(0.92); }
  50%  { transform: scale(1.05); }
  65%, 100% { transform: scale(1); }
}

.v2-motion-chip--blur-in .v2-motion-chip-glyph { animation: v2-mp-blurIn 2.0s ease-out infinite; }
@keyframes v2-mp-blurIn { 0% { filter: blur(4px); opacity: 0; } 45% { filter: blur(0); opacity: 1; } 100% { filter: blur(0); opacity: 1; } }

.v2-motion-chip--rise-in .v2-motion-chip-glyph { animation: v2-mp-riseIn 2.0s ease-out infinite; }
@keyframes v2-mp-riseIn { 0% { transform: translateY(10px) scale(0.92); opacity: 0; } 40% { transform: translateY(0) scale(1); opacity: 1; } 100% { transform: translateY(0) scale(1); opacity: 1; } }

/* ----- EXIT ----- */
.v2-motion-chip--fade-out .v2-motion-chip-glyph { animation: v2-mp-fadeOut 2.0s ease-in infinite; }
@keyframes v2-mp-fadeOut { 0% { opacity: 1; } 60% { opacity: 1; } 100% { opacity: 0; } }

.v2-motion-chip--pop-out .v2-motion-chip-glyph { animation: v2-mp-popOut 2.2s ease-in infinite; }
@keyframes v2-mp-popOut { 0%, 50% { transform: scale(1); opacity: 1; } 100% { transform: scale(0); opacity: 0; } }

.v2-motion-chip--slide-out-left  .v2-motion-chip-glyph { animation: v2-mp-soL 2.0s ease-in infinite; }
@keyframes v2-mp-soL { 0%, 50% { transform: translateX(0); opacity: 1; } 100% { transform: translateX(-22px); opacity: 0; } }

.v2-motion-chip--slide-out-right .v2-motion-chip-glyph { animation: v2-mp-soR 2.0s ease-in infinite; }
@keyframes v2-mp-soR { 0%, 50% { transform: translateX(0); opacity: 1; } 100% { transform: translateX(22px); opacity: 0; } }

.v2-motion-chip--slide-out-up    .v2-motion-chip-glyph { animation: v2-mp-soU 2.0s ease-in infinite; }
@keyframes v2-mp-soU { 0%, 50% { transform: translateY(0); opacity: 1; } 100% { transform: translateY(-14px); opacity: 0; } }

.v2-motion-chip--slide-out-down  .v2-motion-chip-glyph { animation: v2-mp-soD 2.0s ease-in infinite; }
@keyframes v2-mp-soD { 0%, 50% { transform: translateY(0); opacity: 1; } 100% { transform: translateY(14px); opacity: 0; } }

.v2-motion-chip--spin-out .v2-motion-chip-glyph { animation: v2-mp-spinOut 2.2s ease-in infinite; }
@keyframes v2-mp-spinOut { 0%, 50% { transform: rotate(0) scale(1); opacity: 1; } 100% { transform: rotate(180deg) scale(0.3); opacity: 0; } }

.v2-motion-chip--zoom-out .v2-motion-chip-glyph { animation: v2-mp-zoomOut 2.0s ease-in infinite; }
@keyframes v2-mp-zoomOut { 0%, 50% { transform: scale(1); opacity: 1; } 100% { transform: scale(1.6); opacity: 0; } }

.v2-motion-chip--blur-out .v2-motion-chip-glyph { animation: v2-mp-blurOut 2.0s ease-in infinite; }
@keyframes v2-mp-blurOut { 0%, 50% { filter: blur(0); opacity: 1; } 100% { filter: blur(4px); opacity: 0; } }

/* ----- LOOP ----- */
.v2-motion-chip--pulse .v2-motion-chip-glyph { animation: v2-mp-pulse 1.4s ease-in-out infinite; }
@keyframes v2-mp-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.30); } }

.v2-motion-chip--shake .v2-motion-chip-glyph { animation: v2-mp-shake 0.6s ease-in-out infinite; }
@keyframes v2-mp-shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-3px); } 75% { transform: translateX(3px); } }

.v2-motion-chip--glow .v2-motion-chip-glyph { animation: v2-mp-glow 1.6s ease-in-out infinite; }
@keyframes v2-mp-glow { 0%, 100% { opacity: 0.55; box-shadow: 0 0 4px rgba(163, 255, 18, 0.4); } 50% { opacity: 1; box-shadow: 0 0 12px rgba(163, 255, 18, 0.95); } }

.v2-motion-chip--heartbeat .v2-motion-chip-glyph { animation: v2-mp-hb 1.2s ease-in-out infinite; }
@keyframes v2-mp-hb {
  0%, 60%, 100% { transform: scale(1); }
  6%            { transform: scale(1.30); }
  12%           { transform: scale(1.00); }
  18%           { transform: scale(1.20); }
  24%           { transform: scale(1.00); }
}

.v2-motion-chip--float .v2-motion-chip-glyph { animation: v2-mp-float 2.4s ease-in-out infinite; }
@keyframes v2-mp-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } }

.v2-motion-chip--glitch .v2-motion-chip-glyph { animation: v2-mp-glitch 1.4s linear infinite; }
@keyframes v2-mp-glitch {
  0%, 90%, 100% { transform: translateX(0); filter: blur(0); }
  5%   { transform: translateX(-3px); filter: blur(0.5px); }
  10%  { transform: translateX(3px);  filter: blur(0); }
  15%  { transform: translateX(0); }
  45%  { transform: translateX(0); }
  50%  { transform: translateX(2px); }
  55%  { transform: translateX(-2px); }
  60%  { transform: translateX(0); }
}

.v2-motion-chip--rotate-loop .v2-motion-chip-glyph { animation: v2-mp-rotL 2.6s linear infinite; }
@keyframes v2-mp-rotL { from { transform: rotate(0); } to { transform: rotate(360deg); } }

.v2-motion-chip--wobble .v2-motion-chip-glyph { animation: v2-mp-wobble 1.6s ease-in-out infinite; }
@keyframes v2-mp-wobble { 0%, 100% { transform: rotate(-12deg); } 50% { transform: rotate(12deg); } }

/* ----- BEAT (BPM-synced) ----- */
/* All run at 120 BPM (= 0.5s period) for the preview, matching the
   default param. Pink-tinted glyph differentiates from loop effects. */
.v2-motion-chip--tempo-pulse  .v2-motion-chip-glyph,
.v2-motion-chip--tempo-wobble .v2-motion-chip-glyph,
.v2-motion-chip--tempo-shake  .v2-motion-chip-glyph {
  background: #ff2bd6;
  box-shadow: 0 0 5px rgba(255, 43, 214, 0.7);
}
.v2-motion-chip--tempo-pulse .v2-motion-chip-glyph { animation: v2-mp-tpls 0.5s steps(20, end) infinite; }
@keyframes v2-mp-tpls {
  0%   { transform: scale(1);    }
  12%  { transform: scale(1.40); }
  100% { transform: scale(1);    }
}
.v2-motion-chip--tempo-wobble .v2-motion-chip-glyph { animation: v2-mp-twob 1.0s steps(40, end) infinite; }
@keyframes v2-mp-twob {
  0%   { transform: rotate(0); }
  6%   { transform: rotate(14deg); }
  50%  { transform: rotate(0); }
  56%  { transform: rotate(-14deg); }
  100% { transform: rotate(0); }
}
.v2-motion-chip--tempo-shake .v2-motion-chip-glyph { animation: v2-mp-tshk 0.5s steps(20, end) infinite; }
@keyframes v2-mp-tshk {
  0%   { transform: translate(0, 0);   }
  3%   { transform: translate(3px, -3px);   }
  6%   { transform: translate(-3px, 3px);   }
  9%   { transform: translate(2px, 2px);    }
  12%  { transform: translate(-2px, -2px);  }
  18%, 100% { transform: translate(0, 0);   }
}

/* Speech Pulse — show the chip as 3 small "word" dots that pulse
   in sequence. Visually communicates "syncs to words" rather than
   "syncs to a fixed BPM". We override the default 12×12 single
   glyph with a row of mini-glyphs via the chip's :before content. */
.v2-motion-chip--speech-pulse .v2-motion-chip-glyph {
  display: none;
}
.v2-motion-chip--speech-pulse .v2-motion-chip-preview::before,
.v2-motion-chip--speech-pulse .v2-motion-chip-preview::after,
.v2-motion-chip--speech-pulse .v2-motion-chip-preview {
  /* base bg + border stays */
}
.v2-motion-chip--speech-pulse .v2-motion-chip-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
}
.v2-motion-chip--speech-pulse .v2-motion-chip-preview::before {
  content: '';
  display: block;
  width: 7px; height: 7px;
  background: #ff2bd6;
  border-radius: 2px;
  box-shadow: 0 0 4px rgba(255, 43, 214, 0.7);
  animation: v2-mp-speech-a 1.8s ease-out infinite;
}
.v2-motion-chip--speech-pulse .v2-motion-chip-preview::after {
  content: '';
  display: block;
  width: 7px; height: 7px;
  background: #ff2bd6;
  border-radius: 2px;
  box-shadow: 0 0 4px rgba(255, 43, 214, 0.7);
  animation: v2-mp-speech-b 1.8s ease-out infinite;
}
@keyframes v2-mp-speech-a {
  0%, 100%  { transform: scale(1); opacity: 0.5; }
  15%       { transform: scale(1.45); opacity: 1; }
  30%       { transform: scale(1); opacity: 0.5; }
}
@keyframes v2-mp-speech-b {
  0%, 100%  { transform: scale(1); opacity: 0.5; }
  45%       { transform: scale(1.45); opacity: 1; }
  60%       { transform: scale(1); opacity: 0.5; }
}

/* ----- PATH (curve-following motion) -----
   Each chip's glyph traces the path effect's curve in a 2.4s loop.
   Purple-tinted glyph differentiates from other categories. Paths
   approximated via stepped translate keyframes — visually close
   enough to the parametric curves used in renderer composition. */
.v2-motion-chip--path-arc-up   .v2-motion-chip-glyph,
.v2-motion-chip--path-arc-down .v2-motion-chip-glyph,
.v2-motion-chip--path-loop     .v2-motion-chip-glyph,
.v2-motion-chip--path-s-wave   .v2-motion-chip-glyph,
.v2-motion-chip--path-zigzag   .v2-motion-chip-glyph {
  background: #a78bfa;
  box-shadow: 0 0 5px rgba(167, 139, 250, 0.7);
}
.v2-motion-chip--path-arc-up .v2-motion-chip-glyph { animation: v2-mp-arcup 2.4s ease-in-out infinite; }
@keyframes v2-mp-arcup {
  0%   { transform: translate(-16px, 4px); }
  50%  { transform: translate(0,    -10px); }
  100% { transform: translate(16px,  4px); }
}
.v2-motion-chip--path-arc-down .v2-motion-chip-glyph { animation: v2-mp-arcdown 2.4s ease-in-out infinite; }
@keyframes v2-mp-arcdown {
  0%   { transform: translate(-16px, -4px); }
  50%  { transform: translate(0,    10px); }
  100% { transform: translate(16px, -4px); }
}
.v2-motion-chip--path-loop .v2-motion-chip-glyph { animation: v2-mp-loop 2.4s linear infinite; }
@keyframes v2-mp-loop {
  0%   { transform: translate(0,    -10px); }
  25%  { transform: translate(10px,  0); }
  50%  { transform: translate(0,    10px); }
  75%  { transform: translate(-10px, 0); }
  100% { transform: translate(0,    -10px); }
}
.v2-motion-chip--path-s-wave .v2-motion-chip-glyph { animation: v2-mp-swave 2.4s linear infinite; }
@keyframes v2-mp-swave {
  0%    { transform: translate(-18px, 0); }
  12.5% { transform: translate(-13.5px, -7px); }
  25%   { transform: translate(-9px, 0); }
  37.5% { transform: translate(-4.5px, 7px); }
  50%   { transform: translate(0, 0); }
  62.5% { transform: translate(4.5px, -7px); }
  75%   { transform: translate(9px, 0); }
  87.5% { transform: translate(13.5px, 7px); }
  100%  { transform: translate(18px, 0); }
}
.v2-motion-chip--path-zigzag .v2-motion-chip-glyph { animation: v2-mp-zigzag 2.4s linear infinite; }
@keyframes v2-mp-zigzag {
  0%   { transform: translate(-18px, 0); }
  20%  { transform: translate(-9px,  -8px); }
  40%  { transform: translate(0,      8px); }
  60%  { transform: translate(9px,   -8px); }
  80%  { transform: translate(18px,   0); }
  100% { transform: translate(-18px,  0); }
}

/* ----- SPECIAL ----- */
.v2-motion-chip--ken-burns-in  .v2-motion-chip-glyph { animation: v2-mp-kbi 2.4s ease-in-out infinite; }
@keyframes v2-mp-kbi { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.6); } }

.v2-motion-chip--ken-burns-out .v2-motion-chip-glyph { animation: v2-mp-kbo 2.4s ease-in-out infinite; }
@keyframes v2-mp-kbo { 0%, 100% { transform: scale(1.6); } 50% { transform: scale(1); } }

/* =========================================================
   v2.0.231 — Smart compositions tab.

   Composition cards are larger than effect chips because they pack
   more info: name + description + a "stacked motion" preview that
   hints at the composition's character. Two glyphs (primary +
   secondary) animate inside the preview area so the card visually
   suggests "multi-effect recipe", not "single effect".

   Cards take ~2 per row on a typical portrait phone (panel width
   ~340px); each card is 110px wide. wraps on smaller phones.
   ========================================================= */
.v2-motion-comp-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  padding: 4px 0 8px;
}
.v2-motion-comp-card {
  flex: 1 1 calc(50% - 5px);
  min-width: 110px;
  display: flex; flex-direction: column; align-items: stretch;
  padding: 10px;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.07), rgba(255, 255, 255, 0.03));
  border: 1px solid rgba(255, 255, 255, 0.10);
  border-radius: 12px;
  color: #f7f8fa;
  cursor: pointer;
  text-align: left;
  touch-action: manipulation;
  transition:
    transform var(--t-fast) var(--ease),
    background var(--t-fast) var(--ease),
    border-color var(--t-fast) var(--ease),
    box-shadow var(--t-fast) var(--ease);
}
.v2-motion-comp-card:hover {
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.11), rgba(255, 255, 255, 0.05));
  border-color: rgba(255, 255, 255, 0.22);
  transform: translateY(-1px);
  box-shadow: 0 6px 14px rgba(0, 0, 0, 0.40);
}
.v2-motion-comp-card:active {
  transform: scale(0.97);
  background: linear-gradient(180deg, rgba(163, 255, 18, 0.20), rgba(163, 255, 18, 0.08));
  border-color: rgba(163, 255, 18, 0.55);
  box-shadow: 0 0 16px rgba(163, 255, 18, 0.40);
}
.v2-motion-comp-preview {
  height: 44px;
  position: relative;
  border-radius: 8px;
  background: linear-gradient(135deg, #1a1d2e 0%, #0a0b10 100%);
  border: 1px solid rgba(255, 255, 255, 0.05);
  margin-bottom: 8px;
  overflow: hidden;
}
/* Two stacked glyphs — primary lime, secondary white. Each
   composition class chooses its own animation pair (entrance +
   loop signature). Centered with margin trick. */
.v2-motion-comp-glyph {
  position: absolute;
  top: 50%; left: 50%;
  width: 11px; height: 11px;
  margin: -5.5px 0 0 -5.5px;
  background: #a3ff12;
  border-radius: 2px;
  box-shadow: 0 0 4px rgba(163, 255, 18, 0.5);
  transform-origin: center;
  will-change: transform, opacity;
}
.v2-motion-comp-glyph--b {
  background: rgba(255, 255, 255, 0.80);
  width: 8px; height: 8px;
  margin: -4px 0 0 -4px;
  box-shadow: 0 0 3px rgba(255, 255, 255, 0.6);
}
.v2-motion-comp-label {
  font: 800 12px/1.2 'Inter', system-ui, sans-serif;
  letter-spacing: 0.005em;
  color: #e7eaf0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.v2-motion-comp-desc {
  font: 500 10px/1.3 'Inter', system-ui, sans-serif;
  color: #9ca3af;
  margin-top: 2px;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

/* ----- Per-composition signature animations (preview area) -----

   Each pair shows the composition's character in 2.6s loops.
   Lime glyph = entrance motion; white glyph (b) = loop/secondary
   motion. Same GPU-cheap transform+opacity language as the effect
   chip previews. */

/* v2.0.232 — Saved-preset card variants. Subtle yellow accent in
   border + a corner trash chip for delete. The shared
   .v2-motion-comp-card styling above carries the rest. */
.v2-motion-comp-card--saved {
  position: relative;
  border-color: rgba(255, 230, 109, 0.30);
}
.v2-motion-comp-card--saved:hover { border-color: rgba(255, 230, 109, 0.50); }
.v2-motion-comp-card--saved .v2-motion-comp-glyph { background: #ffe66d; box-shadow: 0 0 4px rgba(255, 230, 109, 0.6); animation: v2-mc-saved 2.0s ease-in-out infinite; }
@keyframes v2-mc-saved {
  0%, 100% { transform: scale(1); opacity: 1; }
  50%      { transform: scale(1.20); opacity: 0.85; }
}
.v2-motion-comp-delete,
.v2-motion-comp-rename {
  position: absolute;
  top: 6px;
  width: 18px; height: 18px;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(0, 0, 0, 0.55);
  border: 1px solid rgba(255, 255, 255, 0.18);
  color: #cdd2dc;
  border-radius: 50%;
  cursor: pointer;
  touch-action: manipulation;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.v2-motion-comp-delete { right: 6px; }
.v2-motion-comp-rename { right: 28px; }     /* sits left of delete */
.v2-motion-comp-rename:hover  { background: rgba(0, 0, 0, 0.75); }
.v2-motion-comp-rename:active {
  transform: scale(0.88);
  background: linear-gradient(180deg, #c5ff4a, #a3ff12);
  color: #0a0b10;
}
.v2-motion-comp-delete:hover  { background: rgba(0, 0, 0, 0.75); }
.v2-motion-comp-delete:active {
  transform: scale(0.88);
  background: rgba(239, 68, 68, 0.92);
  color: #ffffff;
}
/* v2.0.234 — Inline rename input. Replaces the label span when in
   rename mode. Tight visual fit so the card doesn't jump size. */
.v2-motion-comp-rename-input {
  background: rgba(0, 0, 0, 0.50);
  border: 1px solid rgba(163, 255, 18, 0.55);
  border-radius: 5px;
  color: #ffffff;
  font: 800 12px/1.2 'Inter', system-ui, sans-serif;
  letter-spacing: 0.005em;
  padding: 3px 6px;
  width: 100%;
  box-sizing: border-box;
  outline: none;
  box-shadow: 0 0 8px rgba(163, 255, 18, 0.35);
}
.v2-motion-comp-rename-input:focus {
  border-color: rgba(163, 255, 18, 0.85);
  box-shadow: 0 0 12px rgba(163, 255, 18, 0.55);
}
/* When renaming, dim the action chips so they don't compete for
   visual attention with the input. */
.v2-motion-comp-card.is-renaming .v2-motion-comp-rename,
.v2-motion-comp-card.is-renaming .v2-motion-comp-delete {
  opacity: 0.5;
  pointer-events: none;
}

/* Cinematic Title — slow zoom + soft fade */
.v2-motion-comp-card--cinematic-title .v2-motion-comp-glyph     { animation: v2-mc-cin 2.6s ease-in-out infinite; }
.v2-motion-comp-card--cinematic-title .v2-motion-comp-glyph--b  { display: none; }
@keyframes v2-mc-cin { 0% { opacity: 0; transform: scale(0.9); } 25% { opacity: 1; } 75% { opacity: 1; transform: scale(1.25); } 100% { opacity: 0; transform: scale(1.30); } }

/* TikTok Hook — pop in + pulse */
.v2-motion-comp-card--tiktok-hook .v2-motion-comp-glyph     { animation: v2-mc-tk 2.0s cubic-bezier(.34,1.56,.64,1) infinite; }
.v2-motion-comp-card--tiktok-hook .v2-motion-comp-glyph--b  { display: none; }
@keyframes v2-mc-tk { 0% { opacity: 0; transform: scale(0); } 25% { opacity: 1; transform: scale(1.25); } 50% { transform: scale(0.95); } 75% { transform: scale(1.15); } 100% { opacity: 0; transform: scale(1); } }

/* Smooth Caption — slide up + fade out */
.v2-motion-comp-card--smooth-caption .v2-motion-comp-glyph    { animation: v2-mc-sc 2.4s ease-out infinite; }
.v2-motion-comp-card--smooth-caption .v2-motion-comp-glyph--b { display: none; }
@keyframes v2-mc-sc { 0% { transform: translateY(12px); opacity: 0; } 30% { transform: translateY(0); opacity: 1; } 75% { opacity: 1; } 100% { opacity: 0; } }

/* Spotlight — zoom + glow pulse */
.v2-motion-comp-card--spotlight .v2-motion-comp-glyph    { animation: v2-mc-sp 2.4s ease-in-out infinite; }
.v2-motion-comp-card--spotlight .v2-motion-comp-glyph--b { display: none; }
@keyframes v2-mc-sp {
  0%   { transform: scale(0.3); opacity: 0; box-shadow: 0 0 0 rgba(163, 255, 18, 0); }
  30%  { transform: scale(1);   opacity: 1; box-shadow: 0 0 8px rgba(163, 255, 18, 0.95); }
  50%  { transform: scale(1.18); opacity: 1; box-shadow: 0 0 14px rgba(163, 255, 18, 0.95); }
  100% { transform: scale(1);   opacity: 0; box-shadow: 0 0 4px rgba(163, 255, 18, 0.3); }
}

/* Storytelling — rise in + slide out up */
.v2-motion-comp-card--storytelling-reveal .v2-motion-comp-glyph     { animation: v2-mc-st 2.6s ease-in-out infinite; }
.v2-motion-comp-card--storytelling-reveal .v2-motion-comp-glyph--b  { display: none; }
@keyframes v2-mc-st { 0% { transform: translateY(8px); opacity: 0; } 25% { transform: translateY(0); opacity: 1; } 70% { transform: translateY(0); opacity: 1; } 100% { transform: translateY(-14px); opacity: 0; } }

/* Burst — bounce in + shake */
.v2-motion-comp-card--burst .v2-motion-comp-glyph     { animation: v2-mc-bs 2.0s ease-out infinite; }
.v2-motion-comp-card--burst .v2-motion-comp-glyph--b  { display: none; }
@keyframes v2-mc-bs {
  0%        { transform: translateX(0) scale(0.5); opacity: 0; }
  15%       { transform: translateX(0) scale(1.2); opacity: 1; }
  25%       { transform: translateX(-3px); }
  35%       { transform: translateX(3px); }
  45%       { transform: translateX(-2px); }
  55%       { transform: translateX(2px); }
  65%, 85%  { transform: translateX(0) scale(1); opacity: 1; }
  100%      { transform: scale(0); opacity: 0; }
}

/* Floaty — fade in + float drift */
.v2-motion-comp-card--floaty .v2-motion-comp-glyph     { animation: v2-mc-ft 2.6s ease-in-out infinite; }
.v2-motion-comp-card--floaty .v2-motion-comp-glyph--b  { display: none; }
@keyframes v2-mc-ft { 0% { transform: translateY(0); opacity: 0; } 25% { opacity: 1; } 50% { transform: translateY(-5px); } 75% { opacity: 1; transform: translateY(0); } 100% { opacity: 0; } }

/* Glitch Pop — pop + digital glitch */
.v2-motion-comp-card--glitch-pop .v2-motion-comp-glyph     { animation: v2-mc-gp 1.6s linear infinite; }
.v2-motion-comp-card--glitch-pop .v2-motion-comp-glyph--b  { display: none; }
@keyframes v2-mc-gp {
  0%   { transform: scale(0); opacity: 0; filter: blur(0); }
  15%  { transform: scale(1); opacity: 1; filter: blur(0); }
  25%  { transform: translateX(-3px); filter: blur(0.5px); }
  30%  { transform: translateX(3px); }
  35%  { transform: translateX(0); filter: blur(0); }
  60%  { transform: translateX(2px); }
  65%  { transform: translateX(-2px); }
  70%  { transform: translateX(0); }
  85%  { transform: scale(1); opacity: 1; }
  100% { transform: scale(0.4); opacity: 0; }
}

/* Dramatic Drop — drop in + heartbeat */
.v2-motion-comp-card--dramatic-drop .v2-motion-comp-glyph     { animation: v2-mc-dd 2.4s ease-in-out infinite; }
.v2-motion-comp-card--dramatic-drop .v2-motion-comp-glyph--b  { display: none; }
@keyframes v2-mc-dd {
  0%        { transform: translateY(-12px) scale(1); opacity: 0; }
  20%       { transform: translateY(0); opacity: 1; }
  35%       { transform: translateY(0) scale(1.18); }
  45%       { transform: translateY(0) scale(1.00); }
  55%       { transform: translateY(0) scale(1.12); }
  65%, 80%  { transform: translateY(0) scale(1); opacity: 1; }
  100%      { transform: translateY(14px); opacity: 0; }
}

/* Spin Showcase — spin in + wobble */
.v2-motion-comp-card--spin-showcase .v2-motion-comp-glyph     { animation: v2-mc-ss 2.6s ease-in-out infinite; }
.v2-motion-comp-card--spin-showcase .v2-motion-comp-glyph--b  { display: none; }
@keyframes v2-mc-ss {
  0%   { transform: rotate(-180deg) scale(0.4); opacity: 0; }
  25%  { transform: rotate(0) scale(1); opacity: 1; }
  40%  { transform: rotate(-10deg); }
  55%  { transform: rotate(10deg); }
  70%  { transform: rotate(0) scale(1); opacity: 1; }
  100% { transform: rotate(180deg) scale(0.4); opacity: 0; }
}

/* Minimal Fade — just fades */
.v2-motion-comp-card--minimal-fade .v2-motion-comp-glyph     { animation: v2-mc-mf 2.0s ease-in-out infinite; }
.v2-motion-comp-card--minimal-fade .v2-motion-comp-glyph--b  { display: none; }
@keyframes v2-mc-mf { 0% { opacity: 0; } 25% { opacity: 1; } 75% { opacity: 1; } 100% { opacity: 0; } }

/* Attention — pop + continuous pulse */
.v2-motion-comp-card--attention-pulse .v2-motion-comp-glyph     { animation: v2-mc-at 1.6s cubic-bezier(.34,1.56,.64,1) infinite; }
.v2-motion-comp-card--attention-pulse .v2-motion-comp-glyph--b  { display: none; }
@keyframes v2-mc-at {
  0%   { transform: scale(0); opacity: 0; }
  20%  { transform: scale(1.25); opacity: 1; }
  40%  { transform: scale(1); }
  55%  { transform: scale(1.18); }
  70%  { transform: scale(1); }
  85%  { transform: scale(1.18); }
  100% { transform: scale(1); opacity: 1; }
}

/* ----- Preview controls strip -----
   v2.0.150 — Tightened up: bigger play button, mono tabular-num time
   readout with current digits standing out from total, subtle live
   indicator dot pulse when playing. */
.v2-controls {
  grid-row: 3;
  display: flex; align-items: center; justify-content: space-between;
  padding: 0 var(--s3);
  background: var(--bg);
  z-index: var(--z-controls);
}
.v2-controls-left, .v2-controls-right {
  display: flex; align-items: center; gap: var(--s2);
}
.v2-time {
  display: inline-flex; align-items: baseline; gap: 4px;
  font: 700 13px/1 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  color: var(--text);
  padding: 4px 10px;
  background: rgba(255,255,255,0.04);
  border-radius: 999px;
  border: 1px solid var(--border-2);
  transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
.v2-time-total {
  font-weight: 600;
  font-size: 11px;
  color: var(--text-3);
  margin-left: 0;
}
/* v2.0.150 — When playing, the time readout flips to lime + a tiny
   pulsing dot prepends. Communicates "live" at a glance, like CapCut's
   recording indicator. */
.v2-play.is-playing ~ * .v2-time,
.v2-controls:has(.v2-play.is-playing) .v2-time {
  color: var(--accent);
  border-color: rgba(163, 255, 18, 0.35);
  background: rgba(163, 255, 18, 0.08);
}
.v2-controls:has(.v2-play.is-playing) .v2-time::before {
  content: '';
  width: 5px; height: 5px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 6px rgba(163, 255, 18, 0.7);
  margin-right: 2px;
  align-self: center;
  animation: v2-time-blink 1100ms ease-in-out infinite;
}
@keyframes v2-time-blink {
  0%, 100% { opacity: 1; transform: scale(1); }
  50%      { opacity: 0.35; transform: scale(0.75); }
}

.v2-play {
  position: relative;
  width: 48px; height: 48px; border-radius: 50%;
  background: var(--surface-2); color: var(--text);
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--border);
  cursor: pointer;
  touch-action: manipulation;
  transition:
    background var(--t-fast) var(--ease),
    color var(--t-fast) var(--ease),
    border-color var(--t-fast) var(--ease),
    transform 140ms cubic-bezier(0.2, 1.0, 0.4, 1),
    box-shadow var(--t-fast) var(--ease);
}
.v2-play:active { transform: scale(0.93); background: var(--surface-3); }
.v2-play.is-playing {
  background: var(--accent);
  color: var(--on-accent);
  border-color: var(--accent);
  box-shadow: 0 0 0 0 rgba(163, 255, 18, 0.55);
  animation: v2-play-pulse 1500ms var(--ease) infinite;
}
@keyframes v2-play-pulse {
  0%   { box-shadow: 0 0 0 0   rgba(163, 255, 18, 0.55); }
  70%  { box-shadow: 0 0 0 10px rgba(163, 255, 18, 0);   }
  100% { box-shadow: 0 0 0 0   rgba(163, 255, 18, 0);   }
}

/* ----- Timeline rail ----- */
.v2-timeline {
  grid-row: 4;
  position: relative;
  background: var(--bg);
  border-top: 1px solid var(--border);
  z-index: var(--z-timeline);
  display: flex;
  overflow: hidden;
}
.v2-tl-tray {
  flex: 0 0 auto;
  width: 64px;
  display: flex; flex-direction: column; align-items: stretch;
  background: var(--bg);
  padding: 26px 8px 0;
  gap: 8px;
  z-index: 2;
  /* v2.0.50 — tray collapses to width 0 when the user is actively
     touching the rail, so the timeline content takes the full width.
     Returns 600ms after release. */
  overflow: hidden;
  transition: width 200ms var(--ease), padding 200ms var(--ease), opacity 180ms var(--ease);
}
.v2-edit.is-timeline-active .v2-tl-tray {
  width: 0;
  padding-left: 0;
  padding-right: 0;
  opacity: 0;
  pointer-events: none;
}
.v2-tl-tray-btn {
  height: 44px; width: 100%;
  border-radius: 10px;
  background: var(--surface-2);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px;
  color: var(--text-2);
  border: 1px solid var(--border-2);
  transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
}
.v2-tl-tray-btn:active { background: var(--surface-3); border-color: var(--text-3); }
.v2-tl-tray-btn.is-on {
  background: rgba(163, 255, 18, 0.14);
  border-color: var(--accent);
  color: var(--accent);
}
.v2-tl-tray-btn.is-on .v2-tl-tray-lbl { color: var(--accent); }

/* Add Media — prominent accent fill. Anchors the tray and always
   reachable regardless of timeline scroll. v2.0.49. */
.v2-tl-tray-btn--add {
  background: var(--accent);
  color: var(--on-accent);
  border-color: var(--accent);
  box-shadow: 0 4px 14px rgba(163, 255, 18, 0.25);
}
.v2-tl-tray-btn--add .v2-tl-tray-lbl { color: var(--on-accent); }
.v2-tl-tray-btn--add:active {
  transform: scale(0.95);
  background: var(--accent);
}

/* Floating Add button anchored to the right edge of the timeline
   viewport. Always visible regardless of how far the rail is
   scrolled — sits OVER the rail with a strong shadow so it reads
   as an action affordance, not part of the track. v2.0.51. */
.v2-tl-rail-add-float {
  position: absolute;
  right: 8px;
  top: 34px;                      /* sits over the video track strip */
  width: 30px; height: 30px;
  border-radius: 8px;
  background: #fff;
  color: #0a0b10;
  border: 1px solid rgba(0,0,0,0.15);
  display: inline-flex; align-items: center; justify-content: center;
  box-shadow: 0 3px 10px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.05);
  cursor: pointer;
  touch-action: manipulation;
  z-index: 12;
  transition: transform var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.v2-tl-rail-add-float:active {
  transform: scale(0.92);
  box-shadow: 0 2px 6px rgba(0,0,0,0.5);
}
.v2-tl-tray-btn--zoom {
  height: 36px;            /* compact zoom buttons */
  color: var(--text-2);
  touch-action: manipulation;
}
.v2-tl-tray-btn--zoom:active { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
.v2-tl-tray-lbl {
  font: 600 8px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  color: var(--text-2);
}
.v2-tl-rail {
  flex: 1 1 auto;
  position: relative;
  overflow-x: auto; overflow-y: hidden;
  scrollbar-width: none;
  touch-action: pan-x;
  overscroll-behavior-x: contain;
  /* v2.0.340 — Inertial momentum scrolling on iOS Safari. Pre-v340
     the timeline rail was the only scroller in the file without this
     property; swiping a 2-minute project stopped dead with the
     finger instead of coasting. CapCut feel demands native momentum.
     Other browsers ignore this (no-op). */
  -webkit-overflow-scrolling: touch;
}
.v2-tl-rail::-webkit-scrollbar { display: none; }
.v2-tl-ruler {
  height: 24px;                /* taller — easier touch target for scrub */
  position: sticky; top: 0;
  display: block;
  padding: 0;
  font: 600 9px/1 'JetBrains Mono', ui-monospace, monospace;
  color: var(--text-3);
  background: linear-gradient(180deg, var(--bg) 70%, transparent);
  z-index: 6;
  touch-action: none;          /* absorbs pointer; rail won't scroll under */
  cursor: ew-resize;
  user-select: none;
}
.v2-tl-ruler.is-scrubbing {
  background: linear-gradient(180deg, rgba(163, 255, 18, 0.10) 70%, transparent);
}
/* v2.0.151 — Ruler ticks: minor (border-2, 60% height) vs major
   (text-3, full height). is-major class assigned in JSX based on
   the zoom-adapted step. Old `:nth-child(5n)` rule retained as a
   fallback but is-major takes precedence for the new zoom logic. */
.v2-tl-tick {
  position: absolute;
  top: 14px; bottom: 0;
  width: 1px;
  background: var(--border-2);
  pointer-events: none;
}
.v2-tl-tick:nth-child(5n) { background: var(--text-3); top: 10px; }
.v2-tl-tick.is-major { background: var(--text-3); top: 8px; }
.v2-tl-tick-lbl {
  position: absolute;
  top: -10px;
  left: 3px;
  white-space: nowrap;
  color: var(--text-2);
  font-variant-numeric: tabular-nums;
}

/* Track variants — video gets full clip-strip height,
   audio/text are compact stub rows (CapCut pattern). */
.v2-tl-track {
  position: relative;
  margin: 0 0 6px;
  border-radius: 8px;
  background: var(--surface);
  border: 1px solid var(--border);
  overflow: hidden;
}
.v2-tl-track--video { height: 56px; }
.v2-tl-track--audio,
.v2-tl-track--text  { height: 32px; }

.v2-tl-track-stub {
  position: absolute; inset: 0;
  display: flex; align-items: center; gap: 8px;
  padding: 0 12px;
  font: 700 11px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  color: var(--text-3);
}
.v2-tl-track-stub-icon {
  width: 22px; height: 22px;
  border-radius: 6px;
  background: var(--surface-2);
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--text-2);
  border: 1px solid var(--border-2);
  flex: 0 0 auto;
}
/* Video track empty state — call-to-action vibe with dashed accent
   border + faint accent fill so the user reads it as "tap here to
   add". Audio/text stay subtle (they're sub-tracks). */
.v2-tl-track--video:has(.v2-tl-track-stub) {
  background:
    repeating-linear-gradient(135deg, rgba(163,255,18,0.04) 0 12px, transparent 12px 24px),
    var(--surface);
  border-style: dashed;
  border-color: rgba(163, 255, 18, 0.35);
}
.v2-tl-track--video .v2-tl-track-stub {
  color: var(--accent);
  font-weight: 800;
}
.v2-tl-track--video .v2-tl-track-stub-icon {
  background: rgba(163, 255, 18, 0.14);
  border-color: rgba(163, 255, 18, 0.45);
  color: var(--accent);
}

/* v2.0.156 — Audio + text track stubs get their own color-coded
   accent treatment so the user instantly knows what kind of media
   each empty lane is for. Cyan for audio (matches the waveform);
   amber for text (matches the text bar). */
.v2-tl-track--audio:has(.v2-tl-track-stub) {
  background:
    repeating-linear-gradient(135deg, rgba(6, 182, 212, 0.04) 0 12px, transparent 12px 24px),
    var(--surface);
  border-style: dashed;
  border-color: rgba(6, 182, 212, 0.30);
}
.v2-tl-track--audio .v2-tl-track-stub { color: #06b6d4; font-weight: 800; }
.v2-tl-track--audio .v2-tl-track-stub-icon {
  background: rgba(6, 182, 212, 0.14);
  border-color: rgba(6, 182, 212, 0.40);
  color: #06b6d4;
}
.v2-tl-track--text:has(.v2-tl-track-stub) {
  background:
    repeating-linear-gradient(135deg, rgba(245, 158, 11, 0.04) 0 12px, transparent 12px 24px),
    var(--surface);
  border-style: dashed;
  border-color: rgba(245, 158, 11, 0.30);
}
.v2-tl-track--text .v2-tl-track-stub { color: #f59e0b; font-weight: 800; }
.v2-tl-track--text .v2-tl-track-stub-icon {
  background: rgba(245, 158, 11, 0.14);
  border-color: rgba(245, 158, 11, 0.40);
  color: #f59e0b;
}

/* Stub-as-button — used when the stub IS the tap target (no clips yet).
   Inherits the dashed accent treatment from .v2-tl-track--video; we
   just add :active feedback + remove default button chrome. */
.v2-tl-track-stub--cta {
  appearance: none;
  background: transparent;
  border: 0;
  width: 100%; height: 100%;
  cursor: pointer;
  touch-action: manipulation;
  transition: transform var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
.v2-tl-track-stub--cta:active {
  transform: scale(0.985);
  background: rgba(163, 255, 18, 0.06);
}

/* Inline "+" tile that appears after the last video clip — lets the
   user keep adding without scrolling back to an empty state. Floats
   to the right of the last sceneBlock at its computed `left` position. */
.v2-tl-bar-add {
  position: absolute;
  top: 4px; bottom: 4px;
  width: 40px;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(163, 255, 18, 0.10);
  color: var(--accent);
  border: 1px dashed rgba(163, 255, 18, 0.40);
  border-radius: 6px;
  cursor: pointer;
  touch-action: manipulation;
  transition: background var(--t-fast) var(--ease), transform var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
  z-index: 5;
}
.v2-tl-bar-add:active {
  transform: scale(0.94);
  background: rgba(163, 255, 18, 0.18);
  border-color: var(--accent);
}

/* Transition button at clip boundaries (v2.0.48). Small chip on
   the join between two clips — tap to open the transition picker.
   Active state when a non-None transition is set. */
.v2-tl-trans-btn {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 22px; height: 22px;
  border-radius: 50%;
  background: var(--surface-3);
  color: var(--text-2);
  border: 1px solid var(--border-2);
  display: inline-flex; align-items: center; justify-content: center;
  font: 800 12px/1 'Inter', system-ui, sans-serif;
  cursor: pointer;
  touch-action: manipulation;
  z-index: 11;
  transition: background var(--t-fast) var(--ease), transform var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
}
.v2-tl-trans-btn:active { transform: translateY(-50%) scale(0.9); }
.v2-tl-trans-btn.is-on {
  background: var(--accent);
  color: var(--on-accent);
  border-color: var(--accent);
  box-shadow: 0 0 0 2px rgba(163, 255, 18, 0.18);
}

.v2-tl-playhead {
  position: absolute;
  top: 0;                     /* lives inside scroll content; spans full height */
  bottom: 0;
  width: 1.5px;
  background: var(--accent);
  pointer-events: none;
  z-index: 7;
  box-shadow: 0 0 6px rgba(163, 255, 18, 0.45);
  transform: translateX(-0.75px);   /* center the line on its time */
  transition: opacity var(--t-fast) var(--ease);
}
.v2-tl-playhead::before {
  content: '';
  position: absolute;
  top: 4px; left: -4px;
  width: 9px; height: 9px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 8px rgba(163, 255, 18, 0.7);
}
.v2-tl-playhead.is-scrubbing {
  width: 2px;
}
.v2-tl-playhead.is-scrubbing::before {
  width: 13px; height: 13px;
  top: 2px; left: -6px;
  box-shadow: 0 0 12px rgba(163, 255, 18, 0.9);
}

/* v2.0.134 — Floating time bubble pinned to the playhead head. Visible
   during scrub or playback; quietly fades at rest. The lime fill + mono
   text reads as a focused "you are here" indicator, matching CapCut's
   playhead chrome. Positioned ABOVE the head so the user's finger
   (during scrub) doesn't cover the readout. */
.v2-tl-playhead-bubble {
  position: absolute;
  bottom: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%);
  padding: 3px 7px;
  border-radius: 5px;
  background: var(--accent);
  color: var(--on-accent);
  font: 800 10px/1 'JetBrains Mono', ui-monospace, monospace;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  white-space: nowrap;
  box-shadow: 0 2px 6px rgba(0,0,0,0.4), 0 0 8px rgba(163, 255, 18, 0.45);
  pointer-events: none;
  opacity: 0;
  transition: opacity 140ms var(--ease), transform 140ms var(--ease);
  transform-origin: bottom center;
}
.v2-tl-playhead-bubble::after {
  /* Tiny pointer triangle pointing DOWN at the playhead head. */
  content: '';
  position: absolute;
  top: 100%; left: 50%;
  transform: translateX(-50%);
  width: 0; height: 0;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 4px solid var(--accent);
}
.v2-tl-playhead.is-scrubbing .v2-tl-playhead-bubble,
.v2-tl-playhead.is-playing   .v2-tl-playhead-bubble {
  opacity: 1;
}
.v2-tl-playhead.is-scrubbing .v2-tl-playhead-bubble {
  transform: translateX(-50%) scale(1.08);
}

/* ----- Bars (clip / overlay color blocks) ----- */
/* Each bar absolutely positions itself by left/width based on time.
   Color comes from --bar-color (set inline) for video, or fixed
   for audio/text. Refined corners + subtle inner highlight.       */
.v2-tl-bar {
  position: absolute;
  top: 4px; bottom: 4px;
  background: var(--bar-color, var(--surface-2));
  border-radius: 6px;
  padding: 0 8px;
  display: flex; align-items: center;
  overflow: hidden;
  cursor: pointer;
  border: 1px solid rgba(255,255,255,0.08);
  transition: transform var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.v2-tl-bar:active {
  transform: scale(0.985);
  box-shadow: 0 4px 12px rgba(0,0,0,0.4);
}

/* Video clip thumbnails — strip of mini frames extracted from the
   source video and tiled across the full bar width. Layered BELOW
   the label (which gets a text-shadow for legibility against
   varied frame contents). Path A.3. */
.v2-tl-bar.has-thumbs { background: #0d0f15; }    /* dark base behind thumbs */
.v2-tl-bar-thumbs {
  position: absolute; inset: 0;
  display: flex;
  overflow: hidden;
  border-radius: 6px;
  pointer-events: none;
  z-index: 0;
}
.v2-tl-bar-thumb {
  flex: 1 1 0;
  height: 100%;
  width: 0;       /* let flex distribute */
  object-fit: cover;
  display: block;
  min-width: 0;
}
/* v2.0.175 — sprite-based thumbnails. ONE server-generated jpeg
   contains N frames tiled side-by-side; each .v2-tl-bar-thumb--sprite
   div positions its background to expose just its frame's slice. The
   parent .v2-tl-bar-thumbs--sprite is still flex so widths distribute
   evenly across the bar regardless of bar zoom. */
.v2-tl-bar-thumb--sprite {
  flex: 1 1 0;
  height: 100%;
  min-width: 0;
  background-color: var(--surface-2);
  /* background-image / size / position set inline (sprite URL is per-asset) */
}
.v2-tl-bar.has-thumbs .v2-tl-bar-lbl {
  position: relative;
  z-index: 1;
  text-shadow: 0 1px 2px rgba(0,0,0,0.8), 0 0 8px rgba(0,0,0,0.6);
  background: linear-gradient(90deg, rgba(0,0,0,0.55), transparent 40%);
  padding-right: 16px;
  width: 100%;
}

/* v2.0.158 — Broken-media indicator on timeline video bars. The video
   element fired an error event (network / CORS / decode), the canvas
   would otherwise stay black with no explanation. Now the bar shows a
   red "Unavailable" badge so the user knows to re-upload that clip. */
.v2-tl-bar.is-broken {
  background:
    repeating-linear-gradient(135deg, rgba(249, 119, 119, 0.16) 0 6px, rgba(249, 119, 119, 0.08) 6px 12px),
    var(--surface-2);
  border-color: rgba(249, 119, 119, 0.55);
}
.v2-tl-bar.is-broken .v2-tl-bar-thumbs { display: none; }
/* v2.0.308 GAP C — Hide the PiP-specific overlays (keyframe diamond
   strip + speed-curve SVG) when the bar is in a broken state. They
   sit at z-index 2 atop the diagonal-stripe broken background +
   collide visually with the Failed banner. Generic rule: any bar
   that's broken hides these overlay layers. Text/audio bars don't
   have speed curves; PiP + scene bars share the kf-strip pattern. */
.v2-tl-bar.is-broken .v2-tl-bar-kf-strip { display: none; }
.v2-tl-bar.is-broken .v2-tl-bar-speed-curve { display: none; }
.v2-tl-bar-broken {
  position: absolute;
  inset: 0;
  display: flex; align-items: center; justify-content: center; gap: 6px;
  color: #fff;
  z-index: 2;
  pointer-events: none;
}
.v2-tl-bar-broken-icon {
  width: 18px; height: 18px;
  border-radius: 50%;
  background: #f97777;
  color: #0a0b10;
  display: inline-flex; align-items: center; justify-content: center;
  font: 800 12px/1 'Inter', system-ui, sans-serif;
  box-shadow: 0 1px 4px rgba(0,0,0,0.5);
}
.v2-tl-bar-broken-lbl {
  font: 700 9px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #fff;
  text-shadow: 0 1px 2px rgba(0,0,0,0.7);
  white-space: nowrap;
}
/* v2.0.174 — Retry button on processing_failed bars. Lime fill, dark
   text. Small + tappable. Sits inline with the failure label. The
   broken container needs pointer-events:auto on this child so click
   passes through, but pointer-events:none stays on the rest so the
   bar's normal drag/select still works on the surrounding space. */
.v2-tl-bar-broken { pointer-events: none; }
.v2-tl-bar-retry {
  pointer-events: auto;
  appearance: none;
  margin-left: 4px;
  padding: 2px 8px;
  border: none;
  border-radius: 999px;
  background: #c4ff57;
  color: #0a0b10;
  font: 700 9px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
  box-shadow: 0 1px 4px rgba(0,0,0,0.5);
}
.v2-tl-bar-retry:active { transform: scale(0.96); background: #adff14; }

/* v2.0.171 — async upload "Converting…" placeholder. The bar lives
   in the timeline while the server worker normalizes + proxies the
   uploaded clip. Subtle animated diagonal stripe + lime spinner
   tells the user something's happening without screaming. Same
   structural pattern as .is-broken so layout / hit-targets stay
   consistent. */
.v2-tl-bar.is-processing {
  background:
    repeating-linear-gradient(135deg, rgba(196, 255, 87, 0.14) 0 8px, rgba(196, 255, 87, 0.04) 8px 16px),
    var(--surface-2);
  border-color: rgba(196, 255, 87, 0.4);
  /* Slow gentle pulse on the border so it's visibly "live." */
  animation: v2-tl-processing-pulse 1.8s ease-in-out infinite;
}
.v2-tl-bar.is-processing .v2-tl-bar-thumbs { display: none; }
.v2-tl-bar-processing {
  position: absolute;
  inset: 0;
  display: flex; align-items: center; justify-content: center; gap: 8px;
  color: #fff;
  z-index: 2;
  pointer-events: none;
  overflow: hidden;
}
/* v2.0.173 — real progress fill sliding behind the spinner+label.
   Width = pct%; lime tint over the diagonal-stripe background. The
   width transition smooths the 2s poll cadence so the bar appears to
   crawl forward rather than jump. */
.v2-tl-bar-processing-fill {
  position: absolute;
  left: 0; top: 0; bottom: 0;
  background: rgba(196, 255, 87, 0.18);
  border-right: 1px solid rgba(196, 255, 87, 0.5);
  transition: width 1.8s linear;
  z-index: 0;
}
.v2-tl-bar-processing > * + * { position: relative; z-index: 1; }
.v2-tl-bar-processing-spinner,
.v2-tl-bar-processing-lbl { position: relative; z-index: 1; }
.v2-tl-bar-processing-spinner {
  width: 14px; height: 14px;
  border-radius: 50%;
  border: 2px solid rgba(196, 255, 87, 0.25);
  border-top-color: #c4ff57;
  animation: v2-tl-processing-spin 0.8s linear infinite;
}
.v2-tl-bar-processing-lbl {
  font: 700 9px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  color: #c4ff57;
  text-shadow: 0 1px 2px rgba(0,0,0,0.8);
  white-space: nowrap;
}
@keyframes v2-tl-processing-spin {
  to { transform: rotate(360deg); }
}
@keyframes v2-tl-processing-pulse {
  0%, 100% { border-color: rgba(196, 255, 87, 0.35); }
  50%      { border-color: rgba(196, 255, 87, 0.65); }
}

/* Audio waveform — peak bars rendered as SVG <rect>s scaled to the
   full bar width via preserveAspectRatio:none. Color picked up via
   currentColor on the parent. Path A.3. */
.v2-tl-bar.has-waveform { background: #06070b; }
.v2-tl-bar-waveform {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  color: rgba(6, 182, 212, 0.85);   /* audio cyan */
  pointer-events: none;
  z-index: 0;
}
.v2-tl-bar--audio.has-waveform .v2-tl-bar-lbl {
  position: relative;
  z-index: 1;
  text-shadow: 0 1px 2px rgba(0,0,0,0.8);
}

/* v2.0.145 (Polish R) — Fade-in / fade-out triangular wedges drawn on
   the audio bar to show where the audio ramps. The fade-in wedge
   covers the left edge with a black gradient going from solid → clear;
   fade-out is the mirror on the right. Pure CSS — width sized in
   percentage via inline style. */
.v2-tl-bar-fade {
  position: absolute;
  top: 0; bottom: 0;
  pointer-events: none;
  z-index: 1;
}
.v2-tl-bar-fade--in {
  left: 0;
  background: linear-gradient(90deg, rgba(0, 0, 0, 0.72) 0%, transparent 100%);
  border-top-left-radius: 6px;
  border-bottom-left-radius: 6px;
}
.v2-tl-bar-fade--out {
  right: 0;
  background: linear-gradient(270deg, rgba(0, 0, 0, 0.72) 0%, transparent 100%);
  border-top-right-radius: 6px;
  border-bottom-right-radius: 6px;
}

/* Muted bar: dim the whole thing + grayscale the waveform color. */
.v2-tl-bar.is-muted {
  filter: grayscale(0.7) brightness(0.7);
}
.v2-tl-bar-muted-badge {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: 20px; height: 20px;
  background: rgba(249, 119, 119, 0.85);
  color: #fff;
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  z-index: 2;
  pointer-events: none;
  box-shadow: 0 1px 4px rgba(0,0,0,0.5);
}

/* v2.0.266 — Beat-tick layer. Sits on top of the waveform but below
   text labels. Transition on opacity so toggling beat-snap fades
   ticks in/out smoothly instead of popping. */
.v2-tl-bar-beats {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  pointer-events: none;
  z-index: 1;        /* above .v2-tl-bar-waveform, below label/badges */
  transition: opacity 200ms var(--ease);
}
/* v2.0.266 — BPM badge. Top-right corner of the audio bar — leaves
   the bar's main visual real estate (waveform + label) untouched.
   Pill chip, mono numeric, faint when not selected so it reads as
   metadata not chrome. */
.v2-tl-bar-bpm {
  position: absolute;
  top: 4px; right: 6px;
  z-index: 3;
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 2px 6px;
  font: 700 9px/1 "JetBrains Mono", ui-monospace, monospace;
  letter-spacing: 0.04em;
  color: rgba(255, 255, 255, 0.62);
  background: rgba(0, 0, 0, 0.30);
  border-radius: 8px;
  pointer-events: none;
  transition: color 160ms var(--ease), background 160ms var(--ease);
}
.v2-tl-bar-bpm.is-selected {
  color: #a3ff12;
  background: rgba(0, 0, 0, 0.55);
}
.v2-tl-bar-bpm-unit {
  font-size: 8px;
  opacity: 0.7;
  margin-left: 1px;
}
.v2-tl-bar-bpm-low {
  margin-left: 2px;
  color: #fbbf24;
  font-size: 9px;
}

/* Pre-lift "pressing" state — set IMMEDIATELY on pointerdown by the
   gesture layer so the user gets visible confirmation during the
   250ms long-press wait. Pulses the bar with an accent ring + slight
   inward scale, ramping in over 200ms so it doesn't fight a tap.
   v2.0.56 — keyframe ends at scale(1.03) to match the JS drag-lift
   scale, eliminating the press→drag discontinuity. */
.v2-tl-bar.is-pressing {
  animation: v2-bar-press 240ms var(--ease) forwards;
}
@keyframes v2-bar-press {
  from {
    box-shadow: 0 0 0 0 rgba(163, 255, 18, 0);
    transform: scale(1);
  }
  to {
    box-shadow: 0 0 0 2px rgba(163, 255, 18, 0.45),
                0 6px 16px rgba(0,0,0,0.45);
    transform: scale(1.03);
  }
}

/* ----- Drag visual states -----
   v2.0.154 — Beefier lift: deeper drop shadow + accent ring on the
   dragged bar so it visibly floats above the rail. Adds a subtle
   accent-tinted highlight at the top edge for that "picked up" feel.
   filter:brightness keeps the content slightly lighter to read against
   the dimmed surrounding rail. */
.v2-tl-bar.is-dragging {
  z-index: 50;
  box-shadow:
    0 16px 36px rgba(0,0,0,0.62),
    0 0 0 2px rgba(163, 255, 18, 0.55),
    0 0 18px rgba(163, 255, 18, 0.30),
    inset 0 1px 0 rgba(255,255,255,0.18);
  filter: brightness(1.12) saturate(1.08);
  transition: box-shadow 140ms var(--ease), filter 140ms var(--ease);
}
/* v2.0.355 — Settle z-index lift. Pre-v355 the dragged bar lost its
   z-index:50 the moment is-dragging was removed in onDrop. But the
   reorder mutates scenes.value → Preact moves the bar's DOM node to
   its new index → siblings now paint AFTER it → during the 180ms
   left-slide transition they overlap it visually, making the dropped
   bar appear "behind" the others. Especially obvious when dropping the
   rightmost clip onto the first slot — it slid leftward but rendered
   behind every clip it passed. is-settling stays on top for the
   transition duration. CapCut's clip-reorder feel. */
.v2-tl-bar.is-settling {
  z-index: 49;
}
.v2-tl-bar.is-dragging:active { transform: none; }   /* override the press scale */

/* Smooth drop-down: when is-dragging is removed and JS clears the inline
   transform, the lift-scale animates back to rest. v2.0.56 — also
   transitions left + width so that on release, as JSX writes the new
   `left` (now-correct after the move), the transform offset SHRINKS in
   lock-step. Visual position stays constant during the transition →
   no "release jump." Only kicks in for non-dragging state so the
   per-frame translate during drag stays instantaneous. */
.v2-tl-bar:not(.is-dragging):not(.is-trimming) {
  transition:
    left 180ms var(--ease),
    width 180ms var(--ease),
    transform 180ms var(--ease),
    box-shadow 180ms var(--ease);
}

/* v2.0.362 — is-reorder-shuffling: applied to NON-source bars during
   pack-tight continuous shuffle drag. Per-frame transform writes need
   immediate visual response (no 180ms lag from the default transition
   rule), so we kill all transitions on the shuffling bars. Removed on
   drop, after which the default transition takes over for the settle
   animation. */
.v2-tl-bar.is-reorder-shuffling {
  transition: none !important;
}

/* is-drop-target: a candidate target that the pointer is over,
   but not yet armed. Subtle outline. */
.v2-tl-bar.is-drop-target {
  box-shadow: 0 0 0 2px rgba(163, 255, 18, 0.35), 0 4px 12px rgba(0,0,0,0.3);
  transition: box-shadow 120ms var(--ease);
}

/* is-drop-armed: pointer has hovered over this target for
   DROP_ARM_MS continuously. Strong outline + slight scale.
   On release, this is the target that gets the source. */
.v2-tl-bar.is-drop-armed {
  box-shadow: 0 0 0 3px var(--accent), 0 6px 16px rgba(163, 255, 18, 0.25);
  transform: scale(1.04);
  transition: box-shadow 140ms var(--ease), transform 140ms var(--ease);
}
.v2-tl-bar.is-drop-armed::after {
  content: 'MOVE HERE';
  position: absolute;
  top: -22px; left: 50%;
  transform: translateX(-50%);
  font: 800 9px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.16em;
  color: var(--accent);
  background: var(--bg);
  padding: 4px 6px;
  border-radius: 4px;
  border: 1px solid var(--accent);
  pointer-events: none;
  white-space: nowrap;
}

/* ----- Selected clip + trim handles ----- */
/* Tap a clip → it gets is-selected → handles render at its edges.
   Handles intentionally extend slightly above and below the bar so
   they're easy to grab without obscuring the bar's content.       */
.v2-tl-bar.is-selected {
  box-shadow: 0 0 0 2px var(--accent), 0 4px 16px rgba(163, 255, 18, 0.18);
  z-index: 10;
}
/* Trim handle — RESTORED original 14px visible accent block
   (matches the look users tested earlier and asked us to keep)
   sitting INSIDE a 28px-wide invisible hit zone (finger touches
   anywhere within 14px of the bar edge still register as a grab).
   So: same look as before, ~2× the touch area. */
.v2-tl-bar-handle {
  position: absolute;
  top: -6px; bottom: -6px;
  width: 22px;                    /* invisible hit area — between old 14 and v2.0.26's 28 */
  background: transparent;
  cursor: ew-resize;
  display: flex; align-items: center; justify-content: center;
  z-index: 12;
  touch-action: none;
}
.v2-tl-bar-handle::after {
  /* v2.0.155 — Visible accent pill. Lime fill + brighter glow makes
     the grab affordance unmissable. The interior grip lines (::before)
     give the handle a "grippy" texture that says "I'm draggable."  */
  content: '';
  width: 14px;
  height: 100%;
  background: linear-gradient(180deg, #c9ff4d 0%, var(--accent) 100%);
  border-radius: 5px;
  box-shadow:
    0 0 8px rgba(163, 255, 18, 0.55),
    0 0 0 1px rgba(10, 11, 16, 0.45) inset;
  transition: transform 140ms cubic-bezier(0.2, 1.0, 0.4, 1), box-shadow var(--t-fast) var(--ease);
}
.v2-tl-bar-handle:active::after {
  transform: scaleY(0.96);
  box-shadow:
    0 0 12px rgba(163, 255, 18, 0.85),
    0 0 0 1px rgba(10, 11, 16, 0.55) inset;
}
.v2-tl-bar-handle::before {
  /* v2.0.155 — TWO parallel grip lines (was single) for a real
     "barbell grip" texture. CapCut's trim handles use the same
     vertical-line motif. Lines stop short of the pill's rounded
     ends so they look intentional, not clipped. */
  content: '';
  position: absolute;
  width: 5px; height: 14px;
  border-left:  1.5px solid rgba(10, 11, 16, 0.55);
  border-right: 1.5px solid rgba(10, 11, 16, 0.55);
  z-index: 1;
  pointer-events: none;
}
/* Position: invisible hit zone centered on the bar's edge, so the
   visible pill (14px, centered in the 22px box) lands at the bar
   edge — looks like the old handle with 4px of extra grab tolerance
   on each side. */
.v2-tl-bar-handle--left  { left: -11px; }
.v2-tl-bar-handle--right { right: -11px; }

/* Smooth snap-back when video bars re-flow after a left-trim commit
   (the bar visually slides during drag, then snaps to its sequential
   position; this transition smooths that jump). is-trimming exemption
   keeps the active drag itself instantaneous. */
.v2-tl-bar--video {
  transition: left 200ms var(--ease), width 200ms var(--ease);
}
.v2-tl-bar--video.is-trimming,
.v2-tl-bar--video.is-dragging {
  transition: none;
}

.v2-tl-bar.is-trimming {
  box-shadow: 0 0 0 2px var(--accent), 0 6px 18px rgba(163, 255, 18, 0.22);
  z-index: 11;
}

/* ----- Snap guide ----- */
/* Vertical accent line drawn at the snap target's time during a
   drag/trim. Spans the full rail body so it visually unifies all
   three tracks at the snapped time. Different colors per snap kind
   so the user knows what they snapped to. */
/* v2.0.157 — Pinch-zoom floating indicator. Centered above the
   timeline rail; visible only while two-finger pinching. Shows the
   current zoom level + raw pixel-per-second value. Flashes red on the
   is-min / is-max class when the user has hit the zoom bounds so they
   know there's no more room to expand/contract. */
.v2-tl-zoom-chip {
  position: absolute;
  top: 8px; left: 50%;
  transform: translateX(-50%);
  display: inline-flex; align-items: baseline; gap: 8px;
  padding: 8px 14px;
  background: rgba(10, 11, 16, 0.92);
  border: 1.5px solid rgba(163, 255, 18, 0.55);
  border-radius: 999px;
  box-shadow:
    0 4px 12px rgba(0, 0, 0, 0.5),
    0 0 14px rgba(163, 255, 18, 0.25);
  color: var(--text);
  z-index: 50;
  pointer-events: none;
  animation: v2-zoom-chip-in 180ms var(--ease) 1;
}
@keyframes v2-zoom-chip-in {
  from { opacity: 0; transform: translate(-50%, -6px) scale(0.92); }
  to   { opacity: 1; transform: translate(-50%, 0) scale(1); }
}
.v2-tl-zoom-chip-val {
  font: 800 14px/1 'JetBrains Mono', ui-monospace, monospace;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
  letter-spacing: 0.02em;
}
.v2-tl-zoom-chip-meta {
  font: 600 10px/1 'JetBrains Mono', ui-monospace, monospace;
  color: var(--text-3);
  letter-spacing: 0.04em;
}
.v2-tl-zoom-chip.is-min,
.v2-tl-zoom-chip.is-max {
  border-color: rgba(249, 119, 119, 0.65);
  box-shadow:
    0 4px 12px rgba(0, 0, 0, 0.5),
    0 0 14px rgba(249, 119, 119, 0.35);
  animation: v2-zoom-chip-in 180ms var(--ease) 1, v2-zoom-chip-bound 600ms ease-out 1;
}
.v2-tl-zoom-chip.is-min .v2-tl-zoom-chip-val,
.v2-tl-zoom-chip.is-max .v2-tl-zoom-chip-val { color: #f97777; }
@keyframes v2-zoom-chip-bound {
  0%   { transform: translate(-50%, 0) scale(1.06); }
  50%  { transform: translate(-50%, 0) scale(0.98); }
  100% { transform: translate(-50%, 0) scale(1); }
}

/* v2.0.152 — Magnetic snap visual: brighter strobe on engage, glowing
   capsule end-caps (top + bottom), identity chip floating above so the
   user sees WHAT the bar locked onto. The chip animates in 220ms after
   the line settles, then strobes opacity to reinforce "I'm latched." */
.v2-tl-snap {
  position: absolute;
  top: 0; bottom: 0;
  width: 2px;
  background: var(--accent);
  z-index: 8;
  pointer-events: none;
  box-shadow:
    0 0 8px rgba(163, 255, 18, 0.85),
    0 0 24px rgba(163, 255, 18, 0.40);
  border-radius: 1px;
  animation: v2-snap-strobe 320ms var(--ease) 1;
  transform-origin: top center;
}
/* Strobe envelope: pop in tall + bright, contract slightly, settle. */
@keyframes v2-snap-strobe {
  0%   { transform: scaleY(0.4) scaleX(2.2); opacity: 0; }
  35%  { transform: scaleY(1)   scaleX(2.2); opacity: 1; }
  60%  { transform: scaleY(1)   scaleX(1);   opacity: 1; }
  100% { transform: scaleY(1)   scaleX(1);   opacity: 1; }
}
/* End caps: small accent dots at top + bottom so the line reads as a
   "capsule" not a hairline. Subtle visual weight that says "this is
   the snap target," not a ruler line. */
.v2-tl-snap::before,
.v2-tl-snap::after {
  content: '';
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  width: 7px; height: 7px;
  border-radius: 50%;
  background: inherit;
  box-shadow: 0 0 6px currentColor;
}
.v2-tl-snap::before { top: -3px; }
.v2-tl-snap::after  { bottom: -3px; }
/* Identity chip floats above the top end-cap. */
.v2-tl-snap-chip {
  position: absolute;
  top: -22px;
  left: 50%;
  transform: translateX(-50%);
  padding: 3px 6px;
  background: var(--accent);
  color: var(--on-accent);
  border-radius: 4px;
  font: 800 9px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.08em;
  white-space: nowrap;
  box-shadow: 0 2px 6px rgba(0,0,0,0.5);
  animation: v2-snap-chip-in 320ms var(--ease) 1;
}
@keyframes v2-snap-chip-in {
  0%   { opacity: 0; transform: translate(-50%, 4px) scale(0.7); }
  50%  { opacity: 1; transform: translate(-50%, 0) scale(1.06); }
  100% { opacity: 1; transform: translate(-50%, 0) scale(1); }
}

.v2-tl-snap--playhead   { background: var(--accent); color: var(--accent); }
.v2-tl-snap--playhead   .v2-tl-snap-chip { background: var(--accent); color: var(--on-accent); }
.v2-tl-snap--clip-edge  {
  background: #5aa1ff; color: #5aa1ff;
  box-shadow: 0 0 8px rgba(90, 161, 255, 0.85), 0 0 24px rgba(90, 161, 255, 0.40);
}
.v2-tl-snap--clip-edge  .v2-tl-snap-chip { background: #5aa1ff; color: #061421; }
.v2-tl-snap--overlay    {
  background: var(--accent-2); color: var(--accent-2);
  box-shadow: 0 0 8px rgba(255, 43, 214, 0.85), 0 0 24px rgba(255, 43, 214, 0.40);
}
.v2-tl-snap--overlay    .v2-tl-snap-chip { background: var(--accent-2); color: #fff; }
.v2-tl-snap--start,
.v2-tl-snap--end        {
  background: var(--text-2); color: var(--text-2);
  box-shadow: 0 0 8px rgba(255, 255, 255, 0.55), 0 0 24px rgba(255, 255, 255, 0.22);
}
.v2-tl-snap--start .v2-tl-snap-chip,
.v2-tl-snap--end   .v2-tl-snap-chip { background: var(--text); color: var(--bg); }

/* ----- Lock-to-clip toggle -----
   v2.0.153 — Matches the new delete chip shape (24px circle, lifted
   shadow) so the two chips read as a pair flanking the bar's top.
   Active state gets the same lime glow used elsewhere. */
.v2-tl-bar-lock {
  position: absolute;
  top: -28px;
  left: -2px;
  width: 24px; height: 24px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--surface-2);
  color: var(--text-2);
  border: 2px solid var(--bg);
  border-radius: 50%;
  cursor: pointer;
  z-index: 13;
  touch-action: manipulation;
  box-shadow: 0 2px 6px rgba(0,0,0,0.5);
  transition:
    transform 140ms cubic-bezier(0.2, 1.0, 0.4, 1),
    background var(--t-fast) var(--ease),
    color var(--t-fast) var(--ease),
    box-shadow var(--t-fast) var(--ease);
}
.v2-tl-bar-lock:active { transform: scale(0.88); }
.v2-tl-bar-lock.is-on {
  background: var(--accent);
  color: var(--on-accent);
  box-shadow:
    0 2px 6px rgba(0,0,0,0.5),
    0 0 10px rgba(163, 255, 18, 0.45);
}

/* Locked-bar visual: subtle dashed top edge. Doesn't change layout,
   just signals "I'm linked to a clip." */
.v2-tl-bar.is-locked {
  border-top-style: dashed;
  border-top-color: rgba(255, 255, 255, 0.55);
}
.v2-tl-bar--video {
  /* takes inline --bar-color */
  top: 2px; bottom: 2px;
}
.v2-tl-bar--audio {
  background: linear-gradient(180deg, #06b6d4 0%, #0891b2 100%);
  top: 3px; bottom: 3px;
}
/* v2.0.299 — KF diamond strip on audio bars (parity with PiP +
   text bars). Audio is where volume envelopes matter most. */
.v2-tl-bar--audio.is-keyframed {
  box-shadow: inset 0 0 0 1px rgba(255, 215, 0, 0.55);
}
.v2-tl-bar--audio.is-keyframed.is-selected {
  box-shadow: 0 0 0 2px rgba(6, 182, 212, 0.85),
              inset 0 0 0 1px rgba(255, 215, 0, 0.50);
}
.v2-tl-bar--text {
  background: linear-gradient(180deg, #f59e0b 0%, #d97706 100%);
  top: 3px; bottom: 3px;
}
/* v2.0.298 — Keyframe diamond strip on text bars (parity with the
   v291 PiP bar treatment). The .v2-tl-bar-kf-strip + .v2-tl-bar-kf-dot
   rules in the PiP CSS block apply universally — only the parent
   glow rule needs duplication. */
.v2-tl-bar--text.is-keyframed {
  box-shadow: inset 0 0 0 1px rgba(255, 215, 0, 0.55);
}
.v2-tl-bar--text.is-keyframed.is-selected {
  box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.85),
              inset 0 0 0 1px rgba(255, 215, 0, 0.50);
}
/* v2.0.104 (A1.5) — Auto-caption bars are visually distinct so the user
   can see at a glance that "this is a group from auto-captions" vs. a
   one-off text overlay. Slight violet tint + left accent stripe to
   echo CapCut's caption-track convention. The "CC" badge appears only
   on bars wide enough to fit it (>= 36px). */
.v2-tl-bar--text.is-caption {
  background: linear-gradient(180deg, #a855f7 0%, #7e22ce 100%);
  box-shadow: inset 3px 0 0 0 #fef08a;
}
/* v2.0.108 polish — CC badge is now always visible (not just on
   selected/hovered). Bars too narrow to fit the label clip the badge
   via overflow:hidden on the bar itself. Removing the conditional
   reveal makes the timeline instantly readable: at-a-glance "these
   are auto-captions" without having to tap a bar to check. */
.v2-tl-bar--text.is-caption::before {
  content: 'CC';
  position: absolute;
  left: 6px; top: 50%;
  transform: translateY(-50%);
  font: 800 9px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  color: #fef08a;
  pointer-events: none;
  opacity: 0.9;
}
.v2-tl-bar--text.is-caption.is-selected::before {
  opacity: 1;
  text-shadow: 0 0 6px rgba(254, 240, 138, 0.6);
}
.v2-tl-bar--text.is-caption .v2-tl-bar-lbl {
  /* Nudge label right so it doesn't crash into the CC stripe. */
  padding-left: 18px;
}
/* v2.0.108 polish — group highlight. When one caption bar is selected,
   every sibling in the same _caption_session gets a soft yellow ring
   so the user instantly sees "edits to this bar style every bar shown
   with this ring." CapCut shows the relationship the same way. */
.v2-tl-bar--text.is-caption-sibling {
  box-shadow:
    inset 3px 0 0 0 #fef08a,
    0 0 0 1px rgba(254, 240, 138, 0.45);
  /* Subtle pulse so the connection feels alive rather than static.
     Tied to the bar's existing animation namespace. */
  animation: v2-caption-sibling-pulse 1800ms ease-in-out infinite;
}
@keyframes v2-caption-sibling-pulse {
  0%,100% { box-shadow: inset 3px 0 0 0 #fef08a, 0 0 0 1px rgba(254, 240, 138, 0.30); }
  50%     { box-shadow: inset 3px 0 0 0 #fef08a, 0 0 0 2px rgba(254, 240, 138, 0.65); }
}
.v2-tl-bar-lbl {
  font: 700 11px/1 'Inter', system-ui, sans-serif;
  color: rgba(255,255,255,0.95);
  letter-spacing: 0.01em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  text-shadow: 0 1px 2px rgba(0,0,0,0.4);
  pointer-events: none;
}

/* ----- Lane stack ----- */
/* In lane mode (text/audio focused), each overlay of the focused
   kind gets its own .v2-tl-track row inside this .v2-tl-stack
   wrapper. The "Add" button at the bottom is the new affordance.
   Video timeline stays rendered above this stack — see the video
   track in TimelineDefault.                                       */
.v2-tl-stack {
  display: flex;
  flex-direction: column;
  margin-top: 4px;
}
.v2-tl-stack--audio .v2-tl-track--audio { border-left: 3px solid #06b6d4; }
.v2-tl-stack--text  .v2-tl-track--text  { border-left: 3px solid #f59e0b; }

.v2-tl-lane-add {
  display: inline-flex; align-items: center; gap: 6px;
  margin-top: 6px;
  padding: 8px 12px;
  align-self: flex-start;
  background: var(--surface);
  border: 1px dashed var(--border-2);
  border-radius: 8px;
  color: var(--text-2);
  font: 600 11px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.02em;
  position: sticky;
  left: 64px;
  z-index: 4;
}
.v2-tl-lane-add:active {
  background: var(--surface-2);
  border-style: solid;
  border-color: var(--accent);
  color: var(--accent);
}

/* When in lane mode, give the timeline area more vertical room so
   stacked rows don't crowd. Triggered by the body of TimelineDefault
   wrapping itself in this class — see main.js. Falls back to
   scrolling if content STILL exceeds (very many overlays).      */
.v2-edit:has(.v2-tl-stack) { --tl-h: 240px; }
.v2-edit:has(.v2-tl-stack) .v2-tl-rail { overflow-y: auto; }

/* ----- Bottom ribbon (horizontal scroll, icon+label) -----
   v2.0.140 — Edge fades so the user knows there's more content off
   to the right. The ::before/::after pseudos can't go on a scroll
   container directly (they'd scroll with the content), so we use a
   wrapper trick: the ribbon itself is the scroll viewport; mask-image
   gradient fades its content at left + right edges. Falls back to no
   fade in browsers without mask-image (older Safari) — purely visual. */
.v2-ribbon {
  grid-row: 5;
  display: flex;
  background: var(--bg);
  border-top: 1px solid var(--border);
  z-index: var(--z-ribbon);
  overflow-x: auto; overflow-y: hidden;
  scrollbar-width: none;
  scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
  padding: 2px var(--s2);
  gap: 4px;
}
.v2-ribbon::-webkit-scrollbar { display: none; }
/* v2.0.473 — scroll-aware edge fade (was a static both-edge mask). Fades ONLY
   the side(s) with more off-screen content, so a cut-off chip means "scroll
   this way" → all 12+ actions are discoverable. JS sets is-fade-l / is-fade-r.
   A short, non-scrollable ribbon gets NO fade (nothing is cut off). */
.v2-ribbon.is-fade-r {
  -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 30px), transparent 100%);
          mask-image: linear-gradient(90deg, #000 calc(100% - 30px), transparent 100%);
}
.v2-ribbon.is-fade-l {
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 30px);
          mask-image: linear-gradient(90deg, transparent 0, #000 30px);
}
.v2-ribbon.is-fade-l.is-fade-r {
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 30px, #000 calc(100% - 30px), transparent 100%);
          mask-image: linear-gradient(90deg, transparent 0, #000 30px, #000 calc(100% - 30px), transparent 100%);
}
.v2-ribbon-item {
  flex: 0 0 auto;
  min-width: 60px; height: 58px;
  position: relative;        /* anchor for the ::after active dot */
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 4px;
  border-radius: var(--r-sm);
  color: var(--text-2);
  scroll-snap-align: start;
  transition:
    background 140ms var(--ease),
    color 140ms var(--ease),
    transform 140ms cubic-bezier(0.2, 1.0, 0.4, 1);
  cursor: pointer;
}
/* v2.0.140 — bouncier press scale so taps feel kinetic, not flat. */
.v2-ribbon-item:active { background: var(--surface-2); transform: scale(0.94); }
.v2-ribbon-item.is-primary { color: var(--accent); }
.v2-ribbon-item.is-active {
  color: var(--accent);
  background: rgba(163, 255, 18, 0.08);
}
.v2-ribbon-item.is-active .v2-ribbon-lbl { color: var(--accent); }
/* v2.0.140 — Active state gets a small lime dot under the label,
   matching CapCut's "you're in this tool" indicator. Pop in via
   transform so the swap feels alive. */
.v2-ribbon-item.is-active::after {
  content: '';
  position: absolute;
  bottom: 3px;
  left: 50%;
  width: 4px; height: 4px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 4px rgba(163, 255, 18, 0.7);
  transform: translateX(-50%) scale(1);
  animation: v2-ribbon-dot-in 200ms var(--ease) 1;
}
@keyframes v2-ribbon-dot-in {
  0%   { transform: translateX(-50%) scale(0); opacity: 0; }
  60%  { transform: translateX(-50%) scale(1.4); opacity: 1; }
  100% { transform: translateX(-50%) scale(1); }
}
.v2-ribbon-lbl {
  font: 600 10px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.02em;
}
.v2-ribbon-divider {
  flex: 0 0 auto;
  width: 1px; height: 32px;
  margin: auto 4px;
  background: var(--border);
}

/* Context ribbon — appears when a timeline bar is selected.
   Justified center so 2-3 tools breathe rather than left-aligning.
   Gets a subtle accent top edge to telegraph that it's contextual. */
.v2-ribbon.v2-ribbon--context {
  /* v2.0.442 — `safe center` not plain `center`. Plain centering an
     OVERFLOWING flex row pushes the first items off the LEFT edge, and
     scrollLeft can't go negative — so Split/Duplicate (the leading,
     most-used edit actions) became permanently unreachable on the
     7–8-item video ribbon. This was the real "edit functions are hard
     to find" bug. `safe` falls back to start-alignment whenever the row
     overflows, keeping the leading items reachable; short ribbons
     (sticker/PiP = 4 items) still center as before. */
  justify-content: safe center;
  background: linear-gradient(180deg, rgba(163, 255, 18, 0.04) 0%, var(--bg) 60%);
  border-top: 1px solid rgba(163, 255, 18, 0.18);
  animation: v2-ribbon-slide 220ms var(--ease) 1;
}
@keyframes v2-ribbon-slide {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0);   }
}
.v2-ribbon--context .v2-ribbon-item {
  min-width: 76px;
}

/* Danger variant — Delete action. Red accent, never on by default. */
.v2-ribbon-item--danger {
  color: #f97777;
}
.v2-ribbon-item--danger:active {
  background: rgba(249, 119, 119, 0.10);
}
.v2-ribbon-item--danger .v2-ribbon-lbl {
  color: #f97777;
}

/* v2.0.394 [4/5] — Pro chip on AI-gated ribbon buttons (free tier).
   The button stays visible + tappable; tap fires the upsell toast.
   `.is-pro` is the marker class; `.v2-ribbon-pro` is the chip itself
   positioned top-right of the icon. Subtle gold treatment so it reads
   as "premium" without being garish next to the lime accent. */
.v2-ribbon-item.is-pro {
  /* Soften the icon + label so the eye lands on the Pro chip first
     and the button as a whole reads as "locked but available." */
  color: var(--text-3);
}
.v2-ribbon-item.is-pro .v2-ribbon-lbl {
  color: var(--text-3);
}
.v2-ribbon-pro {
  position: absolute;
  top: 4px;
  right: 6px;
  padding: 2px 5px;
  border-radius: 4px;
  font: 800 8px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  /* Gold gradient distinct from the accent green so the upsell
     reads as "premium tier" not "active state." */
  background: linear-gradient(135deg, #d4a64a 0%, #b27e1f 100%);
  color: #1a1208;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45), 0 0 6px rgba(212, 166, 74, 0.35);
  pointer-events: none;
  /* Render above the icon. */
  z-index: 1;
}
/* Ribbon item is position:relative already (v2-ribbon-item is-active
   uses ::after for the underline) — relying on that. The base
   .v2-ribbon-item already sets position:relative (see line 5704). */

/* =========================================================
   Tool sheet — v2.0.197. CapCut-style bottom sheet that hosts
   every sub-tool panel (Volume, Speed, Style, Filter, Anim, Fade,
   Transition). Replaces the fixed-height `.v2-secondary` grid row
   which crushed tab + chip content into a 56–130px strip with no
   scroll — content was clipped off the right edge and stuck in
   place. The sheet:

     * Floats ABOVE the ribbon (position: fixed + z-sheet) so the
       ribbon stays visible behind it. User sees which tool is
       active and can switch by tapping a different ribbon item.
     * Has a vertically scrollable `.v2-tool-sheet-body` so long
       content (Filter presets, Caption styles, Text shadow) is
       always reachable.
     * Optional horizontal tab strip `.v2-tool-sheet-tabs`.
     * Swipe down on the header to dismiss (wireToolSheetSwipe).
   ========================================================= */
.v2-tool-sheet {
  position: fixed;
  left: 0; right: 0;
  /* Sit ABOVE the ribbon, not over it. Ribbon stays visible + tappable
     so the user can switch tools by tapping a different ribbon item
     without an extra close step (CapCut convention). */
  bottom: calc(var(--ribbon-h) + env(safe-area-inset-bottom, 0px));
  z-index: var(--z-sheet);
  background: rgba(20, 22, 28, 0.96);
  backdrop-filter: blur(28px) saturate(160%);
  -webkit-backdrop-filter: blur(28px) saturate(160%);
  border-top: 1px solid rgba(163, 255, 18, 0.22);
  border-radius: 18px 18px 0 0;
  box-shadow: 0 -16px 48px rgba(0,0,0,0.55),
              0 -1px 0 rgba(255,255,255,0.04) inset;
  display: flex; flex-direction: column;
  /* Sheet is allowed up to ~55% of screen so canvas + timeline stay
     visible behind it. Min-height keeps a useful body even for tiny
     panels (Volume / Speed). */
  max-height: 55dvh;
  min-height: 220px;
  animation: v2-tool-sheet-up 240ms cubic-bezier(.2, .8, .2, 1);
}
@keyframes v2-tool-sheet-up {
  from { transform: translateY(100%); }
  to   { transform: translateY(0);    }
}
.v2-tool-sheet-head {
  position: relative;
  display: flex; flex-direction: column; align-items: center;
  padding: 6px var(--s3) 8px;
  flex: 0 0 auto;
  cursor: grab;
  touch-action: none;           /* JS owns swipe-down */
  user-select: none;
  -webkit-user-select: none;
}
.v2-tool-sheet-head:active { cursor: grabbing; }
.v2-tool-sheet-grab {
  width: 40px; height: 4px;
  border-radius: 2px;
  background: rgba(255,255,255,0.22);
  margin: 2px auto 8px;
  flex: 0 0 auto;
}
.v2-tool-sheet-title {
  font: 700 11px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-2);
  text-align: center;
}
.v2-tool-sheet-close {
  position: absolute;
  right: var(--s3); top: 50%;
  transform: translateY(-30%);
  width: 32px; height: 32px;
  border-radius: 50%;
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--border-2);
  display: inline-flex; align-items: center; justify-content: center;
  touch-action: manipulation;
}
.v2-tool-sheet-close:active { transform: translateY(-30%) scale(0.94); }
/* v2.0.437 — explicit Done button (opt-in via ToolSheet confirmLabel). A clear
   primary commit affordance so panels with live auto-apply don't feel
   uncertain ("no done button" / "auto-edits as I press buttons"). */
.v2-tool-sheet-done {
  position: absolute;
  right: var(--s3); top: 50%;
  transform: translateY(-50%);
  height: 32px; padding: 0 14px;
  border-radius: 16px;
  background: var(--accent, #a3ff12);
  color: #0b0c10;
  border: none;
  font-weight: 700; font-size: 13px;
  display: inline-flex; align-items: center; gap: 5px;
  touch-action: manipulation;
}
.v2-tool-sheet-done:active { transform: translateY(-50%) scale(0.95); }
.v2-tool-sheet-tabs {
  display: flex; gap: 6px;
  padding: 0 var(--s3) 10px;
  flex: 0 0 auto;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  border-bottom: 1px solid rgba(255,255,255,0.04);
  /* v2.0.218 — Edge fade on the trailing edge so the user sees there
     are more tabs to scroll to. v216 ("Anim") + v217 ("Blend") pushed
     the TextStylePanel tab count from 7 to 9, which doesn't fit on
     360px phones — without the fade, the new tabs look invisible and
     users assume they don't exist. We fade only the trailing edge
     (not leading) so the "active tab" indicator on the left never
     gets dimmed. */
  -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 24px), transparent);
          mask-image: linear-gradient(90deg, #000 calc(100% - 24px), transparent);
}
.v2-tool-sheet-tabs::-webkit-scrollbar { display: none; }
.v2-tool-sheet-body {
  flex: 1 1 auto;
  overflow-y: auto;
  overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
  padding: 12px var(--s3) 16px;
  scrollbar-width: thin;
  scrollbar-color: rgba(255,255,255,0.18) transparent;
}
.v2-tool-sheet-body::-webkit-scrollbar { width: 6px; }
.v2-tool-sheet-body::-webkit-scrollbar-thumb {
  background: rgba(255,255,255,0.18);
  border-radius: 3px;
}

/* Section block within the sheet body — title + content. */
.v2-tool-sec {
  display: flex; flex-direction: column;
  gap: 10px;
  padding: 8px 0;
}
.v2-tool-sec + .v2-tool-sec {
  border-top: 1px solid rgba(255,255,255,0.04);
  padding-top: 14px;
  margin-top: 4px;
}
.v2-tool-sec-lbl {
  font: 700 9px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-3);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
/* v2.0.267 — Right-aligned status hint that lives in the section
   label row (e.g. "on" / "off" status for auto-duck). Quiet so it
   reads as metadata; the section content carries the actual UI. */
.v2-tool-sec-hint {
  font: 600 9px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-2);
  opacity: 0.7;
}
.v2-tool-sec-note {
  margin-top: 10px;
  padding: 8px 10px;
  background: rgba(251, 191, 36, 0.08);
  border-left: 2px solid #fbbf24;
  border-radius: 4px;
  font: 500 11px/1.4 'Inter', system-ui, sans-serif;
  color: var(--text-2);
}

/* v2.0.271 — Audio library sheet (TikTok-style search across
   Pixabay/Freesound/Jamendo). Compact list of track cards with
   preview + add buttons. License badge visible on every card so
   the user always knows what they're agreeing to. */
.v2-tool-audiolib { max-height: 80dvh; }
.v2-audiolib-search {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border-2);
  border-radius: 999px;
  margin-bottom: 12px;
  color: var(--text-2);
}
.v2-audiolib-search-input {
  flex: 1;
  background: none;
  border: 0;
  outline: 0;
  color: var(--text);
  font: 500 14px/1.2 'Inter', system-ui, sans-serif;
  -webkit-user-select: text; user-select: text;
}
.v2-audiolib-search-input::-webkit-search-cancel-button { display: none; }
.v2-audiolib-search-clear {
  width: 22px; height: 22px;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,0.06);
  border-radius: 50%;
  color: var(--text-2);
}
.v2-audiolib-tabs {
  display: flex;
  gap: 6px;
  margin-bottom: 10px;
}
.v2-audiolib-tab {
  flex: 1;
  padding: 8px 10px;
  background: var(--surface-2);
  border: 1px solid transparent;
  border-radius: 999px;
  font: 600 12px/1 'Inter', system-ui, sans-serif;
  color: var(--text-2);
  transition: background 140ms var(--ease), color 140ms var(--ease), border-color 140ms var(--ease);
}
.v2-audiolib-tab.is-active {
  background: rgba(163, 255, 18, 0.12);
  border-color: rgba(163, 255, 18, 0.4);
  color: var(--accent);
}
.v2-audiolib-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin-bottom: 12px;
}
.v2-audiolib-filter-lbl {
  font: 700 9px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-3);
  margin: 0 2px;
}
.v2-audiolib-filter-sep {
  width: 1px; height: 16px;
  background: rgba(255,255,255,0.10);
  margin: 0 2px;
}
.v2-audiolib-chip {
  padding: 6px 10px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 999px;
  font: 600 11px/1 'Inter', system-ui, sans-serif;
  color: var(--text-2);
  transition: background 140ms var(--ease), color 140ms var(--ease), border-color 140ms var(--ease);
}
.v2-audiolib-chip.is-on {
  background: rgba(163, 255, 18, 0.12);
  border-color: var(--accent);
  color: var(--accent);
}
.v2-audiolib-loading {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 24px;
  color: var(--text-2);
  font: 600 12px/1 'Inter', system-ui, sans-serif;
}
.v2-audiolib-spinner {
  width: 18px; height: 18px;
  border-radius: 50%;
  border: 2px solid rgba(168, 85, 247, 0.18);
  border-top-color: #a855f7;
  animation: v2-spin 720ms linear infinite;
}
.v2-audiolib-empty,
.v2-audiolib-err {
  padding: 18px;
  text-align: center;
  color: var(--text-2);
}
.v2-audiolib-err {
  background: rgba(255, 107, 107, 0.08);
  border-left: 2px solid #ff6b6b;
  text-align: left;
  border-radius: 4px;
  margin: 8px 0;
  padding: 8px 10px;
  font: 500 11px/1.4 'Inter', system-ui, sans-serif;
}
.v2-audiolib-empty-icon {
  width: 56px; height: 56px;
  margin: 0 auto 12px;
  border-radius: 50%;
  background: rgba(255,255,255,0.04);
  display: inline-flex;
  align-items: center; justify-content: center;
  color: var(--text-3);
}
.v2-audiolib-empty-title {
  font: 700 14px/1.2 'Inter', system-ui, sans-serif;
  color: var(--text);
  margin-bottom: 4px;
}
.v2-audiolib-empty-sub {
  font: 500 12px/1.4 'Inter', system-ui, sans-serif;
  color: var(--text-2);
}
.v2-audiolib-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  overflow-y: auto;
  max-height: 56dvh;
}
.v2-audiolib-card {
  display: grid;
  grid-template-columns: 36px 1fr auto;
  align-items: center;
  gap: 10px;
  padding: 10px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
}
/* v2.0.272 — Actions cluster (fav + add). Grouped so the grid's third
   column accommodates both without the card jumping width. */
.v2-audiolib-actions {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.v2-audiolib-fav {
  width: 30px; height: 30px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center; justify-content: center;
  background: rgba(255,255,255,0.04);
  color: var(--text-3);
  transition: background 140ms var(--ease), color 140ms var(--ease);
}
.v2-audiolib-fav:hover {
  background: rgba(255,255,255,0.08);
  color: var(--text-2);
}
.v2-audiolib-fav.is-on {
  background: rgba(251, 191, 36, 0.16);
  color: #fbbf24;
}
/* v2.0.272 — Smart-suggest BPM chip ("Match 120 BPM"). Wider than
   bin chips because of the icon + longer label; accent-tinted even
   when off so it reads as a recommendation, not a normal filter. */
.v2-audiolib-chip--match {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: rgba(163, 255, 18, 0.06);
  border-color: rgba(163, 255, 18, 0.22);
  color: var(--accent);
}
.v2-audiolib-chip--match.is-on {
  background: rgba(163, 255, 18, 0.18);
  border-color: var(--accent);
}
/* v2.0.272 — Smart preview + add toggles row. Pill-shaped switch-
   like buttons; the small dot at the leading edge fills + glows when
   on so the toggle state reads at a glance. */
.v2-audiolib-smart {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 10px;
}
.v2-audiolib-smart-toggle {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--text-2);
  font: 600 11px/1 'Inter', system-ui, sans-serif;
  transition: background 140ms var(--ease), color 140ms var(--ease), border-color 140ms var(--ease);
}
.v2-audiolib-smart-toggle.is-on {
  background: rgba(163, 255, 18, 0.12);
  border-color: var(--accent);
  color: var(--accent);
}
.v2-audiolib-smart-dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: rgba(255,255,255,0.18);
  transition: background 140ms var(--ease), box-shadow 140ms var(--ease);
}
/* v2.0.369 — Mood chips: horizontal-scroll row of curated browse
   buttons. Each chip = emoji + label. CapCut-style discovery layer
   above the search input. Touch-scroll the row; overflow hidden so
   the rest of the sheet doesn't jump. */
.v2-audiolib-moods {
  display: flex;
  gap: 6px;
  padding: 4px 10px 8px;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.v2-audiolib-moods::-webkit-scrollbar { display: none; }
.v2-audiolib-mood {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 12px;
  border-radius: 16px;
  background: rgba(255,255,255,0.06);
  border: 1px solid rgba(255,255,255,0.10);
  color: rgba(255,255,255,0.88);
  font: 600 12px/1 'Inter', system-ui, sans-serif;
  cursor: pointer;
  scroll-snap-align: start;
  transition: background 120ms var(--ease), border-color 120ms var(--ease), color 120ms var(--ease);
  -webkit-tap-highlight-color: transparent;
  white-space: nowrap;
}
.v2-audiolib-mood:active { transform: scale(0.96); }
.v2-audiolib-mood.is-active {
  background: rgba(163,255,18,0.18);
  border-color: rgba(163,255,18,0.55);
  color: #a3ff12;
}
.v2-audiolib-mood-emoji { font-size: 14px; line-height: 1; }
.v2-audiolib-mood-label { line-height: 1; }

/* v2.0.369 — Featured collections grid. Shown only in browse mode
   (no query/mood/filter). Two-column responsive grid of pill cards.
   Tapping fires a curated preset search. */
.v2-audiolib-browse {
  padding: 12px 14px 6px;
}
.v2-audiolib-browse-lbl {
  font: 700 11px/1 'Inter', system-ui, sans-serif;
  color: rgba(255,255,255,0.55);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  margin-bottom: 10px;
}
.v2-audiolib-featured {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 8px;
}
.v2-audiolib-featured-card {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 14px 12px;
  background: linear-gradient(135deg, rgba(163,255,18,0.08), rgba(168,85,247,0.08));
  border: 1px solid rgba(255,255,255,0.10);
  border-radius: 14px;
  color: #fff;
  font: 600 13px/1 'Inter', system-ui, sans-serif;
  cursor: pointer;
  text-align: left;
  -webkit-tap-highlight-color: transparent;
  transition: transform 100ms var(--ease), border-color 140ms var(--ease);
}
.v2-audiolib-featured-card:active {
  transform: scale(0.97);
  border-color: rgba(163,255,18,0.40);
}
.v2-audiolib-featured-emoji {
  font-size: 22px;
  line-height: 1;
}
.v2-audiolib-featured-label {
  line-height: 1.2;
  flex: 1;
}

/* v2.0.369 — Preview progress bar at bottom of the actively-previewing
   result card. Lime fill, 2px tall, anchored to bottom. Tracks live
   playback position via _audioLibPreviewProgress signal. */
.v2-audiolib-card { position: relative; }
.v2-audiolib-progress {
  position: absolute;
  left: 8px; right: 8px; bottom: 4px;
  height: 2px;
  background: rgba(255,255,255,0.08);
  border-radius: 2px;
  overflow: hidden;
  pointer-events: none;
}
.v2-audiolib-progress-fill {
  height: 100%;
  background: #a3ff12;
  border-radius: 2px;
  transition: width 100ms linear;
}
.v2-audiolib-card.is-previewing {
  border-color: rgba(163,255,18,0.35);
  background: rgba(163,255,18,0.04);
}

.v2-audiolib-smart-toggle.is-on .v2-audiolib-smart-dot {
  background: var(--accent);
  box-shadow: 0 0 6px rgba(163, 255, 18, 0.65);
}
.v2-audiolib-preview,
.v2-audiolib-add {
  width: 36px; height: 36px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center; justify-content: center;
  background: var(--surface-3);
  color: var(--text);
  transition: background 140ms var(--ease), color 140ms var(--ease), transform 90ms var(--ease);
}
.v2-audiolib-preview:hover,
.v2-audiolib-add:hover { background: rgba(163, 255, 18, 0.12); color: var(--accent); }
.v2-audiolib-preview.is-playing { background: var(--accent); color: var(--on-accent); }
.v2-audiolib-add { background: var(--accent); color: var(--on-accent); }
.v2-audiolib-add.is-importing { opacity: 0.7; }
.v2-audiolib-add-spin {
  width: 14px; height: 14px;
  border-radius: 50%;
  border: 2px solid rgba(10, 11, 16, 0.25);
  border-top-color: var(--on-accent);
  animation: v2-spin 600ms linear infinite;
}
.v2-audiolib-meta {
  min-width: 0;             /* let text-overflow ellipsis work in grid */
}
.v2-audiolib-name {
  font: 700 13px/1.2 'Inter', system-ui, sans-serif;
  color: var(--text);
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.v2-audiolib-sub {
  font: 500 11px/1.3 'Inter', system-ui, sans-serif;
  color: var(--text-2);
  margin-top: 2px;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.v2-audiolib-bpm {
  color: var(--accent);
  font-weight: 700;
}
.v2-audiolib-lic-row {
  display: flex;
  gap: 6px;
  align-items: center;
  margin-top: 4px;
  flex-wrap: wrap;
}
.v2-audiolib-lic-badge {
  font: 700 9px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 3px 6px;
  border-radius: 4px;
}
.v2-audiolib-lic-badge.is-ok {
  background: rgba(163, 255, 18, 0.10);
  color: var(--accent);
}
.v2-audiolib-lic-badge.is-restricted {
  background: rgba(251, 191, 36, 0.10);
  color: #fbbf24;
}
.v2-audiolib-lic-attr {
  font: 600 9px/1 'Inter', system-ui, sans-serif;
  color: var(--text-3);
}

/* v2.0.284 — Speed curve preset chips in the Speed sheet. Each chip
   stacks: inline SVG curve glyph + label + sublabel. Active state =
   accent border + tint. Glyph itself uses currentColor so the active
   ramp color cascades through. */
.v2-speed-curves {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 10px;
}
.v2-speed-curve-chip {
  display: grid;
  grid-template-columns: 48px 1fr;
  grid-template-rows: auto auto;
  align-items: center;
  gap: 2px 12px;
  padding: 10px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  color: var(--text);
  text-align: left;
  transition: background 140ms var(--ease), border-color 140ms var(--ease);
  cursor: pointer;
}
.v2-speed-curve-chip:hover { background: var(--surface-3); }
.v2-speed-curve-chip.is-on {
  background: rgba(163, 255, 18, 0.10);
  border-color: var(--accent);
  color: var(--accent);
}
.v2-speed-curve-glyph {
  grid-column: 1;
  grid-row: 1 / span 2;
  width: 48px;
  height: 24px;
  display: block;
  color: currentColor;
}
.v2-speed-curve-label {
  grid-column: 2;
  grid-row: 1;
  font: 700 13px/1.2 'Inter', system-ui, sans-serif;
}
.v2-speed-curve-sub {
  grid-column: 2;
  grid-row: 2;
  font: 500 11px/1.3 'Inter', system-ui, sans-serif;
  color: var(--text-2);
}
.v2-speed-curve-chip.is-on .v2-speed-curve-sub {
  color: rgba(163, 255, 18, 0.7);
}

/* v2.0.286 — PiP Style sheet chrome. Shadow preset chips + color
   input + simple sliders. Reuses .v2-vol-slider-wrap from the
   volume panel for slider consistency. */
.v2-tool-pipstyle { max-height: 55dvh; }  /* v2.0.491 — control panel: keep canvas visible (was 80dvh) */
.v2-pip-shadows {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 8px;
  margin-top: 8px;
}
.v2-pip-shadow-chip {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 10px 4px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  color: var(--text-2);
  font: 600 11px/1 'Inter', system-ui, sans-serif;
  transition: background 140ms var(--ease), border-color 140ms var(--ease), color 140ms var(--ease);
}
.v2-pip-shadow-chip:hover { background: var(--surface-3); }
.v2-pip-shadow-chip.is-on {
  background: rgba(168, 85, 247, 0.12);
  border-color: rgba(168, 85, 247, 0.7);
  color: #d8b4fe;
}
.v2-pip-shadow-glyph-wrap {
  width: 100%;
  display: block;
  color: var(--text);
}
.v2-pip-shadow-glyph { width: 100%; height: 14px; display: block; }
.v2-tool-icon-stub {
  width: 36px; height: 36px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--text-2);
}
.v2-pip-color-input {
  flex: 1;
  height: 30px;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  padding: 0 4px;
  cursor: pointer;
}

/* v2.0.287 — TemplatesSheet. Two-tab sheet for curated templates +
   AI Director. Template cards are tappable preset chips; AI tab
   has a textarea + Generate button; both populate the same "Plan"
   section with explainability step chips. */
.v2-tool-templates { max-height: 86dvh; }
.v2-templates-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 10px;
  margin-top: 12px;
}
@media (min-width: 540px) {
  .v2-templates-grid { grid-template-columns: repeat(3, 1fr); }
}
.v2-template-card {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 14px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  color: var(--text);
  text-align: left;
  cursor: pointer;
  transition: background 140ms var(--ease), border-color 140ms var(--ease), transform 90ms var(--ease);
}
.v2-template-card:hover { background: var(--surface-3); }
.v2-template-card:active { transform: scale(0.98); }
.v2-template-card.is-on {
  background: rgba(163, 255, 18, 0.10);
  border-color: var(--accent);
}
.v2-template-card-icon {
  width: 28px; height: 28px;
  display: inline-flex;
  align-items: center; justify-content: center;
  color: var(--accent);
}
.v2-template-card-label {
  font: 700 13px/1.2 'Inter', system-ui, sans-serif;
}
.v2-template-card-sub {
  font: 500 11px/1.3 'Inter', system-ui, sans-serif;
  /* v2.0.393 — audit A1.F9 (MED): was var(--text-2) (~0.55 alpha) →
     ~3.5:1 contrast on surface-2, failing WCAG AA for small text
     (4.5:1 required). Bumped to 0.72 for ~5.2:1 — passes AA. */
  color: rgba(255,255,255,0.72);
}
/* v2.0.392 — Template card live preview canvas. 156×72 synthesized
   visualization of the template's signature look — filter tint,
   transition shape, caption preset. Sits at the top of every card. */
.v2-template-card-preview {
  width: 100%;
  aspect-ratio: 156 / 72;
  border-radius: 8px;
  background: linear-gradient(180deg, #1d2230, #0e1119);
  border: 1px solid rgba(255,255,255,0.06);
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 4px;
  position: relative;
}
.v2-template-card-preview canvas {
  display: block;
  width: 100%;
  height: 100%;
}
.v2-template-card-preview.is-fallback {
  color: var(--text-2);
  background: var(--surface-3);
}
.v2-template-card.is-on .v2-template-card-preview {
  border-color: rgba(163, 255, 18, 0.45);
  box-shadow: 0 0 0 1px rgba(163, 255, 18, 0.18) inset;
}
/* v2.0.393 — Wrapper for cards so the "+ Add" affordance can sit on
   top of the card without breaking the card's own button. */
.v2-template-card-wrap {
  position: relative;
  display: flex;
}
.v2-template-card-wrap > .v2-template-card { flex: 1; min-width: 0; }
/* v2.0.393 — "+ Add to plan" appears top-right when a preview is
   already active and this isn't the source card. Cyan accent to
   differentiate from the lime "is-on" selected state. */
.v2-template-card-add {
  position: absolute;
  top: 8px;
  right: 8px;
  background: rgba(34, 211, 238, 0.85);
  border: 1px solid rgba(34, 211, 238, 1);
  color: #0a1c20;
  font: 700 10px/1 'Inter', sans-serif;
  letter-spacing: 0.04em;
  padding: 5px 8px;
  min-height: 28px;
  border-radius: 999px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  z-index: 2;
  box-shadow: 0 2px 8px rgba(0,0,0,0.35);
}
.v2-template-card-add:active { transform: scale(0.94); }
/* v2.0.393 — "Popular" badge top-left of card; static once rendered. */
.v2-template-card-badge {
  position: absolute;
  top: 8px;
  left: 8px;
  background: linear-gradient(135deg, #fbbf24, #f59e0b);
  color: #1a1300;
  font: 800 9px/1 'Inter', sans-serif;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 4px 7px;
  border-radius: 999px;
  z-index: 1;
  box-shadow: 0 1px 4px rgba(0,0,0,0.30);
  pointer-events: none;
}
/* v2.0.393 — Apply-scope chip row. Sits inside the plan preview block,
   above the Requires hint + step list. Selected chip gets the same
   cyan accent as the director-target chips for visual continuity. */
.v2-director-scope-pick {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 6px 0 10px;
}
.v2-director-scope-chip {
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(255,255,255,0.10);
  color: rgba(255,255,255,0.72);
  font: 600 11px/1 'Inter', sans-serif;
  padding: 8px 12px;
  min-height: 32px;
  border-radius: 999px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition: background 120ms var(--ease), border-color 120ms var(--ease), color 120ms var(--ease);
  position: relative;
}
.v2-director-scope-chip::after {
  /* v2.0.393 — invisible bleed-out so chip clears iOS 44pt target */
  content: '';
  position: absolute;
  inset: -6px;
}
.v2-director-scope-chip:active { transform: scale(0.96); }
.v2-director-scope-chip.is-active {
  background: rgba(34, 211, 238, 0.18);
  border-color: rgba(34, 211, 238, 0.55);
  color: #b8ecf6;
}
.v2-director-scope-chip:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
/* v2.0.392 — Category filter chip strip + inline search. Toolbar sits
   between the tabs and the grid; chips scroll horizontally on narrow
   screens, search input is a compact pill that doesn't dominate. */
.v2-templates-toolbar {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 8px 0 12px;
}
.v2-templates-cats {
  display: flex;
  gap: 6px;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  padding-bottom: 4px;
  scroll-snap-type: x proximity;
  /* v2.0.393 — audit A1.F6 (MED): pan-x so horizontal swipe stays
     in the chip strip + doesn't race with parent vertical scroll.
     overscroll-behavior-x: contain prevents body bounce when user
     flings past the last chip. */
  touch-action: pan-x;
  overscroll-behavior-x: contain;
  /* Hide scrollbar for cleaner mobile look — chips have visual
     overflow indication via fade gradient at the edge. */
  scrollbar-width: none;
}
.v2-templates-cats::-webkit-scrollbar { display: none; }
.v2-templates-cat {
  background: rgba(255,255,255,0.06);
  border: 1px solid rgba(255,255,255,0.12);
  color: rgba(255,255,255,0.78);
  font: 600 12px/1 'Inter', sans-serif;
  /* v2.0.393 — audit A1.F1 (HIGH): bumped padding 8 → 11 + min-height
     32 + ::after bleed-out to clear iOS 44pt touch target without
     making the chip visually larger. */
  padding: 11px 14px;
  min-height: 32px;
  position: relative;
  border-radius: 999px;
  cursor: pointer;
  white-space: nowrap;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  scroll-snap-align: start;
  transition: background 120ms var(--ease), border-color 120ms var(--ease), color 120ms var(--ease);
}
.v2-templates-cat::after {
  /* v2.0.393 — invisible 6px bleed-out clears iOS 44pt without
     changing visible chip size. Same pattern as caption word chips. */
  content: '';
  position: absolute;
  inset: -6px;
}
.v2-templates-cat:active { transform: scale(0.97); }
.v2-templates-cat.is-active {
  background: rgba(163, 255, 18, 0.18);
  border-color: rgba(163, 255, 18, 0.55);
  color: #d8f5a3;
}
.v2-templates-search-wrap {
  position: relative;
  display: flex;
}
.v2-templates-search {
  flex: 1;
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(255,255,255,0.10);
  color: var(--text);
  font: 500 13px/1 'Inter', sans-serif;
  padding: 9px 14px;
  border-radius: 10px;
  outline: none;
  -webkit-appearance: none;
  appearance: none;
}
.v2-templates-search::placeholder { color: rgba(255,255,255,0.40); }
.v2-templates-search:focus {
  border-color: rgba(34,211,238,0.55);
  background: rgba(34,211,238,0.05);
}
.v2-templates-empty {
  padding: 28px 16px;
  text-align: center;
  color: rgba(255,255,255,0.55);
  font: 500 13px/1.5 'Inter', sans-serif;
  background: rgba(255,255,255,0.03);
  border: 1px dashed rgba(255,255,255,0.10);
  border-radius: 10px;
  margin-top: 4px;
}
/* v2.0.394 — Saved presets manager view (Mine tab). List layout with
   per-row preview + inline-renamable label + Apply + Delete, plus a
   top toolbar for batch Export / Import JSON. */
.v2-mine-view { display: flex; flex-direction: column; gap: 8px; }
.v2-mine-toolbar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 4px;
  border-bottom: 1px solid rgba(255,255,255,0.06);
  margin-bottom: 4px;
}
.v2-mine-count {
  font: 500 11px/1 'Inter', sans-serif;
  color: rgba(255,255,255,0.55);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.v2-mine-toolbar-spacer { flex: 1; }
.v2-mine-toolbar-btn {
  background: rgba(255,255,255,0.06);
  border: 1px solid rgba(255,255,255,0.14);
  color: rgba(255,255,255,0.85);
  font: 600 12px/1 'Inter', sans-serif;
  padding: 9px 14px;
  min-height: 36px;
  border-radius: 8px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition: background 120ms var(--ease), border-color 120ms var(--ease);
}
.v2-mine-toolbar-btn:active { background: rgba(255,255,255,0.12); transform: scale(0.97); }
.v2-mine-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.v2-mine-row {
  display: grid;
  grid-template-columns: 96px 1fr auto auto;
  gap: 10px;
  align-items: center;
  padding: 10px;
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(255,255,255,0.08);
  border-radius: 10px;
}
.v2-mine-row-preview {
  width: 96px;
  aspect-ratio: 156 / 72;
  border-radius: 6px;
  overflow: hidden;
}
.v2-mine-row-preview .v2-template-card-preview {
  margin: 0;
  width: 100%;
  height: 100%;
}
.v2-mine-row-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.v2-mine-row-label {
  font: 700 13px/1.3 'Inter', sans-serif;
  color: var(--text);
  outline: none;
  padding: 2px 4px;
  margin: -2px -4px;
  border-radius: 4px;
  cursor: text;
  -webkit-tap-highlight-color: transparent;
}
.v2-mine-row-label:focus {
  background: rgba(34, 211, 238, 0.10);
  box-shadow: 0 0 0 1px rgba(34, 211, 238, 0.40) inset;
}
.v2-mine-row-sub {
  font: 500 11px/1.3 'Inter', sans-serif;
  color: rgba(255,255,255,0.55);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.v2-mine-row-apply {
  background: rgba(34, 211, 238, 0.18);
  border: 1px solid rgba(34, 211, 238, 0.55);
  color: #b8ecf6;
  font: 600 11px/1 'Inter', sans-serif;
  padding: 9px 12px;
  min-height: 36px;
  border-radius: 8px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}
.v2-mine-row-apply:active { background: rgba(34, 211, 238, 0.28); transform: scale(0.97); }
.v2-mine-row-del {
  background: transparent;
  border: 1px solid rgba(220, 38, 38, 0.30);
  color: rgba(248, 113, 113, 0.85);
  width: 36px; height: 36px;
  border-radius: 8px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}
.v2-mine-row-del:active { background: rgba(220, 38, 38, 0.12); transform: scale(0.93); }

/* v2.0.392 — Requires hint surface. Two variants: .is-ok (green
   affirmative when all requirements met) + .is-warn (yellow caution
   listing what's missing — non-blocking, just transparency). */
.v2-director-requires {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 7px 10px;
  border-radius: 8px;
  font: 600 11px/1.3 'Inter', sans-serif;
  margin: 6px 0 4px;
}
.v2-director-requires.is-ok {
  background: rgba(34, 197, 94, 0.10);
  border: 1px solid rgba(34, 197, 94, 0.32);
  color: #a7f3d0;
}
.v2-director-requires.is-warn {
  background: rgba(254, 240, 138, 0.08);
  border: 1px solid rgba(254, 240, 138, 0.30);
  color: #fef08a;
}
/* v2.0.392 — Two-button action row + Save as preset + Delete preset.
   v2.0.393 — audit A1.F10 (MED): safe-area-inset-bottom padding so
   the action row clears the iOS home indicator zone on phones
   without an external dock. */
.v2-director-actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  margin-top: 8px;
  padding-bottom: max(0px, env(safe-area-inset-bottom, 0px));
}
.v2-director-apply--save {
  background: rgba(34, 211, 238, 0.12);
  border-color: rgba(34, 211, 238, 0.45);
  color: #b8ecf6;
}
.v2-director-apply--save:active { background: rgba(34, 211, 238, 0.22); }
.v2-director-preset-del {
  margin-top: 6px;
  background: transparent;
  border: 1px solid rgba(220, 38, 38, 0.30);
  color: rgba(248, 113, 113, 0.85);
  font: 600 11px/1 'Inter', sans-serif;
  /* v2.0.393 — audit A1.F11 (MED): bumped padding 7/10 → 10/14 +
     min-height 36 so the destructive button clears iOS HIG. Was ~25px
     tall — too easy to miss-tap (red button = accidental tap is BAD). */
  padding: 10px 14px;
  min-height: 36px;
  border-radius: 8px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}
.v2-director-preset-del:active {
  background: rgba(220, 38, 38, 0.10);
}

/* v2.0.288 — AI Director mood chips. Quick-tap row above the
   textarea — first-time users have a starting point instead of
   blank-page paralysis. Each chip prepends its phrase to the
   prompt; user can then add specifics. */
.v2-director-moods {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 6px 0 10px;
}
.v2-director-mood-chip {
  padding: 6px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--text-2);
  font: 600 11px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.02em;
  transition: background 140ms var(--ease), color 140ms var(--ease), border-color 140ms var(--ease), transform 90ms var(--ease);
  cursor: pointer;
}
.v2-director-mood-chip:hover {
  background: rgba(163, 255, 18, 0.10);
  border-color: rgba(163, 255, 18, 0.5);
  color: var(--accent);
}
.v2-director-mood-chip:active { transform: scale(0.96); }
.v2-director-mood-chip:disabled { opacity: 0.5; cursor: not-allowed; }
/* v2.0.392 — Mood chip glyph + color dot. Glyph leads visually for
   fast scanning; --mood-dot custom property colors the bottom rule. */
.v2-director-mood-glyph {
  margin-right: 4px;
  font-size: 13px;
  line-height: 1;
}
.v2-director-mood-chip {
  position: relative;
  display: inline-flex;
  align-items: center;
}
.v2-director-mood-chip::after {
  content: '';
  position: absolute;
  left: 12px;
  right: 12px;
  bottom: 3px;
  height: 2px;
  border-radius: 1px;
  background: var(--mood-dot, transparent);
  opacity: 0.65;
}

/* v2.0.392 — Director target chips (Platform / Length / Aspect).
   Rows of small pills under the mood chips. Selected state uses cyan
   accent to differentiate from the lime mood-chip-hover. Each row has
   an inline label on the left. */
.v2-director-targets {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 0 0 10px;
  padding: 8px;
  background: rgba(255,255,255,0.03);
  border: 1px solid rgba(255,255,255,0.06);
  border-radius: 10px;
}
.v2-director-target-row {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
}
.v2-director-target-label {
  font: 600 10px/1 'Inter', sans-serif;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: rgba(255,255,255,0.45);
  width: 48px;
}
.v2-director-target-chip {
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(255,255,255,0.10);
  color: rgba(255,255,255,0.72);
  font: 600 11px/1 'Inter', sans-serif;
  /* v2.0.393 — audit A1.F2 (HIGH — highest risk): was padding 6/10
     yielding ~24px tall, with rows gap 6px → adjacent rows' chips
     within thumb-mistap distance. Wrong Platform/Length/Aspect
     silently corrupts the LLM prompt. Now padding 10/12 + min-height
     32 + invisible 6px ::after bleed gives true 44pt hit. */
  padding: 10px 12px;
  min-height: 32px;
  position: relative;
  border-radius: 999px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition: background 120ms var(--ease), border-color 120ms var(--ease), color 120ms var(--ease);
}
.v2-director-target-chip::after {
  content: '';
  position: absolute;
  inset: -6px;
}
.v2-director-target-chip:active { transform: scale(0.96); }
.v2-director-target-chip.is-active {
  background: rgba(34,211,238,0.18);
  border-color: rgba(34,211,238,0.55);
  color: #b8ecf6;
}
.v2-director-target-chip:disabled { opacity: 0.5; cursor: not-allowed; }

/* v2.0.288 — Plan scope summary line. "4 scenes affected · 5 steps
   runnable · 1 skipped" — honest pre-commit scope display. */
.v2-director-scope {
  margin: 8px 0 10px;
  padding: 6px 10px;
  background: rgba(255,255,255,0.03);
  border-radius: var(--r-sm);
  font: 600 11px/1.3 'Inter', system-ui, sans-serif;
  letter-spacing: 0.02em;
  color: var(--text-2);
}

/* v2.0.288 — Progress bar at top of step list during Apply. */
.v2-director-progress {
  height: 3px;
  background: rgba(255,255,255,0.06);
  border-radius: 2px;
  overflow: hidden;
  margin-bottom: 8px;
}
.v2-director-progress-fill {
  height: 100%;
  background: var(--accent);
  transition: width 200ms var(--ease);
}

/* v2.0.288 — Pre-flight skip + post-apply skip step states. */
.v2-director-step.is-preflight-skip {
  background: rgba(255,255,255,0.02);
  border-color: rgba(255,255,255,0.06);
  opacity: 0.65;
}
.v2-director-step.is-skipped {
  background: rgba(251, 191, 36, 0.06);
  border-color: rgba(251, 191, 36, 0.3);
}
.v2-director-step-skip {
  font: 600 11px/1.3 'Inter', system-ui, sans-serif;
  color: #fbbf24;
  margin-top: 2px;
}

/* v2.0.288 — Reset variant of the Apply button. Less-emphasis
   color (outline accent, not solid) since it's the secondary
   action after a completed Apply. */
.v2-director-apply--reset {
  background: transparent;
  color: var(--accent);
  border: 1px solid var(--accent);
}
.v2-director-apply--reset:hover {
  background: rgba(163, 255, 18, 0.06);
}

/* AI Director prompt + generate. */
.v2-director-prompt {
  font-size: 14px;
  margin-bottom: 10px;
  width: 100%;
  resize: vertical;
}
.v2-director-generate {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 12px 20px;
  background: var(--accent);
  color: var(--on-accent);
  border-radius: var(--r-pill);
  font: 800 13px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  transition: opacity 140ms var(--ease), transform 90ms var(--ease);
  width: 100%;
  justify-content: center;
}
.v2-director-generate:disabled { opacity: 0.5; }
.v2-director-generate:active { transform: scale(0.98); }
.v2-director-hint {
  margin-top: 10px;
  font: 500 11px/1.5 'Inter', system-ui, sans-serif;
  color: var(--text-3);
}
.v2-director-hint strong {
  color: var(--text-2);
  font-weight: 700;
}

/* Plan + step log shared between Templates and AI Director tabs. */
.v2-director-summary {
  padding: 10px 12px;
  background: rgba(163, 255, 18, 0.06);
  border: 1px solid rgba(163, 255, 18, 0.22);
  border-radius: var(--r-sm);
  font: 500 12px/1.5 'Inter', system-ui, sans-serif;
  color: var(--text);
  margin: 10px 0 12px;
}
.v2-director-steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.v2-director-step {
  display: grid;
  /* v2.0.393 — Added toggle column (28px) + swatch column (auto) so
     each step shows: [toggle] [number] [icon] [swatch?] [body] [status].
     swatch is only present for filter/transition steps; uses auto. */
  grid-template-columns: 28px 24px 24px auto 1fr auto;
  align-items: start;
  gap: 8px;
  padding: 10px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  transition: background 140ms var(--ease), border-color 140ms var(--ease), opacity 140ms var(--ease);
}
.v2-director-step.is-ok {
  background: rgba(163, 255, 18, 0.06);
  border-color: rgba(163, 255, 18, 0.3);
}
.v2-director-step.is-failed {
  background: rgba(255, 107, 107, 0.06);
  border-color: rgba(255, 107, 107, 0.35);
}
/* v2.0.393 — User-disabled step: dim + strikethrough but still visible
   so user can re-enable. Distinct from pre-flight skip (which is amber
   to signal "won't run") — disabled is gray to signal "won't run by
   choice". */
.v2-director-step.is-user-disabled {
  opacity: 0.55;
  background: rgba(255,255,255,0.02);
}
.v2-director-step.is-user-disabled .v2-director-step-label,
.v2-director-step.is-user-disabled .v2-director-step-reason {
  text-decoration: line-through;
}
/* v2.0.393 — Per-step toggle pill. 28×28 visible / clears 44px hit
   target via padding pseudo. Filled when enabled, hollow when
   disabled (close icon). */
.v2-director-step-toggle {
  width: 28px;
  height: 28px;
  min-height: 28px;
  border-radius: 50%;
  background: rgba(163, 255, 18, 0.18);
  border: 1px solid rgba(163, 255, 18, 0.55);
  color: var(--accent);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  padding: 0;
  position: relative;
  transition: background 140ms var(--ease), border-color 140ms var(--ease), color 140ms var(--ease);
}
.v2-director-step-toggle::after {
  /* v2.0.393 — invisible bleed-out to clear 44pt iOS hit target */
  content: '';
  position: absolute;
  inset: -8px;
}
.v2-director-step-toggle:active { transform: scale(0.92); }
.v2-director-step-toggle:disabled { opacity: 0.4; cursor: not-allowed; }
.v2-director-step-toggle.is-off {
  background: rgba(255,255,255,0.04);
  border-color: rgba(255,255,255,0.18);
  color: rgba(255,255,255,0.55);
}
/* v2.0.393 — Inline filter / transition swatch in the step row. Small
   square for filters (background-color from _TEMPLATE_FILTER_TINT) and
   a per-transition glyph for transitions. */
.v2-director-step-swatch {
  width: 14px;
  height: 14px;
  border-radius: 3px;
  margin-top: 4px;
  border: 1px solid rgba(255,255,255,0.18);
  flex-shrink: 0;
  position: relative;
}
.v2-director-step-swatch.is-filter {
  /* color comes from inline style; border slightly brighter so the
     swatch reads against the surface-2 background */
  box-shadow: 0 0 0 1px rgba(0,0,0,0.3) inset;
}
.v2-director-step-swatch[class*="is-transition-"] {
  background: rgba(255,255,255,0.06);
}
.v2-director-step-swatch.is-transition-flash {
  background: radial-gradient(circle, #ffffff 0%, rgba(255,255,255,0.20) 60%, transparent 100%);
}
.v2-director-step-swatch.is-transition-fade,
.v2-director-step-swatch.is-transition-crossfade {
  background: linear-gradient(90deg, rgba(255,255,255,0.05), rgba(255,255,255,0.55), rgba(255,255,255,0.05));
}
.v2-director-step-swatch.is-transition-whip {
  background: repeating-linear-gradient(135deg, rgba(255,255,255,0.55) 0 2px, transparent 2px 4px);
}
.v2-director-step-swatch.is-transition-zoom {
  background: radial-gradient(circle, rgba(255,255,255,0.65) 0%, rgba(255,255,255,0.05) 70%);
}
.v2-director-step-swatch.is-transition-slide {
  background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.55) 50%, transparent 100%);
}
.v2-director-step-swatch.is-transition-iris,
.v2-director-step-swatch.is-transition-wipe,
.v2-director-step-swatch.is-transition-static,
.v2-director-step-swatch.is-transition-glitch {
  background: rgba(255,255,255,0.30);
}
.v2-director-step-num {
  font: 700 11px/1 'JetBrains Mono', ui-monospace, monospace;
  color: var(--text-3);
  padding-top: 4px;
}
.v2-director-step-icon {
  color: var(--accent);
  padding-top: 2px;
}
.v2-director-step-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.v2-director-step-label {
  font: 700 12px/1.3 'Inter', system-ui, sans-serif;
  color: var(--text);
}
.v2-director-step-reason {
  font: 500 11px/1.4 'Inter', system-ui, sans-serif;
  color: var(--text-2);
}
.v2-director-step-err {
  font: 600 11px/1.3 'Inter', system-ui, sans-serif;
  color: #ff6b6b;
  margin-top: 2px;
}
.v2-director-apply {
  margin-top: 12px;
  padding: 14px;
  width: 100%;
  background: var(--accent);
  color: var(--on-accent);
  border-radius: var(--r-md);
  font: 800 14px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  transition: opacity 140ms var(--ease), transform 90ms var(--ease);
}
.v2-director-apply:disabled { opacity: 0.5; }
.v2-director-apply:active { transform: scale(0.98); }

/* v2.0.286 — Picture-in-Picture canvas DOM hit-target. The actual
   pixel rendering lives on the canvas via drawPipOverlay; this
   element is INVISIBLE-but-tappable so the existing canvas-overlay
   drag + pinch systems have something to attach to. On selection,
   a subtle accent outline appears to confirm which PiP is active. */
.v2-canvas-pip {
  position: absolute;
  background: transparent;
  cursor: grab;
  user-select: none;
  z-index: 12;          /* above stickers/text hit-targets (z:10) */
  will-change: transform;
  transition: transform 140ms cubic-bezier(.2, .8, .2, 1),
              left      140ms cubic-bezier(.2, .8, .2, 1),
              top       140ms cubic-bezier(.2, .8, .2, 1);
}
.v2-canvas-pip.is-dragging,
.v2-canvas-pip.is-transforming { transition: none; z-index: 30; cursor: grabbing; }
/* v2.0.335 — Disable PiP transition during playback so DOM bbox + handles
   don't trail the 60Hz keyframed canvas paint (see .v2-canvas-text). */
.v2-canvas-frame.is-playing .v2-canvas-pip { transition: none; }
.v2-canvas-pip:active { cursor: grabbing; }
/* v2.0.363 — PiP selection chrome (2px solid purple outline) REMOVED.
   The PiP DOM is invisible (canvas paint draws the actual pixels), so
   the outline traced an invisible box at the PiP wrapper's DOM bounds
   — which scale/rotate/translate independently of the painted PiP
   pixels (especially during keyframed motion). Result: a visible
   purple box that drifted relative to the actual PiP content. User
   reported it as "shifting from squares to rectangles as i move my
   finger around the screen." Selection cue is now the action toolbar
   + tool sheet, like text and sticker. */

/* v2.0.282 — Picture-in-Picture timeline track. Slim bar above the
   audio row, accent-tinted so it's visually distinct from scenes
   (lime) and audio (purple-ish). Each bar shows a small video-icon
   prefix + clip label; on selection, the same trim handles + delete
   chip as audio bars appear. */
.v2-tl-track--pip {
  height: 22px;
  margin-top: 6px;
  position: relative;
}
.v2-tl-bar--pip {
  position: absolute;
  top: 0; bottom: 0;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 0 8px;
  background: linear-gradient(135deg, rgba(168, 85, 247, 0.35), rgba(99, 102, 241, 0.35));
  border: 1px solid rgba(168, 85, 247, 0.55);
  border-radius: 6px;
  color: #fff;
  font: 700 10px/1 'Inter', system-ui, sans-serif;
  overflow: hidden;
  cursor: grab;
  transition: box-shadow 140ms var(--ease);
}
.v2-tl-bar--pip.is-selected {
  box-shadow: 0 0 0 2px rgba(168, 85, 247, 0.75);
}
.v2-tl-bar-pip-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  opacity: 0.85;
  flex-shrink: 0;
}
.v2-tl-bar--pip .v2-tl-bar-lbl {
  font: 700 10px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.02em;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  /* v2.0.291 — sit above the thumb strip + keyframe dots. The label
     gets a tight semi-opaque ink so it stays readable over busy
     PiP footage thumbnails (a face, an explosion, anything). */
  position: relative;
  z-index: 3;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.65);
}
.v2-tl-bar--pip .v2-tl-bar-pip-icon {
  position: relative;
  z-index: 3;
  filter: drop-shadow(0 1px 1.5px rgba(0, 0, 0, 0.55));
}
/* v2.0.291 — PiP bar thumb strip. Same CSS hook as scene bars
   (.v2-tl-bar-thumbs / .v2-tl-bar-thumb) so the renderer reuses
   the same grid math. The thumbnail layer is z-index 1 — below
   the label + keyframe dots, above the gradient backdrop. */
.v2-tl-bar--pip.has-thumbs {
  /* Reduce backdrop saturation so thumbs read clearly; the purple
     gradient becomes a subtle accent rather than competing with the
     image data. */
  background: linear-gradient(135deg, rgba(168, 85, 247, 0.18), rgba(99, 102, 241, 0.18));
}
.v2-tl-bar--pip .v2-tl-bar-thumbs {
  position: absolute;
  inset: 0;
  display: flex;
  gap: 0;
  z-index: 1;
  opacity: 0.78;
  pointer-events: none;
}
.v2-tl-bar--pip .v2-tl-bar-thumb {
  flex: 1 1 0;
  height: 100%;
  object-fit: cover;
  background-color: rgba(168, 85, 247, 0.10);
}
/* v2.0.291 — Keyframe diamond strip on the PiP bar. One small
   diamond per pinned keyframe time, positioned along the bar.
   Lets the user SEE where motion beats land without opening
   the Motion panel — CapCut-grade at-a-glance feedback. */
.v2-tl-bar--pip.is-keyframed {
  box-shadow: 0 0 0 1px rgba(255, 215, 0, 0.50);
}
.v2-tl-bar--pip.is-keyframed.is-selected {
  box-shadow: 0 0 0 2px rgba(168, 85, 247, 0.75),
              0 0 0 3px rgba(255, 215, 0, 0.50);
}
.v2-tl-bar-kf-strip {
  position: absolute;
  left: 0; right: 0;
  top: 1px;
  /* v2.0.292 — Strip is taller than the visible dots so the
     button hit area is generous (14×14 effective). Strip itself
     is still pointer-events: none so the bar can still receive
     drags + the strip width doesn't steal touches between dots;
     the dot buttons re-enable pointer-events for themselves. */
  height: 14px;
  z-index: 2;
  pointer-events: none;
}
.v2-tl-bar-kf-dot {
  /* v2.0.292 — Now a real button. Larger transparent hit area
     centers a 6×6 visible diamond. Re-enables pointer-events
     against the parent strip. Tap = seek to keyframe time;
     long-press (450ms) = remove every keyframe at that time. */
  position: absolute;
  top: 50%;
  width: 14px; height: 14px;
  margin: -7px 0 0 -7px;
  padding: 0;
  border: 0;
  background: transparent;
  pointer-events: auto;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  outline: none;
}
.v2-tl-bar-kf-dot::before {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  width: 6px; height: 6px;
  margin: -3px 0 0 -3px;
  background: #ffd700;
  transform: rotate(45deg);
  border-radius: 1px;
  box-shadow: 0 0 3px rgba(255, 215, 0, 0.55);
  transition: transform 120ms var(--ease), box-shadow 120ms var(--ease), background 120ms var(--ease);
}
.v2-tl-bar-kf-dot:hover::before {
  background: #fff3a8;
  transform: rotate(45deg) scale(1.35);
  box-shadow: 0 0 6px rgba(255, 243, 168, 0.85);
}
.v2-tl-bar-kf-dot:active::before {
  transform: rotate(45deg) scale(0.95);
}
.v2-tl-bar-kf-dot:focus-visible::before {
  box-shadow: 0 0 0 2px #0a0b10, 0 0 0 4px #ffd700;
}

/* v2.0.289 — Auto-reframe sheet. Visual aspect cards (each shows
   a mini rectangle in the target shape) + mode picker reusing the
   autocut-modes pattern + zoom slider + apply/clear actions. */
.v2-tool-reframe { max-height: 55dvh; }  /* v2.0.491 — control panel: keep canvas visible (was 86dvh) */
.v2-reframe-aspects {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 8px;
  margin-top: 8px;
}
.v2-reframe-aspect {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 10px 4px 8px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  color: var(--text-2);
  transition: background 140ms var(--ease), border-color 140ms var(--ease), color 140ms var(--ease);
  cursor: pointer;
}
.v2-reframe-aspect:hover { background: var(--surface-3); }
.v2-reframe-aspect.is-on {
  background: rgba(163, 255, 18, 0.10);
  border-color: var(--accent);
  color: var(--accent);
}
.v2-reframe-aspect:disabled { opacity: 0.5; cursor: not-allowed; }
.v2-reframe-aspect-vis {
  width: 36px; height: 36px;
  display: inline-flex;
  align-items: center; justify-content: center;
}
.v2-reframe-aspect-rect {
  background: currentColor;
  opacity: 0.35;
  border: 1px solid currentColor;
  border-radius: 2px;
}
.v2-reframe-aspect.is-on .v2-reframe-aspect-rect { opacity: 0.65; }
.v2-reframe-aspect-label {
  font: 700 11px/1 'Inter', system-ui, sans-serif;
}
.v2-reframe-aspect-sub {
  font: 500 9px/1.2 'Inter', system-ui, sans-serif;
  color: var(--text-3);
  text-align: center;
}

/* v2.0.290 — Speed-curve point editor. Interactive SVG graph with
   draggable control points + tap-to-add + long-press-to-remove. */
.v2-speed-graph {
  display: block;
  width: 100%;
  height: 140px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  margin: 8px 0;
  touch-action: none;
  user-select: none;
}
.v2-speed-graph-label {
  font: 700 9px/1 'JetBrains Mono', ui-monospace, monospace;
  fill: var(--text-3);
}
.v2-speed-pt {
  cursor: grab;
  touch-action: none;
}
.v2-speed-pt:active { cursor: grabbing; }
.v2-speed-pt-hit {
  fill: transparent;
  pointer-events: all;
}
.v2-speed-pt-dot {
  fill: var(--accent, #a3ff12);
  stroke: #0a0b10;
  stroke-width: 1.5;
  transition: r 100ms var(--ease);
}
.v2-speed-pt.is-pinned .v2-speed-pt-dot {
  fill: var(--text-2);
  stroke: var(--text-3);
}
.v2-speed-pt.is-dragging .v2-speed-pt-dot {
  r: 8;
  filter: drop-shadow(0 0 4px rgba(163, 255, 18, 0.7));
}
.v2-speed-graph-controls {
  display: flex;
  flex-direction: column;
  gap: 6px;
  align-items: stretch;
  margin-top: 6px;
}
.v2-speed-graph-hint {
  font: 500 10px/1.4 'Inter', system-ui, sans-serif;
  color: var(--text-3);
  text-align: center;
}

/* v2.0.290 — Speed-curve indicator on timeline scene bar. Thin SVG
   strip at the bottom of the bar showing the ramp shape so users
   see the curve at-a-glance from the timeline. */
.v2-tl-bar-speed-curve {
  position: absolute;
  bottom: 1px;
  left: 0;
  right: 0;
  width: 100%;
  height: 14px;
  pointer-events: none;
  opacity: 0.85;
  /* v2.0.293 — Sit above the PiP thumb strip (z:1) but below the
     label + icon (z:3). On scene bars there are no z-index'd thumb
     layers so this rule is a no-op there. Without it, PiP thumbs
     would paint over the curve and hide the ramp shape. */
  z-index: 2;
}
/* v2.0.293 — When a PiP bar has both thumbs AND a speed curve, the
   bottom 14px would dim the lower half of the thumb strip. Bump
   the curve opacity slightly and add a subtle dark underlay so the
   lime stroke reads sharply against busy thumbnail content. */
.v2-tl-bar--pip.has-speed-curve .v2-tl-bar-speed-curve {
  opacity: 1;
  filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.55));
}

/* v2.0.281 — Auto-cut to beat sheet. Compact mode picker with
   plain-English sub-labels so users see the trade-off (frantic vs.
   musical) without needing to know what "1/4 quantize" means. */
.v2-tool-autocut { max-height: 55dvh; }  /* v2.0.491 — control panel: keep canvas visible (was 70dvh) */
.v2-autocut-empty {
  padding: 30px 20px;
  text-align: center;
  color: var(--text-2);
}
.v2-autocut-track {
  padding: 14px 16px;
  background: rgba(163, 255, 18, 0.08);
  border: 1px solid rgba(163, 255, 18, 0.22);
  border-radius: var(--r-md);
  margin-bottom: 16px;
}
.v2-autocut-track-name {
  font: 700 14px/1.2 'Inter', system-ui, sans-serif;
  color: var(--accent);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.v2-autocut-track-meta {
  font: 500 11px/1.4 'Inter', system-ui, sans-serif;
  color: var(--text-2);
  margin-top: 3px;
}
.v2-autocut-modes {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 10px;
}
.v2-autocut-mode {
  display: block;
  text-align: left;
  padding: 12px 14px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  color: var(--text);
  transition: background 140ms var(--ease), border-color 140ms var(--ease);
  cursor: pointer;
}
.v2-autocut-mode:hover { background: var(--surface-3); }
.v2-autocut-mode.is-on {
  background: rgba(163, 255, 18, 0.10);
  border-color: var(--accent);
}
.v2-autocut-mode-label {
  font: 700 13px/1.2 'Inter', system-ui, sans-serif;
}
.v2-autocut-mode-sub {
  font: 500 11px/1.3 'Inter', system-ui, sans-serif;
  color: var(--text-2);
  margin-top: 2px;
}
.v2-autocut-preview {
  margin-top: 12px;
  padding: 8px 10px;
  background: var(--surface-2);
  border-radius: var(--r-sm);
  font: 500 11px/1.4 'Inter', system-ui, sans-serif;
  color: var(--text-2);
}
.v2-autocut-warn { color: #fbbf24; }
.v2-autocut-actions {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 8px;
  margin-top: 18px;
}
.v2-autocut-apply {
  padding: 14px;
  background: var(--accent);
  color: var(--on-accent);
  border-radius: var(--r-md);
  font: 800 14px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  transition: transform 90ms var(--ease);
}
.v2-autocut-apply:active { transform: scale(0.97); }
.v2-autocut-hint {
  text-align: center;
  font: 500 11px/1.3 'Inter', system-ui, sans-serif;
  color: var(--text-3);
}

/* v2.0.274 — Stock-media library grid. Two-column thumbnail grid;
   each thumb shows video preview on hover (autoplay muted loop) +
   duration overlay + import button. License badge below the thumb. */
.v2-tool-stocklib { max-height: 80dvh; }
.v2-stocklib-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 10px;
  max-height: 56dvh;
  overflow-y: auto;
}
@media (min-width: 540px) {
  .v2-stocklib-grid { grid-template-columns: repeat(3, 1fr); }
}
.v2-stocklib-card {
  display: flex;
  flex-direction: column;
  gap: 6px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  overflow: hidden;
}
.v2-stocklib-thumb {
  position: relative;
  width: 100%;
  /* 16:9-ish aspect; works for both landscape video + most stock photos.
     Cropped via object-fit so portrait clips show their middle. */
  aspect-ratio: 16 / 10;
  background: #000;
  overflow: hidden;
}
.v2-stocklib-img,
.v2-stocklib-video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.v2-stocklib-dur {
  position: absolute;
  bottom: 6px; left: 6px;
  padding: 2px 6px;
  background: rgba(0, 0, 0, 0.62);
  color: #fff;
  border-radius: 4px;
  font: 700 10px/1 'JetBrains Mono', ui-monospace, monospace;
  letter-spacing: 0.04em;
  pointer-events: none;
}
.v2-stocklib-add {
  position: absolute;
  top: 6px; right: 6px;
  width: 30px; height: 30px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center; justify-content: center;
  background: var(--accent);
  color: var(--on-accent);
  box-shadow: 0 2px 8px rgba(0,0,0,0.5);
  transition: transform 90ms var(--ease), opacity 90ms var(--ease);
}
.v2-stocklib-add:hover { transform: scale(1.06); }
.v2-stocklib-add:active { transform: scale(0.94); }
.v2-stocklib-add.is-importing { opacity: 0.7; }
.v2-stocklib-meta {
  padding: 6px 8px 8px;
}

/* v2.0.374 — Stock library CapCut polish.
   - Sort chip strip (Popular / Latest)
   - Mood chips share styling with the audio library (v2-audiolib-moods)
   - Resolution badge (4K / HD) bottom-right of each card
   - Touch-friendly preview button bottom-left (sibling to duration chip)
   - Active preview state on the card to dim non-active cards */
.v2-stocklib-sort-row {
  display: flex;
  gap: 6px;
  margin: 6px 0 4px;
  flex-wrap: nowrap;
}
.v2-stocklib-sort {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  height: 28px;
  padding: 0 10px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--text-2);
  font: 600 12px/1 var(--font-ui, system-ui);
  transition: background 90ms var(--ease), color 90ms var(--ease),
              border-color 90ms var(--ease);
}
.v2-stocklib-sort.is-on {
  background: var(--accent);
  color: var(--on-accent);
  border-color: var(--accent);
}
.v2-stocklib-res {
  position: absolute;
  bottom: 6px; right: 6px;
  padding: 2px 5px;
  background: rgba(0, 0, 0, 0.72);
  color: #fff;
  border-radius: 4px;
  font: 800 9px/1 'JetBrains Mono', ui-monospace, monospace;
  letter-spacing: 0.06em;
  pointer-events: none;
}
.v2-stocklib-preview {
  position: absolute;
  /* Top-LEFT — leaves room for the + button top-right and the
     duration chip bottom-left without overlap. */
  top: 6px; left: 6px;
  width: 30px; height: 30px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center; justify-content: center;
  background: rgba(0, 0, 0, 0.62);
  color: #fff;
  box-shadow: 0 2px 8px rgba(0,0,0,0.5);
  transition: background 90ms var(--ease), transform 90ms var(--ease);
}
.v2-stocklib-preview.is-on {
  background: var(--accent);
  color: var(--on-accent);
}
.v2-stocklib-preview:active { transform: scale(0.92); }
.v2-stocklib-card.is-previewing {
  /* Subtle accent halo to draw the eye to the currently-playing card. */
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent);
}

/* v2.0.375 — Provider chip strip + per-card provider tag.
   The strip sits between the kind tabs and the search box; each chip
   scopes the search to one provider, or 'All' for the merged feed.
   The per-card tag tells the user which source served the clip so
   attribution + provenance are always visible. */
.v2-stocklib-providers {
  display: flex;
  gap: 6px;
  overflow-x: auto;
  scrollbar-width: none;
  padding: 6px 0 4px;
  margin: 0;
  -webkit-overflow-scrolling: touch;
}
.v2-stocklib-providers::-webkit-scrollbar { display: none; }
.v2-stocklib-provider {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto;
  height: 28px;
  padding: 0 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--text-2);
  font: 600 12px/1 var(--font-ui, system-ui);
  transition: background 90ms var(--ease), color 90ms var(--ease),
              border-color 90ms var(--ease);
  text-transform: capitalize;
}
.v2-stocklib-provider.is-active {
  background: var(--surface-3, var(--surface-2));
  color: var(--text-1);
  border-color: var(--text-3, var(--border));
}
.v2-stocklib-provider-dot {
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--accent);
  animation: v2-stocklib-refresh-pulse 1.2s ease-in-out infinite;
}
@keyframes v2-stocklib-refresh-pulse {
  0%, 100% { opacity: 0.35; transform: scale(0.8); }
  50%      { opacity: 1;    transform: scale(1.1); }
}
.v2-stocklib-meta {
  /* Override the earlier single-child padding to make room for the
     new provider-tag chip alongside the license badge. */
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  padding: 6px 8px 8px;
}
.v2-stocklib-provider-tag {
  display: inline-block;
  padding: 2px 6px;
  background: rgba(255, 255, 255, 0.06);
  color: var(--text-2);
  border-radius: 4px;
  font: 700 9px/1 'JetBrains Mono', ui-monospace, monospace;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
@media (prefers-reduced-motion: reduce) {
  .v2-stocklib-provider-dot { animation: none; }
}

/* v2.0.268 — Audio FX preset chips. Compact grid of preset cards;
   each card pairs a label with a stylized EQ-curve glyph so the user
   can see the SHAPE of the sound transformation at a glance, not
   just read its name. Smarter than CapCut's text-only chips. */
.v2-audiofx-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(78px, 1fr));
  gap: 8px;
  margin-top: 8px;
}
.v2-audiofx-chip {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 8px 6px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  color: var(--text-2);
  transition: background 140ms var(--ease), border-color 140ms var(--ease), color 140ms var(--ease);
  cursor: pointer;
}
.v2-audiofx-chip:hover { background: var(--surface-3); }
.v2-audiofx-chip.is-on {
  background: rgba(163, 255, 18, 0.08);
  border-color: var(--accent);
  color: var(--accent);
}
.v2-audiofx-chip-curve {
  width: 100%;
  display: block;
  color: currentColor;
}
.v2-audiofx-curve {
  width: 100%; height: 12px;
  display: block;
}
.v2-audiofx-chip-lbl {
  font: 600 10px/1.1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.01em;
  text-align: center;
}
.v2-tool-slider-row {
  display: grid;
  grid-template-columns: 36px 1fr 56px;
  align-items: center;
  gap: var(--s2);
}

/* =========================================================
   Font picker (v2.0.204) — used by the Text Style sheet's
   Font tab. Replaces the 5-chip horizontal strip with a real
   picker: sub-category tabs + search + wrap grid of font cards.

   Layout: this sits as a direct child of .v2-tool-sheet-body and
   needs to OWN its scrolling area. So .v2-font-grid scrolls
   internally; .v2-font-cats + .v2-font-search stay pinned at the
   top.
   ========================================================= */
.v2-font-picker {
  display: flex; flex-direction: column;
  gap: 10px;
}
.v2-font-cats {
  display: flex; gap: 4px;
  flex-wrap: wrap;
}
.v2-font-cat {
  height: 28px;
  padding: 0 11px;
  border-radius: 14px;
  background: transparent;
  color: var(--text-3);
  font: 700 11px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  border: 1px solid var(--border-2);
  touch-action: manipulation;
  transition:
    color   var(--t-fast) var(--ease),
    background var(--t-fast) var(--ease),
    border-color var(--t-fast) var(--ease),
    transform 140ms cubic-bezier(0.2, 1.0, 0.4, 1);
}
.v2-font-cat:active { transform: scale(0.94); }
.v2-font-cat.is-on {
  color: var(--accent);
  background: rgba(163, 255, 18, 0.10);
  border-color: rgba(163, 255, 18, 0.45);
}
.v2-font-search {
  position: relative;
  display: flex; align-items: center; gap: 8px;
  height: 36px;
  padding: 0 10px;
  background: var(--surface-2);
  border: 1px solid var(--border-2);
  border-radius: 10px;
  color: var(--text-3);
  flex: 0 0 auto;
}
.v2-font-search:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px rgba(163, 255, 18, 0.18);
}
.v2-font-search-input {
  flex: 1 1 auto;
  background: transparent;
  border: 0;
  outline: 0;
  color: var(--text);
  font: 600 14px/1 'Inter', system-ui, sans-serif;
  padding: 0;
  -webkit-user-select: text;
  user-select: text;
  min-width: 0;       /* let it shrink in narrow viewports */
}
.v2-font-search-input::placeholder { color: var(--text-3); }
.v2-font-search-input::-webkit-search-cancel-button { display: none; } /* we render our own X */
.v2-font-search-clear {
  width: 22px; height: 22px;
  border-radius: 50%;
  background: rgba(255,255,255,0.08);
  color: var(--text-2);
  display: inline-flex; align-items: center; justify-content: center;
  border: 0;
  touch-action: manipulation;
}
.v2-font-search-clear:active { transform: scale(0.92); }

/* Font cards — wrap grid, each card preview's font rendered in
   its own face so identification is instant. */
.v2-font-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(108px, 1fr));
  gap: 8px;
  padding: 2px 0 4px;
}
.v2-font-card {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  min-height: 76px;
  padding: 10px 6px 8px;
  background: var(--surface-2);
  border: 1.5px solid var(--border-2);
  border-radius: 12px;
  color: var(--text);
  touch-action: manipulation;
  text-align: center;
  gap: 4px;
  transition:
    border-color var(--t-fast) var(--ease),
    background var(--t-fast) var(--ease),
    transform 140ms cubic-bezier(0.2, 1.0, 0.4, 1);
}
.v2-font-card:active { transform: scale(0.96); }
.v2-font-card.is-on {
  border-color: var(--accent);
  background: rgba(163, 255, 18, 0.08);
  box-shadow: 0 0 0 2px rgba(163, 255, 18, 0.20);
}
.v2-font-card-aa {
  font-size: 26px;
  font-weight: 700;
  line-height: 1;
  color: var(--text);
}
.v2-font-card-name {
  font: 600 10px/1.2 'Inter', system-ui, sans-serif;   /* explicit, so card label never renders in the font being previewed */
  color: var(--text-3);
  letter-spacing: 0.02em;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.v2-font-card.is-on .v2-font-card-name { color: var(--accent); }
.v2-font-empty {
  grid-column: 1 / -1;
  padding: 24px 8px;
  text-align: center;
  color: var(--text-3);
  font: 500 13px/1.4 'Inter', system-ui, sans-serif;
}

/* Hide the chip-strip container when the font tab is active (font
   tab uses .v2-font-picker as its own root, not the row container). */
.v2-secondary-body--row.is-hidden { display: none; }

/* =========================================================
   Sticker picker (v2.0.208) — Tier 1, ship 5.
   Bottom sheet shape; tabs (Recent / Emoji-by-category / GIF) +
   wrap grids of emoji buttons and GIF thumbnails.
   ========================================================= */
.v2-tool-stickers { max-height: 55dvh; }  /* v2.0.491 — keep canvas visible (was 70dvh); audio/stock/template libraries stay tall */
.v2-sticker-empty {
  grid-column: 1 / -1;
  padding: 22px 8px;
  text-align: center;
  color: var(--text-3);
  font: 500 13px/1.4 'Inter', system-ui, sans-serif;
}
/* v2.0.503 — GIF-search retry affordance (when Tenor is unreachable/disabled). */
button.v2-sticker-gif-retry {
  background: transparent;
  border: 1px dashed rgba(255,255,255,0.18);
  border-radius: 10px;
  color: var(--text-2, #cbd5e1);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
button.v2-sticker-gif-retry:active { background: rgba(255,255,255,0.05); }
.v2-sticker-search {
  display: flex; align-items: center; gap: 8px;
  height: 38px;
  padding: 0 10px;
  background: var(--surface-2);
  border: 1px solid var(--border-2);
  border-radius: 10px;
  color: var(--text-3);
  margin-bottom: 10px;
}
.v2-sticker-search:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px rgba(163, 255, 18, 0.18);
}
.v2-sticker-search-input {
  flex: 1 1 auto;
  min-width: 0;
  background: transparent; border: 0; outline: 0;
  color: var(--text);
  font: 600 14px/1 'Inter', system-ui, sans-serif;
  -webkit-user-select: text; user-select: text;
}
.v2-sticker-search-input::placeholder { color: var(--text-3); }
.v2-sticker-search-go {
  flex: 0 0 auto;
  height: 28px;
  padding: 0 12px;
  border-radius: 14px;
  background: var(--accent); color: var(--on-accent);
  font: 700 12px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
}
.v2-sticker-search-go:active { transform: scale(0.96); }
.v2-sticker-emoji-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(46px, 1fr));
  gap: 6px;
}
.v2-sticker-emoji {
  width: 100%; aspect-ratio: 1 / 1;
  border-radius: 10px;
  background: var(--surface-2);
  border: 1px solid var(--border-2);
  font-size: 28px;
  line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer;
  touch-action: manipulation;
  transition: transform 120ms cubic-bezier(0.2, 1.0, 0.4, 1),
              border-color var(--t-fast) var(--ease),
              background var(--t-fast) var(--ease);
}
.v2-sticker-emoji:active {
  transform: scale(0.94);
  background: rgba(163, 255, 18, 0.08);
  border-color: rgba(163, 255, 18, 0.45);
}
.v2-sticker-gif-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(108px, 1fr));
  gap: 8px;
}
.v2-sticker-gif {
  width: 100%; aspect-ratio: 1 / 1;
  border-radius: 12px;
  background: var(--surface-2);
  border: 1.5px solid var(--border-2);
  overflow: hidden;
  cursor: pointer;
  touch-action: manipulation;
  padding: 0;
  transition: transform 120ms cubic-bezier(0.2, 1.0, 0.4, 1),
              border-color var(--t-fast) var(--ease);
}
.v2-sticker-gif:active {
  transform: scale(0.96);
  border-color: var(--accent);
}
.v2-sticker-gif img {
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
}

/* DOM-side GIF render on the canvas (centered at the overlay's
   x/y, scale already handled by the wrapping transform).
   v2.0.351 — opacity:0. The DOM <img> is INPUT-ONLY (drag/pinch hit
   target + GIF frame source for the renderer). The canvas paints the
   visible bitmap so blend_mode + globalAlpha + animations stay
   authoritative. Pre-v351 the DOM <img> painted on top of the canvas
   at z=30 → double-render ghosting on motion effects, and tints
   (blend modes like Multiply/Screen) were silently defeated because
   the unblended DOM copy covered the blended canvas copy. Export
   (canvas-only) then disagreed with preview. Parallel to
   .v2-canvas-sticker-content { color: transparent } at line 408. */
.v2-canvas-sticker-gif {
  display: block;
  opacity: 0;
  pointer-events: none;
  user-select: none;
  -webkit-user-select: none;
}

/* =========================================================
   Color picker (v2.0.205) — used by the Text Style sheet's
   Color/Stroke/Shadow/BG tabs + the studio background sheet.

   Layout: quick swatch grid → brand kit row → recents row →
   hue strip → saturation/value square → hex input + optional
   opacity slider. Picks the same idiom as CapCut: dense, touch-
   friendly, instant live preview while dragging.
   ========================================================= */
.v2-color-picker {
  display: flex; flex-direction: column;
  gap: 14px;
}
.v2-color-section {
  display: flex; flex-direction: column;
  gap: 8px;
}
.v2-color-section-lbl {
  font: 700 9px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-3);
}
.v2-color-swatches {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(36px, 1fr));
  gap: 6px;
}
.v2-color-swatch {
  width: 100%; aspect-ratio: 1 / 1;
  border-radius: 9px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1.5px solid rgba(255,255,255,0.10);
  cursor: pointer;
  touch-action: manipulation;
  transition: transform 120ms cubic-bezier(0.2, 1.0, 0.4, 1),
              border-color var(--t-fast) var(--ease),
              box-shadow var(--t-fast) var(--ease);
}
.v2-color-swatch:active { transform: scale(0.92); }
.v2-color-swatch.is-on {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px rgba(163, 255, 18, 0.40);
}
.v2-color-swatch.is-active {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px rgba(163, 255, 18, 0.40);
}

/* Saturation/Value square. background = pure hue from JSX.
   White overlay (left-to-right) + black overlay (top-to-bottom)
   produce the canonical HSV square. */
.v2-color-sv {
  position: relative;
  width: 100%;
  aspect-ratio: 1 / 1;
  max-height: 240px;
  border-radius: 12px;
  overflow: hidden;
  touch-action: none;           /* JS owns the drag */
  cursor: crosshair;
}
.v2-color-sv-white {
  position: absolute; inset: 0;
  background: linear-gradient(to right, #fff, transparent);
  pointer-events: none;
}
.v2-color-sv-black {
  position: absolute; inset: 0;
  background: linear-gradient(to top, #000, transparent);
  pointer-events: none;
}
.v2-color-sv-handle {
  position: absolute;
  width: 18px; height: 18px;
  border-radius: 50%;
  border: 3px solid #fff;
  box-shadow: 0 0 0 1px rgba(0,0,0,0.55), 0 2px 6px rgba(0,0,0,0.4);
  transform: translate(-50%, -50%);
  pointer-events: none;
}

/* Hue strip — horizontal rainbow gradient, full width. */
.v2-color-hue {
  position: relative;
  width: 100%;
  height: 28px;
  border-radius: 14px;
  touch-action: none;
  background: linear-gradient(to right,
    #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%,
    #0000ff 67%, #ff00ff 83%, #ff0000 100%);
}
.v2-color-hue-handle {
  position: absolute;
  top: 50%;
  width: 22px; height: 22px;
  border-radius: 50%;
  border: 3px solid #fff;
  box-shadow: 0 0 0 1px rgba(0,0,0,0.55), 0 2px 6px rgba(0,0,0,0.4);
  transform: translate(-50%, -50%);
  pointer-events: none;
}

/* Hex + opacity row. */
.v2-color-meta {
  display: grid;
  grid-template-columns: minmax(120px, max-content) 1fr;
  gap: var(--s3);
  align-items: center;
}
.v2-color-hex {
  display: inline-flex; align-items: center;
  height: 36px;
  padding: 0 10px 0 12px;
  background: var(--surface-2);
  border: 1px solid var(--border-2);
  border-radius: 10px;
  color: var(--text-3);
}
.v2-color-hex:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px rgba(163, 255, 18, 0.18);
}
.v2-color-hex-hash {
  font: 700 14px/1 'JetBrains Mono', ui-monospace, monospace;
  color: var(--text-3);
  margin-right: 2px;
}
.v2-color-hex-input {
  background: transparent;
  border: 0;
  outline: 0;
  color: var(--text);
  font: 700 14px/1 'JetBrains Mono', ui-monospace, monospace;
  letter-spacing: 0.04em;
  padding: 0;
  width: 8ch;          /* fits #RRGGBBAA */
  text-transform: uppercase;
  -webkit-user-select: text; user-select: text;
}
.v2-color-opacity {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 8px;
}
.v2-color-opacity-lbl {
  font: 700 9px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  color: var(--text-3);
}
.v2-color-opacity-slider { height: 28px; }
.v2-color-opacity-val {
  min-width: 44px;
  text-align: right;
  font: 700 12px/1 'JetBrains Mono', ui-monospace, monospace;
  color: var(--accent);
}

/* Neutralize old grid-column/grid-row positioning on inner content
   that pre-dated v2.0.197 and assumed the .v2-secondary grid context.
   Inside the sheet body these are just flex-column children, so the
   positioning should be auto. */
.v2-tool-sheet .v2-vol-presets,
.v2-tool-sheet .v2-speed-presets,
.v2-tool-sheet .v2-fade-presets,
.v2-tool-sheet .v2-anim-chips,
.v2-tool-sheet .v2-trans-chips,
.v2-tool-sheet .v2-trans-cats,
.v2-tool-sheet .v2-secondary-body--row,
.v2-tool-sheet .v2-secondary-tabs {
  grid-column: auto;
  grid-row: auto;
}
.v2-tool-sheet .v2-vol-presets,
.v2-tool-sheet .v2-fade-presets,
.v2-tool-sheet .v2-speed-presets {
  flex-wrap: wrap;
}

/* Legacy shim — a few panels (and the timeline tray) still construct
   .v2-secondary-icon-btn / .v2-secondary-body--row / .v2-stab buttons.
   Keep these as pure look-and-feel rules; the outer `.v2-secondary`
   grid container is gone in v2.0.197. */
.v2-secondary-body {
  display: flex; align-items: center; gap: var(--s3);
}
.v2-secondary-icon-btn {
  width: 36px; height: 36px;
  border-radius: 10px;
  background: var(--surface-2);
  color: var(--text-2);
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--border-2);
  flex: 0 0 auto;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
  touch-action: manipulation;
}
.v2-secondary-icon-btn:active { transform: scale(0.94); }
.v2-secondary-icon-btn.is-on {
  background: rgba(163, 255, 18, 0.14);
  border-color: var(--accent);
  color: var(--accent);
}

/* Slider — big, mobile-friendly, accent-colored. Custom thumb +
   track via vendor pseudo-elements. */
.v2-slider {
  flex: 1 1 auto;
  appearance: none;
  -webkit-appearance: none;
  height: 32px;            /* hit area; visible track is thinner */
  background: transparent;
  outline: 0;
  margin: 0;
  touch-action: none;
}
.v2-slider::-webkit-slider-runnable-track {
  height: 4px;
  background: var(--surface-3);
  border-radius: 2px;
}
.v2-slider::-moz-range-track {
  height: 4px;
  background: var(--surface-3);
  border-radius: 2px;
}
.v2-slider::-webkit-slider-thumb {
  appearance: none;
  -webkit-appearance: none;
  width: 22px; height: 22px;
  border-radius: 50%;
  background: var(--accent);
  border: 3px solid #0e1018;
  margin-top: -9px;        /* center on the 4px track */
  cursor: pointer;
  box-shadow: 0 2px 8px rgba(163, 255, 18, 0.45), 0 4px 12px rgba(0,0,0,0.4);
}
.v2-slider::-moz-range-thumb {
  width: 22px; height: 22px;
  border-radius: 50%;
  background: var(--accent);
  border: 3px solid #0e1018;
  cursor: pointer;
  box-shadow: 0 2px 8px rgba(163, 255, 18, 0.45), 0 4px 12px rgba(0,0,0,0.4);
}
.v2-slider-val {
  flex: 0 0 auto;
  min-width: 48px;
  text-align: right;
  font: 700 13px/1 'JetBrains Mono', ui-monospace, monospace;
  color: var(--accent);
  letter-spacing: 0.02em;
}

/* Chip row variant — for preset pickers like Speed (0.25x, 0.5x, ...) */
.v2-secondary-body--chips {
  gap: 8px;
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  padding-right: 8px;
}
.v2-secondary-body--chips::-webkit-scrollbar { display: none; }

/* v2.0.142 — Volume panel: taller variant to fit slider row + presets
   row. Auto rows: 18px title + auto slider + auto presets. */
.v2-secondary.v2-vol {
  height: 96px;
  grid-template-rows: 18px 1fr auto;
  padding-top: 6px; padding-bottom: 8px;
}
.v2-secondary.v2-vol .v2-secondary-title {
  grid-row: 1;
  margin-top: 0;
}
.v2-secondary.v2-vol .v2-vol-body {
  grid-row: 2;
}
.v2-secondary.v2-vol .v2-secondary-close {
  grid-row: 1 / span 3;
  align-self: center;
}
.v2-vol-slider-wrap {
  position: relative;
  flex: 1 1 auto;
  height: 32px;
  display: flex; align-items: center;
}
/* Visual fill bar under the slider thumb. Linear gradient lime → softer
   lime so the level reads as energetic but not screaming. */
.v2-vol-track {
  position: absolute;
  left: 0; right: 0;
  top: 50%; transform: translateY(-50%);
  height: 6px;
  background: rgba(255,255,255,0.05);
  border-radius: 3px;
  overflow: hidden;
  pointer-events: none;
}
.v2-vol-track-fill {
  height: 100%;
  background: linear-gradient(90deg, rgba(163, 255, 18, 0.6) 0%, var(--accent) 100%);
  border-radius: 3px;
  transition: width 80ms linear;
  box-shadow: 0 0 8px rgba(163, 255, 18, 0.45);
}
.v2-vol-slider {
  position: relative;
  z-index: 1;
  background: transparent;
}
/* Hide the slider's own runnable track since we have the visual track
   behind it. The thumb stays visible (the actual handle). */
.v2-vol-slider::-webkit-slider-runnable-track { background: transparent; }
.v2-vol-slider::-moz-range-track            { background: transparent; }

.v2-vol-mute.is-on {
  background: rgba(249, 119, 119, 0.18);
  color: #f97777;
  border-color: rgba(249, 119, 119, 0.4);
}

/* Preset chip row. */
.v2-vol-presets {
  grid-column: 2;
  grid-row: 3;
  display: flex; gap: 4px;
  justify-content: center;
}
.v2-vol-preset {
  flex: 0 0 auto;
  padding: 5px 10px;
  border-radius: 999px;
  background: rgba(255,255,255,0.04);
  border: 1px solid var(--border-2);
  color: var(--text-3);
  font: 700 10px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  touch-action: manipulation;
  cursor: pointer;
  transition: color var(--t-fast) var(--ease), background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.v2-vol-preset:active { transform: scale(0.94); }
.v2-vol-preset.is-on {
  background: rgba(163, 255, 18, 0.12);
  color: var(--accent);
  border-color: rgba(163, 255, 18, 0.45);
}

/* v2.0.296 — Volume keyframe section in VolumePanel.
   Discoverability surface for audio overlays (no canvas bbox =
   no diamond pin button) and convenience for PiP users. */
.v2-vol-kf {
  /* Subtle separator from the Quick-set row above. */
  position: relative;
}
.v2-vol-kf.is-keyframed .v2-tool-sec-lbl .v2-tool-sec-hint {
  color: var(--accent);
  font-weight: 700;
}
.v2-vol-envelope-wrap {
  margin: 8px 0 10px;
  padding: 6px 8px;
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid rgba(255, 255, 255, 0.06);
  border-radius: var(--r-sm);
}
.v2-vol-envelope {
  width: 100%;
  height: 56px;
  display: block;
  /* preserveAspectRatio="none" on the SVG element handles the
     non-uniform scaling — width fills the panel while height
     stays at 56px for consistent readability. */
}
.v2-vol-kf-row {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}
.v2-vol-kf-pin,
.v2-vol-kf-clear {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 14px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.04);
  color: var(--text-2);
  font: 700 11px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.02em;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
  touch-action: manipulation;
}
.v2-vol-kf-pin:hover,
.v2-vol-kf-clear:hover {
  background: rgba(255, 255, 255, 0.08);
  color: var(--text);
}
.v2-vol-kf-pin:active,
.v2-vol-kf-clear:active { transform: scale(0.96); }
.v2-vol-kf-pin.is-active {
  background: rgba(163, 255, 18, 0.10);
  color: var(--accent);
  border-color: rgba(163, 255, 18, 0.45);
}
.v2-vol-kf-clear {
  /* Subtle destructive accent so users don't fire this by accident
     while reaching for Pin. Hover is brighter; active stays the
     same hue but tightens. */
  color: rgba(255, 99, 132, 0.85);
  border-color: rgba(255, 99, 132, 0.25);
}
.v2-vol-kf-clear:hover {
  background: rgba(255, 99, 132, 0.10);
  color: rgba(255, 99, 132, 1);
  border-color: rgba(255, 99, 132, 0.45);
}
.v2-vol-kf-hint {
  margin-top: 8px;
  font: 500 11px/1.4 'Inter', system-ui, sans-serif;
  color: var(--text-3);
  opacity: 0.85;
}

/* v2.0.394 — L/R audio pan slider (VolumePanel). Center button on the
   left snaps to 0; range slider in the middle uses the same custom
   thumb as .v2-slider but the track has a center hairline + a fill
   that extends from center toward the active side. -1 = full left,
   0 = center, +1 = full right. Used by audio + PiP overlays. */
.v2-pan-center {
  /* Same base shape as .v2-secondary-icon-btn (36×36 / r10), but with
     a custom L · R glyph and a soft accent halo so users can spot
     the "snap back to center" affordance without reading copy. */
  font: 700 9px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
}
.v2-pan-center:hover {
  background: rgba(163, 255, 18, 0.10);
  color: var(--accent);
  border-color: rgba(163, 255, 18, 0.35);
}
.v2-pan-glyph {
  display: inline-flex;
  align-items: center;
  gap: 1px;
  /* Slightly tighter than the parent's letter-spacing so the bullet
     reads as the literal stereo midpoint. */
  letter-spacing: 0;
  pointer-events: none;
}
.v2-pan-slider-wrap {
  position: relative;
  flex: 1 1 auto;
  height: 32px;
  display: flex; align-items: center;
}
.v2-pan-track {
  position: absolute;
  left: 0; right: 0;
  top: 50%; transform: translateY(-50%);
  height: 6px;
  background: rgba(255,255,255,0.05);
  border-radius: 3px;
  pointer-events: none;
  /* Allow the fill child to extend with absolute positioning. */
}
.v2-pan-track-center {
  position: absolute;
  left: 50%;
  top: -2px; bottom: -2px;
  width: 2px;
  margin-left: -1px;
  background: rgba(255, 255, 255, 0.28);
  border-radius: 1px;
  pointer-events: none;
}
.v2-pan-track-fill {
  position: absolute;
  top: 0; bottom: 0;
  /* `left` + `width` set inline from the JSX based on pan sign so the
     fill grows outward from center toward the active channel. */
  background: linear-gradient(90deg, rgba(163, 255, 18, 0.55) 0%, var(--accent) 100%);
  border-radius: 3px;
  transition: left 80ms linear, width 80ms linear;
  box-shadow: 0 0 8px rgba(163, 255, 18, 0.4);
  pointer-events: none;
}
.v2-pan-slider {
  position: relative;
  z-index: 1;
  background: transparent;
}
/* Hide the slider's own runnable track so the custom .v2-pan-track
   shows through. The thumb (from .v2-slider) stays visible. */
.v2-pan-slider::-webkit-slider-runnable-track { background: transparent; }
.v2-pan-slider::-moz-range-track            { background: transparent; }

/* v2.0.146 — Speed panel: big readout + slider + preset chips with
   slow/fast color coding. Slow = cyan, Fast = lime. Adapts the same
   3-row layout as the v2.0.142 volume panel. */
.v2-secondary.v2-speed {
  height: 110px;
  grid-template-rows: 18px 1fr auto;
  padding-top: 6px; padding-bottom: 8px;
}
.v2-secondary.v2-speed .v2-secondary-title { grid-row: 1; margin-top: 0; }
.v2-secondary.v2-speed .v2-speed-body { grid-row: 2; }
.v2-secondary.v2-speed .v2-secondary-close { grid-row: 1 / span 3; align-self: center; }

.v2-speed-body {
  display: flex; align-items: center; gap: var(--s3);
}
.v2-speed-readout {
  flex: 0 0 auto;
  min-width: 70px;
  text-align: center;
  font: 900 26px/1 'JetBrains Mono', ui-monospace, monospace;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
  transition: color 200ms var(--ease);
}
.v2-speed-x {
  font-size: 16px;
  margin-left: 1px;
  color: var(--text-3);
  font-weight: 700;
}
.v2-speed-readout.is-slow { color: #06b6d4; }
.v2-speed-readout.is-fast { color: var(--accent); }
.v2-speed-slider-wrap {
  flex: 1 1 auto;
  position: relative;
  height: 32px;
  display: flex; align-items: center;
}
.v2-speed-slider-marks {
  position: absolute;
  left: 0; right: 0;
  top: 50%; transform: translateY(-50%);
  height: 16px;
  pointer-events: none;
}
.v2-speed-mark {
  position: absolute;
  top: 0; bottom: 0;
  width: 2px;
  background: rgba(255,255,255,0.20);
  border-radius: 1px;
}
.v2-speed-slider { flex: 1 1 auto; position: relative; z-index: 1; }

.v2-speed-presets {
  grid-column: 2;
  grid-row: 3;
  display: flex; gap: 4px;
  justify-content: center;
}
.v2-speed-chip {
  flex: 0 0 auto;
  padding: 5px 10px;
  border-radius: 999px;
  background: rgba(255,255,255,0.04);
  border: 1px solid var(--border-2);
  color: var(--text-3);
  font: 700 10px/1 'JetBrains Mono', ui-monospace, monospace;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  touch-action: manipulation;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.v2-speed-chip:active { transform: scale(0.94); }
.v2-speed-chip.is-slow:not(.is-on) { color: rgba(6, 182, 212, 0.7); }
.v2-speed-chip.is-fast:not(.is-on) { color: rgba(163, 255, 18, 0.55); }
.v2-speed-chip.is-on {
  background: rgba(163, 255, 18, 0.14);
  color: var(--accent);
  border-color: rgba(163, 255, 18, 0.55);
}
.v2-speed-chip.is-on.is-slow {
  background: rgba(6, 182, 212, 0.14);
  color: #06b6d4;
  border-color: rgba(6, 182, 212, 0.55);
}

/* v2.0.144 — Anim panel chips with mini-canvas previews. Each chip is
   a tiny dark canvas with a single lime block that animates per the
   selected anim id. tab class ('in' | 'out') reverses the direction
   so the exit-tab chips show the reverse motion. Pure CSS keyframes;
   continuously loops for discoverability. */
.v2-secondary.v2-anim { height: 116px; grid-template-rows: auto 1fr; padding-top: 4px; }
.v2-secondary.v2-anim .v2-secondary-close { grid-row: 1 / span 2; align-self: center; }
.v2-anim-chips {
  grid-column: 2;
  grid-row: 2;
  display: flex; gap: 6px;
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  padding: 4px 0 2px;
}
.v2-anim-chips::-webkit-scrollbar { display: none; }
.v2-anim-chip {
  flex: 0 0 auto;
  display: inline-flex; flex-direction: column; align-items: center; gap: 4px;
  padding: 2px 4px 4px;
  border-radius: 8px;
  background: transparent;
  border: 1.5px solid transparent;
  color: var(--text-3);
  cursor: pointer;
  touch-action: manipulation;
  transition:
    border-color var(--t-fast) var(--ease),
    background var(--t-fast) var(--ease),
    transform 140ms cubic-bezier(0.2, 1.0, 0.4, 1);
}
.v2-anim-chip:active { transform: scale(0.94); }
.v2-anim-chip.is-on {
  border-color: rgba(163, 255, 18, 0.55);
  background: rgba(163, 255, 18, 0.08);
  box-shadow: 0 0 0 2px rgba(163, 255, 18, 0.22);
}
.v2-anim-chip-lbl {
  font: 700 9px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-3);
  white-space: nowrap;
}
.v2-anim-chip.is-on .v2-anim-chip-lbl { color: var(--accent); }

.v2-anim-prev {
  position: relative;
  width: 56px; height: 40px;
  border-radius: 5px;
  overflow: hidden;
  background: #14181f;
  display: block;
}

/* v2.0.147 — Fade panel: SVG envelope shows the volume ramp visually.
   Polygon spans the full clip duration; both fades visible at once
   so the user sees how in + out shape the clip together. */
.v2-secondary.v2-fade {
  height: 130px;
  grid-template-rows: auto auto 1fr;
  padding-top: 6px; padding-bottom: 8px;
}
.v2-secondary.v2-fade .v2-secondary-close { grid-row: 1 / span 3; align-self: center; }
.v2-fade-body {
  display: grid;
  grid-template-columns: 1fr 110px 50px;
  align-items: center;
  gap: var(--s2);
}
.v2-fade-envelope {
  width: 100%;
  height: 32px;
  background: rgba(255, 255, 255, 0.03);
  border-radius: 4px;
  display: block;
  grid-column: 1;
}
.v2-fade-envelope-fill {
  fill: rgba(163, 255, 18, 0.20);
  transition: fill 160ms var(--ease);
}
.v2-fade-envelope-line {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.5;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}
.v2-fade-slider {
  grid-column: 2;
}
.v2-fade-body .v2-slider-val {
  grid-column: 3;
}
.v2-fade-presets {
  grid-column: 2;
  grid-row: 3;
  display: flex; gap: 4px;
  justify-content: center;
  align-items: center;
}
.v2-fade-chip {
  flex: 0 0 auto;
  padding: 5px 10px;
  border-radius: 999px;
  background: rgba(255,255,255,0.04);
  border: 1px solid var(--border-2);
  color: var(--text-3);
  font: 700 10px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  touch-action: manipulation;
  cursor: pointer;
  transition: color var(--t-fast) var(--ease), background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.v2-fade-chip:active { transform: scale(0.94); }
.v2-fade-chip.is-on {
  background: rgba(163, 255, 18, 0.12);
  color: var(--accent);
  border-color: rgba(163, 255, 18, 0.45);
}
.v2-fade-clear {
  flex: 0 0 auto;
  padding: 5px 9px;
  border-radius: 999px;
  background: transparent;
  color: var(--text-3);
  border: 1px solid var(--border-2);
  cursor: pointer;
  touch-action: manipulation;
  display: inline-flex; align-items: center;
}
.v2-fade-clear:active { transform: scale(0.94); background: rgba(255,255,255,0.04); }

.v2-anim-prev-block {
  position: absolute;
  left: 50%; top: 50%;
  width: 20px; height: 20px;
  margin-left: -10px; margin-top: -10px;
  background: var(--accent);
  border-radius: 3px;
  box-shadow: 0 0 6px rgba(163, 255, 18, 0.4);
  display: block;
}

/* None: static block, no animation. */
/* Fade: opacity 0 → 1 → 0. */
.v2-anim-prev--fade .v2-anim-prev-block { animation: a-fade 1800ms ease-in-out infinite; }
@keyframes a-fade { 0%, 30% { opacity: 0; } 50%, 70% { opacity: 1; } 100% { opacity: 0; } }

/* Pop: scale 0 → 1.2 → 1 → 0. */
.v2-anim-prev--pop .v2-anim-prev-block {
  animation: a-pop 1800ms cubic-bezier(0.34, 1.56, 0.64, 1) infinite;
  transform-origin: center;
}
@keyframes a-pop {
  0%, 30%  { opacity: 0; transform: scale(0); }
  45%      { opacity: 1; transform: scale(1.2); }
  55%, 70% { opacity: 1; transform: scale(1); }
  100%     { opacity: 0; transform: scale(0); }
}

/* Slide-up: starts off-screen below, slides up to center, settles. */
.v2-anim-prev--slide-up .v2-anim-prev-block { animation: a-slide-up 1800ms ease-in-out infinite; }
@keyframes a-slide-up    { 0%, 30% { opacity: 0; transform: translateY(35px); } 50%, 70% { opacity: 1; transform: translateY(0); } 100% { opacity: 0; transform: translateY(35px); } }

.v2-anim-prev--slide-down .v2-anim-prev-block { animation: a-slide-down 1800ms ease-in-out infinite; }
@keyframes a-slide-down  { 0%, 30% { opacity: 0; transform: translateY(-35px); } 50%, 70% { opacity: 1; transform: translateY(0); } 100% { opacity: 0; transform: translateY(-35px); } }

.v2-anim-prev--slide-left .v2-anim-prev-block { animation: a-slide-left 1800ms ease-in-out infinite; }
@keyframes a-slide-left  { 0%, 30% { opacity: 0; transform: translateX(40px); } 50%, 70% { opacity: 1; transform: translateX(0); } 100% { opacity: 0; transform: translateX(40px); } }

.v2-anim-prev--slide-right .v2-anim-prev-block { animation: a-slide-right 1800ms ease-in-out infinite; }
@keyframes a-slide-right { 0%, 30% { opacity: 0; transform: translateX(-40px); } 50%, 70% { opacity: 1; transform: translateX(0); } 100% { opacity: 0; transform: translateX(-40px); } }

/* Exit-tab variant: reverse the animation direction so chip previews
   show the OUTRO motion instead of the INTRO. The block fades OUT or
   slides AWAY by the end of the loop. */
.v2-anim-prev--out.v2-anim-prev--fade .v2-anim-prev-block       { animation-direction: reverse; }
.v2-anim-prev--out.v2-anim-prev--pop  .v2-anim-prev-block       { animation-direction: reverse; }
.v2-anim-prev--out.v2-anim-prev--slide-up    .v2-anim-prev-block { animation: a-slide-up-out    1800ms ease-in-out infinite; }
.v2-anim-prev--out.v2-anim-prev--slide-down  .v2-anim-prev-block { animation: a-slide-down-out  1800ms ease-in-out infinite; }
.v2-anim-prev--out.v2-anim-prev--slide-left  .v2-anim-prev-block { animation: a-slide-left-out  1800ms ease-in-out infinite; }
.v2-anim-prev--out.v2-anim-prev--slide-right .v2-anim-prev-block { animation: a-slide-right-out 1800ms ease-in-out infinite; }
@keyframes a-slide-up-out    { 0%, 30% { opacity: 1; transform: translateY(0); } 50%, 70% { opacity: 0; transform: translateY(-35px); } 100% { opacity: 1; transform: translateY(0); } }
@keyframes a-slide-down-out  { 0%, 30% { opacity: 1; transform: translateY(0); } 50%, 70% { opacity: 0; transform: translateY(35px); } 100% { opacity: 1; transform: translateY(0); } }
@keyframes a-slide-left-out  { 0%, 30% { opacity: 1; transform: translateX(0); } 50%, 70% { opacity: 0; transform: translateX(-40px); } 100% { opacity: 1; transform: translateX(0); } }
@keyframes a-slide-right-out { 0%, 30% { opacity: 1; transform: translateX(0); } 50%, 70% { opacity: 0; transform: translateX(40px); } 100% { opacity: 1; transform: translateX(0); } }

/* =========================================================
   v2.0.207 — Animation chip previews for the new presets.
   Each chip's mock block runs the same animation the renderer will,
   so users see what they're picking. Loops are continuous; in/out
   variants flash the block in or out within the 1800ms cycle.
   ========================================================= */

/* Bounce — overshoot pop with secondary settles. */
.v2-anim-prev--bounce .v2-anim-prev-block {
  animation: a-bounce 1800ms cubic-bezier(0.34, 1.56, 0.64, 1) infinite;
}
@keyframes a-bounce {
  0%, 30% { opacity: 0; transform: scale(0); }
  45%     { opacity: 1; transform: scale(1.45); }
  55%     { opacity: 1; transform: scale(0.85); }
  65%     { opacity: 1; transform: scale(1.10); }
  70%     { opacity: 1; transform: scale(1.00); }
  100%    { opacity: 0; transform: scale(0); }
}

/* Zoom — bigger pop-in than `pop`. */
.v2-anim-prev--zoom .v2-anim-prev-block {
  animation: a-zoom 1800ms cubic-bezier(0.2, 1.0, 0.4, 1) infinite;
}
@keyframes a-zoom {
  0%, 30% { opacity: 0; transform: scale(0.05); }
  50%, 70% { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(0.05); }
}

/* Drop — falls in from above with bounce settle. */
.v2-anim-prev--drop .v2-anim-prev-block {
  animation: a-drop 1800ms cubic-bezier(0.34, 1.56, 0.64, 1) infinite;
}
@keyframes a-drop {
  0%, 30% { opacity: 0; transform: translateY(-45px); }
  45% { opacity: 1; transform: translateY(8px); }
  55% { opacity: 1; transform: translateY(-4px); }
  65%, 70% { opacity: 1; transform: translateY(0); }
  100% { opacity: 0; transform: translateY(-45px); }
}

/* Flip-Y — flips around horizontal axis (3D). */
.v2-anim-prev--flip-y .v2-anim-prev-block {
  animation: a-flip-y 1800ms ease-in-out infinite;
}
@keyframes a-flip-y {
  0%, 30% { opacity: 0; transform: perspective(120px) rotateX(180deg) scale(0.7); }
  50%, 70% { opacity: 1; transform: perspective(120px) rotateX(0deg) scale(1); }
  100% { opacity: 0; transform: perspective(120px) rotateX(180deg) scale(0.7); }
}

/* Rotate-in — spins from -90deg to 0. */
.v2-anim-prev--rotate .v2-anim-prev-block {
  animation: a-rotate-in 1800ms cubic-bezier(0.2, 1.0, 0.4, 1) infinite;
}
@keyframes a-rotate-in {
  0%, 30% { opacity: 0; transform: rotate(-90deg) scale(0.6); }
  50%, 70% { opacity: 1; transform: rotate(0deg) scale(1); }
  100% { opacity: 0; transform: rotate(-90deg) scale(0.6); }
}

/* Blur-in — fades + sharpens in. */
.v2-anim-prev--blur-in .v2-anim-prev-block {
  animation: a-blur-in 1800ms ease-in-out infinite;
}
@keyframes a-blur-in {
  0%, 30% { opacity: 0; filter: blur(8px); }
  50%, 70% { opacity: 1; filter: blur(0); }
  100% { opacity: 0; filter: blur(8px); }
}

/* ---- Loop animations — run continuously, no in/out swap.
        The 'loop' chip tab uses these. ---- */
.v2-anim-prev--loop.v2-anim-prev--pulse .v2-anim-prev-block {
  animation: a-loop-pulse 1400ms ease-in-out infinite;
}
@keyframes a-loop-pulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.18); }
}
.v2-anim-prev--loop.v2-anim-prev--shake .v2-anim-prev-block {
  animation: a-loop-shake 600ms cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes a-loop-shake {
  0%, 100% { transform: translate(0, 0); }
  20% { transform: translate(-3px, 2px); }
  40% { transform: translate(3px, -2px); }
  60% { transform: translate(-2px, 3px); }
  80% { transform: translate(2px, -3px); }
}
.v2-anim-prev--loop.v2-anim-prev--glitch .v2-anim-prev-block {
  animation: a-loop-glitch 1100ms steps(1, end) infinite;
}
@keyframes a-loop-glitch {
  0%, 10% { transform: translate(0, 0); opacity: 1; filter: blur(0); }
  12%     { transform: translate(5px, 0);  opacity: 0.7; filter: blur(0.6px); }
  14%     { transform: translate(-4px, 0); opacity: 0.8; filter: blur(0.6px); }
  16%, 50% { transform: translate(0, 0); opacity: 1; filter: blur(0); }
  52%     { transform: translate(-3px, 0); opacity: 0.7; filter: blur(0.6px); }
  56%, 100% { transform: translate(0, 0); opacity: 1; filter: blur(0); }
}
.v2-anim-prev--loop.v2-anim-prev--neon .v2-anim-prev-block {
  animation: a-loop-neon 900ms ease-in-out infinite;
}
@keyframes a-loop-neon {
  0%, 100% { opacity: 1; box-shadow: 0 0 6px rgba(163, 255, 18, 0.7); }
  50% { opacity: 0.7; box-shadow: 0 0 3px rgba(163, 255, 18, 0.3); }
}
.v2-anim-prev--loop.v2-anim-prev--heartbeat .v2-anim-prev-block {
  animation: a-loop-heartbeat 1200ms ease-in-out infinite;
}
@keyframes a-loop-heartbeat {
  0%, 20%, 40%, 100% { transform: scale(1); }
  10% { transform: scale(1.16); }
  30% { transform: scale(1.10); }
}
.v2-anim-prev--loop.v2-anim-prev--float .v2-anim-prev-block {
  animation: a-loop-float 2400ms ease-in-out infinite;
}
@keyframes a-loop-float {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-7px); }
}

/* v2.0.394 — Alignment chip variant: icon + label side-by-side inside
   the chip. Used by Text Style → Align tab. Visual icon (3 horizontal
   rules with alignment-matching offsets) precedes a short label for
   immediate recognition. */
.v2-chip.v2-chip--align {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 0 12px 0 10px;
}
.v2-chip.v2-chip--align .v2-chip-label {
  font: inherit;
}
.v2-chip {
  flex: 0 0 auto;
  height: 36px;
  padding: 0 14px;
  border-radius: var(--r-pill);
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--border-2);
  font: 700 13px/1 'JetBrains Mono', ui-monospace, monospace;
  letter-spacing: 0.02em;
  display: inline-flex; align-items: center; justify-content: center;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
  touch-action: manipulation;
}
.v2-chip:active { transform: scale(0.94); }
.v2-chip.is-on {
  background: var(--accent);
  color: var(--on-accent);
  border-color: var(--accent);
}

/* Tabbed secondary toolbar — for the Text Style panel which holds
   multiple groups (Color/Font/Size/BG). Taller variant with a tab
   row below the title and the controls row below that. */
.v2-secondary--tabbed {
  height: 96px;
  grid-template-columns: 36px 1fr;
  grid-template-rows: auto 1fr;
  padding: 8px var(--s3) 12px;
  align-items: start;
}
.v2-secondary--tabbed .v2-secondary-close { grid-row: 1 / span 2; align-self: center; }
.v2-secondary--tabbed .v2-secondary-tabs {
  grid-column: 2;
  display: flex; gap: 6px;
  margin-top: 2px;
}
.v2-stab {
  flex: 0 0 auto;
  height: 28px;
  padding: 0 12px;
  border-radius: 14px;
  background: transparent;
  color: var(--text-3);
  font: 700 11px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  border: 1px solid transparent;
  touch-action: manipulation;
  transition: color var(--t-fast) var(--ease), background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
}
/* v2.0.121 — Category tabs above the transition chip strip. Compact
   pills, lime accent on active. Includes ★ (favorites) and Recent as
   synthetic tabs whose membership is dynamic (localStorage-backed). */
.v2-trans-cats {
  grid-column: 2;
  display: flex; gap: 4px;
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  padding: 2px 0;
}
.v2-trans-cats::-webkit-scrollbar { display: none; }
.v2-trans-cat {
  flex: 0 0 auto;
  padding: 3px 9px;
  border-radius: 999px;
  background: transparent;
  color: var(--text-3);
  font: 700 9px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  border: 1px solid transparent;
  touch-action: manipulation;
  transition: color var(--t-fast) var(--ease), background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.v2-trans-cat:active { transform: scale(0.94); }
.v2-trans-cat.is-on {
  color: var(--accent);
  background: rgba(163, 255, 18, 0.10);
  border-color: rgba(163, 255, 18, 0.28);
}

/* v2.0.113 — Transition picker. Mini-canvas chips with continuously
   animated previews so the user sees what each transition looks like
   without trial-and-error. Layout: 56px chip (40px preview + 10px
   label) on row 1; reset + slider + applyAll on row 2. */
.v2-trans-chips {
  grid-column: 2;
  display: flex;
  gap: 6px;
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  padding: 4px 0 2px;
  margin-top: -2px;
}
.v2-trans-chips::-webkit-scrollbar { display: none; }
.v2-trans-chip {
  flex: 0 0 auto;
  display: inline-flex; flex-direction: column; align-items: center; gap: 4px;
  padding: 2px 4px 4px;
  border-radius: 8px;
  background: transparent;
  border: 1.5px solid transparent;
  color: var(--text-3);
  cursor: pointer;
  touch-action: manipulation;
  transition:
    border-color var(--t-fast) var(--ease),
    background var(--t-fast) var(--ease),
    transform 140ms cubic-bezier(0.2, 1.0, 0.4, 1),
    box-shadow var(--t-fast) var(--ease);
}
.v2-trans-chip:active { transform: scale(0.94); }
.v2-trans-chip.is-on {
  border-color: rgba(163, 255, 18, 0.55);
  background: rgba(163, 255, 18, 0.08);
  box-shadow:
    0 0 0 2px rgba(163, 255, 18, 0.22),
    0 0 10px rgba(163, 255, 18, 0.22);
}
.v2-trans-chip-lbl {
  font: 700 9px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-3);
  white-space: nowrap;
}
.v2-trans-chip.is-on .v2-trans-chip-lbl { color: var(--accent); }

/* v2.0.121 — Star button on each chip. Sits in the top-right corner of
   the chip's preview canvas. Tap toggles favorite — propagation
   stopped on pointer-down + click so the chip itself isn't selected. */
.v2-trans-star {
  position: absolute;
  top: 1px; right: 2px;
  width: 16px; height: 16px;
  background: rgba(10, 11, 16, 0.55);
  color: rgba(255, 255, 255, 0.55);
  border: 0;
  border-radius: 50%;
  font: 700 10px/1 'Inter', system-ui, sans-serif;
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer;
  touch-action: manipulation;
  transition: color var(--t-fast) var(--ease), background var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.v2-trans-star:active { transform: scale(0.86); }
.v2-trans-star.is-on {
  color: #fef08a;
  background: rgba(10, 11, 16, 0.85);
  text-shadow: 0 0 4px rgba(254, 240, 138, 0.6);
}
.v2-trans-chip { position: relative; }

/* Empty-state shown when a category has no transitions. */
.v2-trans-empty {
  flex: 1 1 auto;
  display: flex; align-items: center; justify-content: center;
  padding: 0 16px;
  color: var(--text-3);
  font: 500 11px/1.3 'Inter', system-ui, sans-serif;
  text-align: center;
  letter-spacing: 0.01em;
}

/* Mini-preview canvas. Two stacked color blocks (A=cyan, B=pink). Each
   transition class animates A and B via CSS keyframes so the user sees
   the effect cycling at ~2.4s intervals. */
.v2-trans-prev {
  position: relative;
  width: 56px; height: 40px;
  border-radius: 5px;
  overflow: hidden;
  background: #1a1d28;
  display: block;
}
.v2-trans-prev-a,
.v2-trans-prev-b {
  position: absolute; inset: 0;
  display: block;
}
/* v2.0.116 — Background longhand so the inline style="background-image:
   url(...)" overrides only the image while size:cover and position:center
   are inherited from these rules. (The shorthand `background:` would
   reset size to auto when the inline image takes over, and the frame
   wouldn't fill the chip.) Cyan/pink fallback is the default image. */
.v2-trans-prev-a {
  background-image: linear-gradient(135deg, #0891b2 0%, #06b6d4 100%);
  background-size: cover;
  background-position: center;
}
.v2-trans-prev-b {
  background-image: linear-gradient(135deg, #db2777 0%, #ec4899 100%);
  background-size: cover;
  background-position: center;
}

/* None: half/half hard cut indicator — a vertical seam down the middle. */
.v2-trans-prev--none .v2-trans-prev-a { clip-path: inset(0 50% 0 0); }
.v2-trans-prev--none .v2-trans-prev-b { clip-path: inset(0 0 0 50%); }

/* All animations cycle 2400ms ease-in-out infinite. Sequence is:
   0..30%   = A visible only
   30..50%  = transition happens (B becomes visible)
   50..70%  = B holds visible
   70..100% = transition back (B becomes A again) — symmetric so the
              loop reads as "tap, transition, hold." */
.v2-trans-prev--cross-fade .v2-trans-prev-b { animation: t-cross 2400ms ease-in-out infinite; }
@keyframes t-cross { 0%, 30% { opacity: 0; } 50%, 70% { opacity: 1; } 100% { opacity: 0; } }

.v2-trans-prev--cross-zoom .v2-trans-prev-a { animation: t-cz-out 2400ms ease-in-out infinite; transform-origin: center; }
.v2-trans-prev--cross-zoom .v2-trans-prev-b { animation: t-cz-in 2400ms ease-in-out infinite; transform-origin: center; }
@keyframes t-cz-in  { 0%, 30% { opacity: 0; transform: scale(1.5); } 50%, 70% { opacity: 1; transform: scale(1); } 100% { opacity: 0; transform: scale(1.5); } }
@keyframes t-cz-out { 0%, 30% { transform: scale(1); } 50%, 70% { transform: scale(1.5); opacity: 0.3; } 100% { transform: scale(1); opacity: 1; } }

.v2-trans-prev--fade-black { position: relative; }
.v2-trans-prev--fade-black::after {
  content: ''; position: absolute; inset: 0; background: #000;
  animation: t-dip 2400ms ease-in-out infinite;
}
.v2-trans-prev--fade-black .v2-trans-prev-b { animation: t-cross 2400ms ease-in-out infinite; }
@keyframes t-dip { 0%, 30% { opacity: 0; } 45%, 55% { opacity: 1; } 70%, 100% { opacity: 0; } }

.v2-trans-prev--fade-white::after {
  content: ''; position: absolute; inset: 0; background: #fff;
  animation: t-dip 2400ms ease-in-out infinite;
}
.v2-trans-prev--fade-white .v2-trans-prev-b { animation: t-cross 2400ms ease-in-out infinite; }

.v2-trans-prev--slide-up    .v2-trans-prev-b { animation: t-slide-up    2400ms ease-in-out infinite; }
.v2-trans-prev--slide-down  .v2-trans-prev-b { animation: t-slide-down  2400ms ease-in-out infinite; }
.v2-trans-prev--slide-left  .v2-trans-prev-b { animation: t-slide-left  2400ms ease-in-out infinite; }
.v2-trans-prev--slide-right .v2-trans-prev-b { animation: t-slide-right 2400ms ease-in-out infinite; }
@keyframes t-slide-up    { 0%, 30% { transform: translateY( 100%); } 50%, 70% { transform: translateY(0); } 100% { transform: translateY( 100%); } }
@keyframes t-slide-down  { 0%, 30% { transform: translateY(-100%); } 50%, 70% { transform: translateY(0); } 100% { transform: translateY(-100%); } }
@keyframes t-slide-left  { 0%, 30% { transform: translateX( 100%); } 50%, 70% { transform: translateX(0); } 100% { transform: translateX( 100%); } }
@keyframes t-slide-right { 0%, 30% { transform: translateX(-100%); } 50%, 70% { transform: translateX(0); } 100% { transform: translateX(-100%); } }

.v2-trans-prev--whip-left  .v2-trans-prev-b { animation: t-whip-left  2400ms cubic-bezier(0.2, 0.8, 0.2, 1) infinite; }
.v2-trans-prev--whip-right .v2-trans-prev-b { animation: t-whip-right 2400ms cubic-bezier(0.2, 0.8, 0.2, 1) infinite; }
@keyframes t-whip-left  { 0%, 30% { transform: translateX( 100%); filter: blur(0); } 40% { filter: blur(3px); } 50%, 70% { transform: translateX(0); filter: blur(0); } 80% { filter: blur(3px); } 100% { transform: translateX( 100%); filter: blur(0); } }
@keyframes t-whip-right { 0%, 30% { transform: translateX(-100%); filter: blur(0); } 40% { filter: blur(3px); } 50%, 70% { transform: translateX(0); filter: blur(0); } 80% { filter: blur(3px); } 100% { transform: translateX(-100%); filter: blur(0); } }

.v2-trans-prev--iris .v2-trans-prev-b {
  animation: t-iris 2400ms ease-in-out infinite;
  clip-path: circle(0% at 50% 50%);
}
@keyframes t-iris { 0%, 30% { clip-path: circle(0% at 50% 50%); } 50%, 70% { clip-path: circle(75% at 50% 50%); } 100% { clip-path: circle(0% at 50% 50%); } }

.v2-trans-prev--zoom-in  .v2-trans-prev-b { animation: t-zoom-in  2400ms ease-in-out infinite; transform-origin: center; }
.v2-trans-prev--zoom-out .v2-trans-prev-b { animation: t-zoom-out 2400ms ease-in-out infinite; transform-origin: center; }
@keyframes t-zoom-in  { 0%, 30% { opacity: 0; transform: scale(1.6); } 50%, 70% { opacity: 1; transform: scale(1); } 100% { opacity: 0; transform: scale(1.6); } }
@keyframes t-zoom-out { 0%, 30% { opacity: 0; transform: scale(0.4); } 50%, 70% { opacity: 1; transform: scale(1); } 100% { opacity: 0; transform: scale(0.4); } }

.v2-trans-prev--spin .v2-trans-prev-b {
  animation: t-spin 2400ms cubic-bezier(0.2, 1, 0.4, 1) infinite;
  transform-origin: center;
}
@keyframes t-spin { 0%, 30% { opacity: 0; transform: rotate(-90deg) scale(0.4); } 50%, 70% { opacity: 1; transform: rotate(0) scale(1); } 100% { opacity: 0; transform: rotate(-90deg) scale(0.4); } }

.v2-trans-prev--blur-in .v2-trans-prev-b { animation: t-blur-in 2400ms ease-in-out infinite; }
@keyframes t-blur-in { 0%, 30% { opacity: 0; filter: blur(8px); } 50%, 70% { opacity: 1; filter: blur(0); } 100% { opacity: 0; filter: blur(8px); } }

.v2-trans-prev--glitch .v2-trans-prev-b { animation: t-glitch-b 2400ms steps(20, end) infinite; }
.v2-trans-prev--glitch::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(0deg, transparent 0%, rgba(255,0,128,0.4) 30%, transparent 50%, rgba(0,200,255,0.4) 70%, transparent 100%);
  mix-blend-mode: screen;
  animation: t-glitch-veil 2400ms steps(8, end) infinite;
}
@keyframes t-glitch-b    { 0%, 30% { opacity: 0; transform: translateX(0); } 40% { opacity: 1; transform: translateX(-4px); } 45% { transform: translateX(3px); } 50%, 70% { opacity: 1; transform: translateX(0); } 80% { transform: translateX(2px); } 100% { opacity: 0; transform: translateX(0); } }
@keyframes t-glitch-veil { 0%, 30%, 70%, 100% { opacity: 0; } 35%, 65% { opacity: 1; } }

/* v2.0.127 — Push: both A and B translate together. Outgoing slides
   out, incoming slides in from the other side. */
.v2-trans-prev--push-left  .v2-trans-prev-a { animation: t-push-out-l 2400ms ease-in-out infinite; }
.v2-trans-prev--push-left  .v2-trans-prev-b { animation: t-push-in-l  2400ms ease-in-out infinite; }
.v2-trans-prev--push-right .v2-trans-prev-a { animation: t-push-out-r 2400ms ease-in-out infinite; }
.v2-trans-prev--push-right .v2-trans-prev-b { animation: t-push-in-r  2400ms ease-in-out infinite; }
@keyframes t-push-out-l { 0%, 30% { transform: translateX(0); } 50%, 70% { transform: translateX(-100%); } 100% { transform: translateX(0); } }
@keyframes t-push-in-l  { 0%, 30% { transform: translateX(100%); } 50%, 70% { transform: translateX(0); } 100% { transform: translateX(100%); } }
@keyframes t-push-out-r { 0%, 30% { transform: translateX(0); } 50%, 70% { transform: translateX(100%); } 100% { transform: translateX(0); } }
@keyframes t-push-in-r  { 0%, 30% { transform: translateX(-100%); } 50%, 70% { transform: translateX(0); } 100% { transform: translateX(-100%); } }

/* Shake: B fades in with rapid jitter that settles. */
.v2-trans-prev--shake .v2-trans-prev-b { animation: t-shake 2400ms ease-out infinite; }
@keyframes t-shake {
  0%, 30%       { opacity: 0; transform: translate(0, 0); }
  35%           { opacity: 1; transform: translate(-3px,  2px); }
  37%           { transform: translate( 2px, -3px); }
  39%           { transform: translate(-2px,  1px); }
  41%           { transform: translate( 3px, -1px); }
  43%           { transform: translate(-1px, -2px); }
  45%, 70%      { transform: translate(0, 0); }
  100%          { opacity: 0; transform: translate(0, 0); }
}

/* Drop: B falls from above with a bounce. */
.v2-trans-prev--drop .v2-trans-prev-b { animation: t-drop 2400ms cubic-bezier(0.34, 1.56, 0.64, 1) infinite; }
@keyframes t-drop {
  0%, 30%   { opacity: 0; transform: translateY(-100%); }
  50%       { opacity: 1; transform: translateY(8%); }
  55%       { transform: translateY(-3%); }
  60%, 70%  { transform: translateY(0); }
  100%      { opacity: 0; transform: translateY(-100%); }
}

/* Heart: similar to iris but with a heart-shaped clip path. */
.v2-trans-prev--heart .v2-trans-prev-b {
  animation: t-heart 2400ms ease-in-out infinite;
  clip-path: path('M28,38 C40,18 56,18 56,28 C56,38 28,52 28,52 C28,52 0,38 0,28 C0,18 16,18 28,38 Z');
}
@keyframes t-heart { 0%, 30% { transform: scale(0); } 50%, 70% { transform: scale(2.2); } 100% { transform: scale(0); } }

/* Light leak: orange/pink gradient sweeps in screen-blend. */
.v2-trans-prev--light-leak::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(135deg, rgba(255,140,0,0.7) 0%, rgba(255,80,120,0.5) 45%, rgba(255,230,100,0.4) 75%, rgba(255,80,0,0) 100%);
  mix-blend-mode: screen;
  animation: t-light-leak 2400ms ease-in-out infinite;
}
.v2-trans-prev--light-leak .v2-trans-prev-b { animation: t-cross 2400ms ease-in-out infinite; }
@keyframes t-light-leak {
  0%, 30%       { opacity: 0; transform: translateX(-30%); }
  45%, 55%      { opacity: 1; transform: translateX(0); }
  70%, 100%     { opacity: 0; transform: translateX(30%); }
}

/* Body row for transition panel: presets · slider · DUR · sound · All */
.v2-trans-body {
  display: flex; align-items: center; gap: 8px;
}

/* v2.0.122 — Duration preset chips (Fast / Default / Slow). Three
   tightly-packed pills on the left of the slider. Active chip gets the
   accent ring matching the "is-on" treatment used elsewhere. Disabled
   when no transition is selected. */
.v2-trans-dur-presets {
  display: inline-flex; gap: 2px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--border-2);
  border-radius: 999px;
  padding: 2px;
}
.v2-trans-dur-chip {
  padding: 4px 9px;
  border-radius: 999px;
  background: transparent;
  color: var(--text-3);
  border: 0;
  font: 700 9px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  touch-action: manipulation;
  cursor: pointer;
  transition: color var(--t-fast) var(--ease), background var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.v2-trans-dur-chip:active:not(:disabled) { transform: scale(0.92); }
.v2-trans-dur-chip.is-on {
  color: var(--accent);
  background: rgba(163, 255, 18, 0.16);
  box-shadow: 0 0 0 1px rgba(163, 255, 18, 0.32) inset;
}
.v2-trans-dur-chip:disabled { opacity: 0.36; pointer-events: none; }
.v2-trans-action {
  flex: 0 0 auto;
  height: 28px;
  padding: 0 10px;
  border-radius: 14px;
  background: rgba(255,255,255,0.04);
  color: var(--text-2);
  border: 1px solid var(--border-2);
  font: 700 10px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  display: inline-flex; align-items: center; justify-content: center; gap: 4px;
  touch-action: manipulation;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.v2-trans-action:active:not(:disabled) { transform: scale(0.94); }
.v2-trans-action:disabled { opacity: 0.32; pointer-events: none; }
.v2-trans-action--reset { width: 32px; padding: 0; }
.v2-trans-action--applyall {
  color: var(--accent);
  background: rgba(163, 255, 18, 0.10);
  border-color: rgba(163, 255, 18, 0.32);
}
.v2-trans-action--applyall:active:not(:disabled) { background: rgba(163, 255, 18, 0.22); }

/* v2.0.117 — Sound on/off toggle for transition SFX. Icon-only chip
   shows speaker (on) or muted speaker (off). Active state uses the
   accent ring same as other on/off toggles. */
.v2-trans-action--sound { width: 32px; padding: 0; }
.v2-trans-action--sound.is-on {
  color: var(--accent);
  background: rgba(163, 255, 18, 0.10);
  border-color: rgba(163, 255, 18, 0.32);
}
.v2-trans-dur {
  flex: 0 0 auto;
  min-width: 36px;
  text-align: center;
  font: 700 11px/1 'JetBrains Mono', ui-monospace, monospace;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

/* Make the transition variant of the tabbed secondary taller so the
   category row + 56px preview chips + body row all fit.
   v2.0.121 — now 3 rows: cats / chips / body. */
.v2-secondary--tabbed:has(.v2-trans-chips) {
  height: 138px;
  grid-template-rows: auto auto 1fr;
}
.v2-secondary--tabbed:has(.v2-trans-chips) .v2-secondary-close {
  grid-row: 1 / span 3;
}

.v2-stab.is-on {
  color: var(--accent);
  background: rgba(163, 255, 18, 0.12);
  border-color: rgba(163, 255, 18, 0.32);
}

/* v2.0.94 (Phase 2 C1) — Filter preset carousel inside the tabbed
   secondary toolbar. Horizontal scroll, each preset = colored swatch
   + label. Active preset has accent halo + scale lift. */
.v2-filter-presets {
  grid-column: 2;
  grid-row: 2;
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: 10px;
  overflow-x: auto;
  scrollbar-width: none;
  padding: 0 0 4px;
  margin-top: 2px;
  scroll-snap-type: x proximity;
}
.v2-filter-presets::-webkit-scrollbar { display: none; }
.v2-filter-preset {
  flex: 0 0 auto;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 2px;
  background: none;
  border: 0;
  color: inherit;
  cursor: pointer;
  touch-action: manipulation;
  scroll-snap-align: start;
  position: relative;
}
.v2-filter-preset:active { transform: scale(0.94); }
.v2-filter-preset-swatch {
  width: 44px;
  height: 44px;
  border-radius: 10px;
  border: 2px solid transparent;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
  background-position: center;
  background-size: cover;
  background-color: #3a3e4a;     /* fallback while thumb generates */
  /* v2.0.107 polish — slight intro fade on first paint so swatches
     don't pop when thumbnails resolve. The thumb generator sets
     background-image after a RAF; the transition catches that. */
  transition:
    transform 160ms cubic-bezier(0.2, 1.0, 0.4, 1),
    border-color var(--t-fast) var(--ease),
    box-shadow var(--t-fast) var(--ease),
    background-image 220ms var(--ease);
}
.v2-filter-preset.is-on .v2-filter-preset-swatch {
  border-color: var(--accent);
  transform: scale(1.08);
  /* v2.0.107 polish — beefier accent glow on active chip + an inner
     ring that mimics CapCut's 'press here to deep-link' affordance.
     The user gets a clear "this is the one" signal at a glance. */
  box-shadow:
    0 0 0 3px rgba(163, 255, 18, 0.22),
    0 0 14px rgba(163, 255, 18, 0.30),
    0 2px 6px rgba(163, 255, 18, 0.28);
}
/* v2.0.107 polish — tiny "double-tap to reset" hint dot above the
   active chip's label. CSS-only; non-active chips don't show it.
   Disappears on press so it doesn't get in the way during interaction. */
.v2-filter-preset.is-on::after {
  content: '';
  position: absolute;
  top: -3px; right: 4px;
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 6px rgba(163, 255, 18, 0.65);
  pointer-events: none;
  opacity: 0.85;
}
.v2-filter-preset:active::after { opacity: 0; }
.v2-filter-preset-lbl {
  font: 600 9px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-3);
  white-space: nowrap;
}
.v2-filter-preset.is-on .v2-filter-preset-lbl {
  color: var(--accent);
}

/* v2.0.96 (C1.1) — intensity slider row, shown beneath the preset
   carousel when a non-Original preset is active. Squeezes into the
   tabbed-secondary toolbar's existing height by tightening the
   carousel paddings. */
.v2-filter-presets-wrap {
  grid-column: 2;
  grid-row: 2;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-height: 0;
}

/* v2.0.98 (C1.3) — Category tab strip above the preset carousel.
   Compact pills, horizontal scroll on overflow. Active tab gets the
   accent ring; inactive are quiet so the carousel underneath stays
   the visual focus. */
.v2-filter-cats {
  display: flex;
  gap: 4px;
  overflow-x: auto;
  scrollbar-width: none;
  padding-bottom: 2px;
}
.v2-filter-cats::-webkit-scrollbar { display: none; }
.v2-filter-cat {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; gap: 5px;
  padding: 4px 10px;
  border-radius: 999px;
  background: transparent;
  color: var(--text-3);
  font: 700 9px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  border: 1px solid transparent;
  touch-action: manipulation;
  transition: color var(--t-fast) var(--ease), background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.v2-filter-cat:active { transform: scale(0.94); }
.v2-filter-cat.is-on {
  color: var(--accent);
  background: rgba(163, 255, 18, 0.10);
  border-color: rgba(163, 255, 18, 0.28);
}
/* v2.0.107 polish — tonal dot per category. Helps the user pre-read
   the tab strip by color instead of word — Warm jumps as amber, Cool
   as cyan, Mono as silver. CapCut uses the same pattern on its filter
   row. The dot is muted (50%) on inactive chips and full on active. */
.v2-filter-cat-dot {
  width: 6px; height: 6px;
  border-radius: 50%;
  opacity: 0.55;
  transition: opacity var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.v2-filter-cat.is-on .v2-filter-cat-dot {
  opacity: 1;
  box-shadow: 0 0 6px currentColor;
}
.v2-filter-presets-wrap .v2-filter-presets {
  grid-column: unset;
  grid-row: unset;
  padding-bottom: 0;
  margin-top: 0;
}
.v2-filter-intensity {
  display: flex; align-items: center; gap: 8px;
  padding: 0 2px;
  font: 600 10px/1 'Inter', system-ui, sans-serif;
}
.v2-filter-intensity-lbl {
  color: var(--text-3);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  flex: 0 0 auto;
}
.v2-filter-intensity .v2-slider { flex: 1; min-width: 0; }
/* v2.0.107 polish — tabular-num readout for the intensity slider.
   Matches the .v2-slider-val mono treatment elsewhere so the value
   doesn't shimmy as digits change. */
.v2-filter-intensity-val {
  flex: 0 0 auto;
  width: 42px;
  text-align: right;
  color: var(--accent);
  font: 700 11px/1 'JetBrains Mono', ui-monospace, monospace;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
}
.v2-filter-applyall {
  flex: 0 0 auto;
  padding: 4px 10px;
  border-radius: 999px;
  background: rgba(163, 255, 18, 0.10);
  border: 1px solid rgba(163, 255, 18, 0.32);
  color: var(--accent);
  font: 700 9px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  touch-action: manipulation;
  transition: background var(--t-fast) var(--ease);
}
.v2-filter-applyall:active {
  background: rgba(163, 255, 18, 0.22);
  transform: scale(0.96);
}

/* v2.0.102 (A1.3) — Caption style chips inside the TextStylePanel's
   Preset tab. Each chip has an 'Aa' preview that renders in the
   actual style so the user can see at a glance what the tap will do.
   Smaller than filter chips because text styles are visual on tap;
   no need for thumbnail real estate. */
.v2-capstyle {
  flex: 0 0 auto;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 5px 9px 6px;
  border-radius: 10px;
  background: transparent;
  border: 1px solid transparent;
  color: inherit;
  cursor: pointer;
  touch-action: manipulation;
  transition:
    border-color var(--t-fast) var(--ease),
    background var(--t-fast) var(--ease),
    box-shadow var(--t-fast) var(--ease),
    transform 140ms cubic-bezier(0.2, 1.0, 0.4, 1);
}
/* v2.0.109 polish — tighter press scale matches filter chip feel. */
.v2-capstyle:active { transform: scale(0.94); }
.v2-capstyle.is-on {
  border-color: rgba(163, 255, 18, 0.55);
  background: rgba(163, 255, 18, 0.10);
  /* Beefier ring + soft outer glow so the active chip reads at a glance
     in a busy chip strip. CapCut's caption-style row uses the same
     "chosen" treatment. */
  box-shadow:
    0 0 0 2px rgba(163, 255, 18, 0.22),
    0 0 12px rgba(163, 255, 18, 0.24);
  transform: scale(1.04);
}
.v2-capstyle-preview {
  /* Inline-style overrides set color/font/shadow/stroke/bg at render time. */
  display: inline-block;
  line-height: 1;
  min-width: 16px;
  height: 22px;
  display: inline-flex; align-items: center; justify-content: center;
  text-align: center;
}
.v2-capstyle-lbl {
  font: 700 9px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-3);
  white-space: nowrap;
}
.v2-capstyle.is-on .v2-capstyle-lbl {
  color: var(--accent);
}
.v2-secondary-body--row {
  grid-column: 2;
  display: flex; align-items: center; gap: 8px;
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  padding: 4px 0;
}
.v2-secondary-body--row::-webkit-scrollbar { display: none; }

/* Color swatch — round, accent ring when selected. */
.v2-swatch {
  flex: 0 0 auto;
  width: 36px; height: 36px;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, 0.18);
  transition: transform var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
  touch-action: manipulation;
}
.v2-swatch:active { transform: scale(0.92); }
.v2-swatch.is-on {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(163, 255, 18, 0.25);
}

/* Font chip — slightly different from generic chip so each chip
   can show in its own font face. */
.v2-chip--font {
  min-width: 64px;
  font-weight: 800;
}

/* =========================================================
   Home — minimal project picker for v2 dev
   ========================================================= */
.v2-home {
  position: fixed; inset: 0;
  display: flex; flex-direction: column;
  background:
    radial-gradient(1200px 600px at 90% -10%, rgba(34, 197, 94, 0.10), transparent 60%),
    radial-gradient(900px 500px at -10% 100%, rgba(16, 185, 129, 0.07), transparent 60%),
    var(--bg);
  color: var(--text);
  overflow-y: auto; overflow-x: hidden;
  padding-top: env(safe-area-inset-top, 0px);
  padding-bottom: env(safe-area-inset-bottom, 0px);
}
.v2-home-head {
  padding: var(--s6) var(--s4) var(--s5);
  display: flex; align-items: baseline; gap: var(--s2);
  flex-wrap: wrap;
}
.v2-home-back {
  flex: 0 0 100%;
  display: inline-flex; align-items: center; gap: 3px; width: max-content;
  font: 800 13px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.01em;
  color: var(--text-3); text-decoration: none;
  margin-bottom: 12px;
  transition: color 120ms var(--ease);
}
.v2-home-back:hover { color: var(--text); }
.v2-home-title {
  font: 900 44px/1 'Inter', system-ui, sans-serif;
  letter-spacing: -0.04em;
  background: linear-gradient(120deg, #10b981 0%, #22c55e 52%, #84cc16 100%);
  -webkit-background-clip: text;
          background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent;
  padding-bottom: 2px;
}
.v2-home-sub {
  font: 700 12px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  color: var(--text-3);
  align-self: center;
}

/* Primary "Create with AI" action — leads to the generator (studio projects are
   made there, then edited here). Brand-gradient icon on a clean dark surface. */
.v2-home-new {
  display: flex; align-items: center; gap: 14px;
  margin: 0 var(--s4) var(--s5);
  padding: 16px 18px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  cursor: pointer; touch-action: manipulation; text-align: left; text-decoration: none;
  transition: transform var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
.v2-home-new:hover { border-color: rgba(34, 197, 94, 0.45); background: var(--surface-2); }
.v2-home-new:active { transform: scale(0.99); }
.v2-home-new-icon {
  flex: 0 0 auto;
  width: 44px; height: 44px;
  border-radius: 13px;
  background: linear-gradient(135deg, #10b981 0%, #22c55e 55%, #84cc16 100%);
  color: #04130b;
  display: inline-flex; align-items: center; justify-content: center;
  box-shadow: 0 4px 16px rgba(34, 197, 94, 0.32);
}
.v2-home-new-body {
  flex: 1 1 auto;
  display: flex; flex-direction: column; gap: 2px;
  min-width: 0;
}
.v2-home-new-title {
  font: 800 15px/1.2 'Inter', system-ui, sans-serif;
  color: var(--text);
}
.v2-home-new-sub {
  font: 600 11px/1.3 'Inter', system-ui, sans-serif;
  color: var(--text-3);
  letter-spacing: 0.01em;
}
.v2-home-new-arrow { color: var(--text-3); flex: 0 0 auto; }

/* two-entry action row: New project (primary) + Create with AI (secondary) */
.v2-home-actions { display: flex; flex-direction: column; gap: 10px; margin: 0 var(--s4) var(--s5); }
.v2-home-actions .v2-home-new { margin: 0; width: 100%; box-sizing: border-box; font: inherit; }
.v2-home-new--primary { border-color: rgba(34, 197, 94, 0.45); }
.v2-home-new--primary:hover { border-color: rgba(34, 197, 94, 0.72); }
.v2-home-new[disabled] { opacity: 0.6; pointer-events: none; }
/* the AI secondary uses a subtler icon (not the full brand gradient) */
.v2-home-new-icon--alt { background: var(--surface-3); color: var(--brand-500); box-shadow: none; }
.v2-home-new--primary .v2-home-new-arrow { color: var(--brand-500); }

/* v2.0.138 — Section heading above the list. Helps the user understand
   the gallery is "Recent." Future: add "Drafts" / "Templates" sections. */
.v2-home-section-lbl {
  padding: 0 var(--s4) var(--s2);
  font: 700 11px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-3);
}

/* A responsive gallery grid of project cards (a "visual" home, not a flat list). */
.v2-home-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
  gap: 14px; padding: 0 var(--s4) var(--s6); min-width: 0;
}
/* Phones: a denser 2-up gallery (the 210px min would otherwise force 1 column). */
@media (max-width: 560px) { .v2-home-grid { grid-template-columns: 1fr 1fr; gap: 10px; } }
.v2-home-card {
  display: flex; flex-direction: column; padding: 0; overflow: visible; min-width: 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  text-align: left; cursor: pointer; touch-action: manipulation; position: relative;
  transition: border-color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.v2-home-card:hover { border-color: rgba(34, 197, 94, 0.40); transform: translateY(-2px); box-shadow: 0 10px 28px rgba(0,0,0,0.42); }
.v2-home-card:active { transform: translateY(0); }
/* When a card's ⋯ menu / rename / confirm is open, lift it ABOVE the fixed
   click-catcher backdrop (z-index:4) — otherwise the card's own stacking context
   traps the menu below the backdrop and taps hit the backdrop (menu "opens" but the
   actions don't fire). z-index needs position (already relative). */
.v2-home-card.is-active { z-index: 20; }
.v2-home-card-cover {
  position: relative; aspect-ratio: 16 / 10; display: grid; place-items: center; overflow: hidden;
  border-radius: var(--r-md) var(--r-md) 0 0;
}
.v2-home-card-glyph {
  font: 900 40px/1 'Inter', system-ui, sans-serif; color: rgba(255,255,255,0.95);
  text-shadow: 0 2px 8px rgba(0,0,0,0.45); letter-spacing: -0.03em;
}
.v2-home-card-body { padding: 11px 13px 13px; min-width: 0; }
.v2-home-card-name {
  font: 700 14px/1.25 'Inter', system-ui, sans-serif; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.v2-home-card-meta {
  font: 600 11px/1 'Inter', system-ui, sans-serif; letter-spacing: 0.02em; color: var(--text-3); margin-top: 5px;
}

/* per-card management: ⋯ kebab → rename / duplicate / delete menu */
.v2-home-card-kebab {
  position: absolute; top: 7px; right: 7px; z-index: 3;
  width: 30px; height: 30px; border-radius: 9px; border: 0; cursor: pointer;
  background: rgba(8, 10, 14, 0.55); color: #fff; font-size: 19px; line-height: 1; font-weight: 700;
  display: grid; place-items: center; backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  opacity: 0; transition: opacity var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
.v2-home-card:hover .v2-home-card-kebab, .v2-home-card-kebab:focus-visible { opacity: 1; }
@media (hover: none) { .v2-home-card-kebab { opacity: 1; } }
.v2-home-card-kebab:hover { background: rgba(8, 10, 14, 0.82); }
.v2-home-card-busy { position: absolute; inset: 0; display: grid; place-items: center; background: rgba(6, 7, 11, 0.5); z-index: 2; }
.v2-home-card-rename {
  width: 100%; box-sizing: border-box; font: 700 14px/1.25 'Inter', system-ui, sans-serif;
  color: var(--text); background: var(--surface-2); border: 1.5px solid #22c55e;
  border-radius: 8px; padding: 5px 8px; outline: none;
}
.v2-home-card-menu {
  position: absolute; top: 44px; right: 8px; z-index: 6; min-width: 170px;
  background: var(--surface-2); border: 1px solid var(--border-2); border-radius: 12px;
  padding: 5px; box-shadow: 0 16px 44px rgba(0, 0, 0, 0.55);
  display: flex; flex-direction: column; gap: 2px;
  animation: v2-home-menu-in 120ms var(--ease);
}
@keyframes v2-home-menu-in { from { opacity: 0; transform: translateY(-4px) scale(0.98); } }
.v2-home-menu-item {
  display: flex; align-items: center; gap: 9px; padding: 9px 11px; border: 0; border-radius: 8px;
  background: transparent; color: var(--text); font: 600 13px/1 'Inter', system-ui, sans-serif;
  cursor: pointer; text-align: left; width: 100%;
}
.v2-home-menu-item:hover { background: var(--surface-3); }
.v2-home-menu-item.is-danger { color: #ff6b6b; }
.v2-home-menu-item.is-danger:hover { background: rgba(255, 80, 80, 0.12); }
.v2-home-menu-confirm { padding: 8px 11px 5px; font: 700 12px/1.3 'Inter', system-ui, sans-serif; color: var(--text-2); }
.v2-home-menu-backdrop { position: fixed; inset: 0; z-index: 4; background: transparent; }

.v2-home-empty {
  margin: var(--s5) var(--s4);
  padding: 40px 24px;
  text-align: center;
  background: var(--surface);
  border: 1px dashed var(--border-2);
  border-radius: var(--r-md);
  display: flex; flex-direction: column; align-items: center; gap: 10px;
}
.v2-home-empty-icon {
  width: 64px; height: 64px;
  border-radius: 50%;
  background: rgba(34, 197, 94, 0.12);
  color: #22c55e;
  display: inline-flex; align-items: center; justify-content: center;
  margin-bottom: 6px;
}
.v2-home-empty-title {
  font: 800 16px/1.2 'Inter', system-ui, sans-serif;
  color: var(--text);
}
.v2-home-empty-sub {
  font: 500 12px/1.4 'Inter', system-ui, sans-serif;
  color: var(--text-3);
  max-width: 280px;
}
.v2-home-empty-actions { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; margin-top: 6px; }
.v2-home-empty-cta {
  padding: 11px 20px; border-radius: 999px; text-decoration: none; border: 0; cursor: pointer;
  font: 800 13px/1 'Inter', system-ui, sans-serif; color: #04130b;
  background: linear-gradient(120deg, #10b981 0%, #22c55e 52%, #84cc16 100%);
  box-shadow: 0 4px 16px rgba(34, 197, 94, 0.30);
}
.v2-home-empty-cta:active { transform: translateY(1px); }
.v2-home-empty-cta--ghost {
  background: transparent; color: var(--brand-500); box-shadow: none;
  border: 1.5px solid var(--border-2); font-weight: 700;
}

/* v2.0.138 — Skeleton variant of v2-home-item. Same layout dims so
   the swap real → loading is invisible. */
.v2-home-skeleton { pointer-events: none; }
.v2-home-skeleton-block {
  background: var(--surface-2);
  overflow: hidden;
  position: relative;
}
.v2-home-skeleton-block::after,
.v2-home-skeleton-line::after {
  content: '';
  position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.04) 50%, transparent 100%);
  animation: v2-shimmer 1400ms ease-in-out infinite;
}
.v2-home-skeleton-line {
  background: var(--surface-2);
  border-radius: 4px;
  position: relative;
  overflow: hidden;
}
.v2-home-skeleton-line--lg { width: 65%; height: 14px; margin-bottom: 8px; }
.v2-home-skeleton-line--sm { width: 40%; height: 10px; }

/* Loading skeleton — placeholder cards that mimic .v2-home-card layout so the
   swap skeleton → real gallery is visually invisible. The cover block + body
   lines shimmer via their own ::after (see .v2-home-skeleton-block/-line). */
@keyframes v2-shimmer {
  from { transform: translateX(-100%); }
  to   { transform: translateX(100%);  }
}

/* =========================================================
   Tiny utilities
   ========================================================= */
.v2-spinner {
  width: 18px; height: 18px; border-radius: 50%;
  border: 2px solid var(--border);
  border-top-color: var(--accent);
  animation: v2-spin 700ms linear infinite;
  display: inline-block;
}
@keyframes v2-spin { to { transform: rotate(360deg); } }

/* =========================================================
   Upload progress (Path A.2)
   Floating glass card top-right of the editor. Stays visible
   as long as files are in flight. Includes per-file label,
   "n of m" counter, percent, and a progress bar.
   ========================================================= */
.v2-upload-progress {
  position: fixed;
  top: calc(60px + env(safe-area-inset-top, 0px));
  right: 16px;
  z-index: 9999;
  display: flex; align-items: center; gap: 12px;
  padding: 12px 14px;
  min-width: 240px; max-width: 320px;
  background: rgba(20, 22, 28, 0.85);
  backdrop-filter: blur(24px) saturate(160%);
  -webkit-backdrop-filter: blur(24px) saturate(160%);
  border: 1px solid rgba(163, 255, 18, 0.25);
  border-radius: var(--r-md);
  box-shadow: 0 12px 32px rgba(0,0,0,0.45);
  animation: v2-upload-slide 240ms var(--ease) 1;
}
@keyframes v2-upload-slide {
  from { opacity: 0; transform: translateY(-8px); }
  to   { opacity: 1; transform: translateY(0);    }
}
.v2-upload-spinner {
  width: 20px; height: 20px; border-radius: 50%;
  border: 2px solid rgba(163, 255, 18, 0.18);
  border-top-color: var(--accent);
  animation: v2-spin 700ms linear infinite;
  flex: 0 0 auto;
}
.v2-upload-info {
  flex: 1 1 auto;
  display: flex; flex-direction: column; gap: 4px;
  min-width: 0;
}
.v2-upload-label {
  font: 700 12px/1.2 'Inter', system-ui, sans-serif;
  color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.v2-upload-meta {
  font: 600 10px/1.1 'JetBrains Mono', ui-monospace, monospace;
  letter-spacing: 0.04em;
  color: var(--text-3);
}
.v2-upload-bar {
  height: 3px;
  background: var(--surface-2);
  border-radius: 2px;
  overflow: hidden;
  margin-top: 2px;
}
.v2-upload-bar-fill {
  height: 100%;
  background: linear-gradient(90deg, var(--accent), #d6ff66);
  border-radius: 2px;
  transition: width 120ms linear;
}

/* Persistent error banner — replaces auto-fading toast for uploads.
   Stays visible until user dismisses. Top-center of viewport. v2.0.51. */
.v2-upload-error {
  position: fixed;
  top: calc(60px + env(safe-area-inset-top, 0px));
  left: 50%;
  transform: translateX(-50%);
  z-index: 10000;
  display: flex; align-items: center; gap: 10px;
  padding: 12px 14px;
  min-width: 280px; max-width: 92vw;
  background: rgba(249, 119, 119, 0.16);
  backdrop-filter: blur(20px) saturate(140%);
  -webkit-backdrop-filter: blur(20px) saturate(140%);
  border: 1px solid rgba(249, 119, 119, 0.45);
  border-radius: var(--r-md);
  box-shadow: 0 12px 32px rgba(0,0,0,0.5);
  color: #fff;
  animation: v2-upload-slide 240ms var(--ease) 1;
}
.v2-upload-error-icon {
  flex: 0 0 auto;
  width: 26px; height: 26px;
  border-radius: 50%;
  background: #f97777;
  color: #fff;
  display: inline-flex; align-items: center; justify-content: center;
  font: 800 14px/1 'Inter', system-ui, sans-serif;
}
.v2-upload-error-info { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.v2-upload-error-title {
  font: 700 12px/1.2 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  color: #ffb4b4;
}
.v2-upload-error-msg {
  font: 600 11px/1.3 'Inter', system-ui, sans-serif;
  color: #fff;
  word-break: break-word;
}
.v2-upload-error-dismiss {
  flex: 0 0 auto;
  width: 28px; height: 28px;
  border-radius: 50%;
  background: rgba(255,255,255,0.08);
  color: #fff;
  border: 1px solid rgba(255,255,255,0.10);
  display: inline-flex; align-items: center; justify-content: center;
  touch-action: manipulation;
}
.v2-upload-error-dismiss:active { transform: scale(0.92); }

/* v2.0.105 (A1.6) — Retry button on caption-error banner. Same banner
   shape as upload errors but with an inline "Retry" affordance so the
   user re-fires the transcribe request without re-opening the picker. */
.v2-upload-error-retry {
  flex: 0 0 auto;
  padding: 6px 12px;
  border-radius: 999px;
  background: #a3ff12;
  color: #0a0b10;
  border: 0;
  font: 800 11px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  touch-action: manipulation;
  cursor: pointer;
}
.v2-upload-error-retry:active { transform: scale(0.94); }

/* v2.0.161 — Broken-clips banner: same shape as the upload-error banner
   but stacks under it so both can coexist if an upload failed while
   broken clips were already in the project. Tinted lighter so the
   alarm pitch matches the situation (it's a fixable warning, not a
   freshly-failed action). */
.v2-broken-banner {
  top: calc(120px + env(safe-area-inset-top, 0px));
  background: rgba(249, 180, 119, 0.16);
  border-color: rgba(249, 180, 119, 0.45);
}
.v2-broken-banner .v2-upload-error-icon { background: #f9b477; color: #0a0b10; }
.v2-broken-banner .v2-upload-error-title { color: #ffd9b4; }
/* v2.0.162 — Two-button banner: Retry (primary lime) + Remove
   (secondary outline). Retry is the lower-risk action so it gets
   the visual emphasis; Remove is destructive so it's outlined. */
.v2-broken-retry { background: #a3ff12 !important; color: #0a0b10 !important; }
.v2-broken-remove {
  background: transparent !important;
  color: #ffd9b4 !important;
  border: 1px solid rgba(255, 217, 180, 0.55) !important;
}
.v2-broken-remove:active { transform: scale(0.94); }

/* v2.0.105 (A1.6) — Captions sheet (source picker + position chips).
   Bottom-sheet pattern reused; just the inner grid is custom. */
.v2-captions-sheet {
  padding: 8px 16px 20px;
  display: flex; flex-direction: column; gap: 18px;
}
.v2-captions-section { display: flex; flex-direction: column; gap: 8px; }
.v2-captions-section-lbl {
  font: 700 11px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-2);
}
.v2-captions-sources {
  display: flex; flex-direction: column; gap: 6px;
  max-height: 38dvh; overflow-y: auto;
}
.v2-captions-src {
  display: flex; align-items: center; gap: 12px;
  min-height: 56px;                  /* v2.0.106 — Apple HIG 44px+ */
  padding: 12px 14px;
  background: var(--surface);
  border: 1px solid var(--border-2);
  border-radius: var(--r-md);
  color: var(--text-1);
  font: 600 13px/1 'Inter', system-ui, sans-serif;
  touch-action: manipulation;
  text-align: left;
  transition: transform 120ms var(--ease), background 120ms var(--ease), border-color 120ms var(--ease);
}
.v2-captions-src:active { transform: scale(0.985); }
.v2-captions-src.is-active {
  background: rgba(163, 255, 18, 0.10);
  border-color: rgba(163, 255, 18, 0.55);
  box-shadow: 0 0 0 1px rgba(163, 255, 18, 0.30) inset;
}
/* v2.0.106 polish — color-coded icon badge per source kind so the
   eye instantly groups by track type. Cyan for audio, violet for video. */
.v2-captions-src-icon {
  flex: 0 0 auto;
  width: 32px; height: 32px;
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  color: #0a0b10;
  background: #06b6d4;
}
.v2-captions-src--video .v2-captions-src-icon { background: #a855f7; }
.v2-captions-src-lbl { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.v2-captions-src-dur {
  flex: 0 0 auto;
  font: 600 11px/1 'JetBrains Mono', ui-monospace, monospace;
  color: var(--text-2);
  letter-spacing: 0.02em;
}
.v2-captions-pos {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px;
}
.v2-captions-pos-btn {
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  padding: 12px 8px;
  background: var(--surface);
  border: 1px solid var(--border-2);
  border-radius: var(--r-md);
  color: var(--text-1);
  font: 600 12px/1 'Inter', system-ui, sans-serif;
  touch-action: manipulation;
  cursor: pointer;
}
.v2-captions-pos-btn.is-active {
  background: rgba(163, 255, 18, 0.10);
  border-color: rgba(163, 255, 18, 0.55);
  box-shadow: 0 0 0 1px rgba(163, 255, 18, 0.30) inset;
}
.v2-captions-pos-preview {
  position: relative;
  width: 56px; height: 84px;
  background: linear-gradient(180deg, #1f2937 0%, #0f1419 100%);
  border-radius: 6px;
  overflow: hidden;
}
.v2-captions-pos-bar {
  position: absolute;
  left: 8px; right: 8px;
  height: 8px;
  background: #fef08a;
  border-radius: 2px;
  box-shadow: 0 1px 0 rgba(0,0,0,0.6);
}
.v2-captions-pos-preview--top    .v2-captions-pos-bar { top: 12px; }
.v2-captions-pos-preview--middle .v2-captions-pos-bar { top: 50%; transform: translateY(-50%); }
.v2-captions-pos-preview--bottom .v2-captions-pos-bar { bottom: 12px; }
.v2-captions-pos-lbl { letter-spacing: 0.02em; }
.v2-captions-generate {
  margin-top: 4px;
  padding: 14px 20px;
  border-radius: 999px;
  background: #a3ff12;
  color: #0a0b10;
  border: 0;
  font: 800 14px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  touch-action: manipulation;
  cursor: pointer;
}
.v2-captions-generate:active { transform: scale(0.97); }

/* v2.0.106 — In-sheet loading + success states. Replaces the
   close-then-toast race with a stay-mounted spinner that morphs into
   a checkmark when the captions are committed. */
.v2-captions-state {
  display: flex; flex-direction: column; align-items: center; gap: 14px;
  padding: 32px 24px 36px;
  text-align: center;
}
.v2-captions-state-title {
  font: 700 14px/1.2 'Inter', system-ui, sans-serif;
  letter-spacing: 0.02em;
  color: var(--text);
}
.v2-captions-state-sub {
  font: 600 11px/1.3 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-2);
}
.v2-captions-spinner {
  width: 44px; height: 44px;
  border-radius: 50%;
  border: 3px solid rgba(168, 85, 247, 0.18);
  border-top-color: #a855f7;
  animation: v2-spin 720ms linear infinite;
}
/* v2.0.257 — Cancel button on the loading state. Restrained look
   (text-like) so it doesn't compete with the spinner; tappable
   target sized for thumb. */
.v2-captions-cancel {
  margin-top: 8px;
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-2);
  font: 600 12px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 10px 22px;
  border-radius: 999px;
  cursor: pointer;
  transition: background 140ms var(--ease), color 140ms var(--ease), border-color 140ms var(--ease);
}
.v2-captions-cancel:hover {
  background: rgba(255, 255, 255, 0.04);
  color: var(--text);
}
.v2-captions-cancel:active {
  background: rgba(255, 255, 255, 0.08);
}
.v2-captions-state--success .v2-captions-check {
  width: 56px; height: 56px;
  border-radius: 50%;
  background: rgba(163, 255, 18, 0.18);
  border: 2px solid rgba(163, 255, 18, 0.55);
  color: var(--accent);
  display: inline-flex; align-items: center; justify-content: center;
  animation: v2-captions-pop 320ms cubic-bezier(0.18, 1.2, 0.4, 1) 1;
}
@keyframes v2-captions-pop {
  0%   { transform: scale(0.4); opacity: 0; }
  60%  { transform: scale(1.12); opacity: 1; }
  100% { transform: scale(1); }
}
/* Modal height eases between phases so it doesn't jump on phase change.
   pick is taller (lots of chips), success/loading are short — letting
   the modal animate height keeps the bottom-sheet feeling continuous. */
.v2-captions-modal {
  transition: min-height 200ms var(--ease);
}
.v2-captions-modal--loading,
.v2-captions-modal--success {
  min-height: 220px;
}

/* =========================================================
   v2.0.300 — Background Removal + Chroma Key (Cutout panel).
   Two-method toggle, eyedropper, sliders, backdrop picker,
   per-keyframe diamonds. All inside a ToolSheet — inherits
   bottom-sheet scrolling + dismiss-on-swipe-down.
   ========================================================= */
.v2-tool-cutout {
  /* v2.0.490 — was 86dvh, which all but covered the canvas. Match the standard
     sheet cap (55dvh, inherited from .v2-tool-sheet) so cutout behaves like
     every other tool tab: it floats above the ribbon and leaves the canvas
     visible + tappable. This matters most for AI subject-pick, where the user
     taps the subject ON the canvas — they need to see it. The body already
     scrolls, so the extra controls just scroll instead of stealing the canvas. */
  max-height: 55dvh;
}

/* Mode toggle — two big cards, chroma | AI */
.v2-cutout-mode-toggle {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  margin-top: 8px;
}
.v2-cutout-mode-btn {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
  padding: 14px 14px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  color: var(--text-2);
  cursor: pointer;
  transition: background var(--t-fast) var(--ease),
              border-color var(--t-fast) var(--ease),
              color var(--t-fast) var(--ease),
              transform var(--t-fast) var(--ease);
}
.v2-cutout-mode-btn:hover { background: var(--surface-3); }
.v2-cutout-mode-btn:active { transform: scale(0.98); }
.v2-cutout-mode-btn.is-on {
  background: rgba(163, 255, 18, 0.10);
  border-color: var(--accent);
  color: var(--accent);
}
.v2-cutout-mode-lbl {
  font: 700 13px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.01em;
}
.v2-cutout-mode-sub {
  font: 500 10px/1.2 'Inter', system-ui, sans-serif;
  opacity: 0.75;
}

/* AI loading indicator */
.v2-cutout-ai-loading {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 12px 0 0;
  padding: 12px 14px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  color: var(--text-2);
  font: 600 12px/1 'Inter', system-ui, sans-serif;
}
.v2-cutout-ai-loading.is-error {
  background: rgba(255, 99, 132, 0.08);
  border-color: rgba(255, 99, 132, 0.35);
  color: rgba(255, 99, 132, 0.95);
}

/* Key color row */
.v2-cutout-key-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 8px;
}
.v2-cutout-key-swatch {
  flex-shrink: 0;
  width: 40px; height: 40px;
  border-radius: var(--r-sm);
  border: 2px solid rgba(255, 255, 255, 0.18);
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.35);
}
.v2-cutout-eyedropper {
  flex: 1 1 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 10px 14px;
  border-radius: 999px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text-2);
  font: 700 12px/1 'Inter', system-ui, sans-serif;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
  touch-action: manipulation;
}
.v2-cutout-eyedropper:hover { background: var(--surface-3); color: var(--text); }
.v2-cutout-eyedropper.is-armed {
  background: rgba(163, 255, 18, 0.18);
  color: var(--accent);
  border-color: var(--accent);
  animation: v2-cutout-eyedrop-pulse 1.4s ease-in-out infinite;
}
@keyframes v2-cutout-eyedrop-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(163, 255, 18, 0.45); }
  50%      { box-shadow: 0 0 0 6px rgba(163, 255, 18, 0); }
}
.v2-cutout-color-input {
  width: 40px; height: 40px;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
}
.v2-cutout-backdrop-color {
  display: block;
  margin-top: 8px;
  width: 100%;
  height: 36px;
  border-radius: var(--r-sm);
}

/* Canvas frame state — eyedropper-armed shows a crosshair cursor */
.v2-canvas-frame.is-eyedropper-armed {
  cursor: crosshair !important;
}
.v2-canvas-frame.is-eyedropper-armed::after {
  content: 'Tap to pick key color';
  position: absolute;
  top: 12px; left: 50%;
  transform: translateX(-50%);
  padding: 6px 12px;
  background: rgba(10, 11, 16, 0.85);
  color: var(--accent);
  font: 700 11px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  border-radius: 999px;
  border: 1px solid rgba(163, 255, 18, 0.55);
  pointer-events: none;
  z-index: 60;
  animation: v2-cutout-eyedrop-pulse 1.4s ease-in-out infinite;
}
/* v2.0.481 — Subject-pick (server matte) armed: the SAME crosshair + top hint
   pill the eyedropper uses, so every armed canvas tool reads the same way. */
.v2-canvas-frame.is-matte-armed {
  cursor: crosshair !important;
}
.v2-canvas-frame.is-matte-armed::after {
  content: 'Tap the subject';
  position: absolute;
  top: 12px; left: 50%;
  transform: translateX(-50%);
  padding: 6px 12px;
  background: rgba(10, 11, 16, 0.85);
  color: var(--accent);
  font: 700 11px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  border-radius: 999px;
  border: 1px solid rgba(163, 255, 18, 0.55);
  pointer-events: none;
  z-index: 60;
  animation: v2-cutout-eyedrop-pulse 1.4s ease-in-out infinite;
}

/* Backdrop picker — 4-card grid */
.v2-cutout-backdrop {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 8px;
  margin-top: 8px;
}
.v2-cutout-backdrop-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 10px 6px 8px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  color: var(--text-2);
  cursor: pointer;
  transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.v2-cutout-backdrop-btn:hover { background: var(--surface-3); color: var(--text); }
.v2-cutout-backdrop-btn.is-on {
  background: rgba(163, 255, 18, 0.10);
  border-color: var(--accent);
  color: var(--accent);
}
.v2-cutout-backdrop-vis {
  width: 36px;
  height: 36px;
  border-radius: 6px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: var(--surface-3);
}
.v2-cutout-backdrop-vis--transparent {
  background-image:
    linear-gradient(45deg, rgba(255,255,255,0.18) 25%, transparent 25%),
    linear-gradient(-45deg, rgba(255,255,255,0.18) 25%, transparent 25%),
    linear-gradient(45deg, transparent 75%, rgba(255,255,255,0.18) 75%),
    linear-gradient(-45deg, transparent 75%, rgba(255,255,255,0.18) 75%);
  background-size: 8px 8px;
  background-position: 0 0, 0 4px, 4px -4px, -4px 0;
}
.v2-cutout-backdrop-vis--blur {
  background: linear-gradient(135deg, #a855f7, #06b6d4);
  filter: blur(3px);
}
.v2-cutout-backdrop-vis--image {
  background: linear-gradient(135deg, #f59e0b, #ef4444);
  display: flex;
  align-items: center;
  justify-content: center;
  font: 700 12px/1 'Inter', system-ui, sans-serif;
  color: #fff;
}
.v2-cutout-backdrop-vis--image::before {
  content: 'IMG';
}
/* v2.0.301 — Image-backdrop thumb shows the actual selected
   image inside the 4-card grid swatch. Cover-fit so portrait/
   landscape sources both look right. */
.v2-cutout-backdrop-vis--thumb {
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

/* v2.0.301 — Image backdrop preview block (renders below the
   4-card grid when image kind is active). Large thumb + Replace +
   Remove action row + processing spinner overlay. */
.v2-cutout-image-preview {
  margin-top: 12px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.v2-cutout-image-thumb {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  background-color: var(--surface-3);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  overflow: hidden;
}
.v2-cutout-image-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  aspect-ratio: 16 / 9;
  background: var(--surface-2);
  border: 1px dashed var(--border);
  border-radius: var(--r-sm);
  color: var(--text-3);
  font: 500 12px/1.4 'Inter', system-ui, sans-serif;
  text-align: center;
  padding: 16px;
}
.v2-cutout-image-processing {
  position: absolute;
  inset: auto 0 0 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 8px;
  background: rgba(10, 11, 16, 0.78);
  color: var(--accent);
  font: 700 11px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  border-top: 1px solid rgba(163, 255, 18, 0.35);
}
/* v2.0.308 GAP A — Backdrop image error block. Renders in place of
   the thumb (replaces it, not overlays — failed uploads have no
   usable image anyway). Dashed rose border + center-aligned icon +
   two-line copy. Same 16:9 aspect as the success thumb so the
   panel layout doesn't jump on error. Discoverable error
   recovery: the Replace button below stays active so user can
   pick a different image without first tapping Remove. */
.v2-cutout-image-error {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  width: 100%;
  aspect-ratio: 16 / 9;
  background: rgba(255, 99, 132, 0.06);
  border: 1px dashed rgba(255, 99, 132, 0.55);
  border-radius: var(--r-sm);
  padding: 16px;
}
.v2-cutout-image-error-icon {
  flex-shrink: 0;
  width: 32px; height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: rgba(255, 99, 132, 0.18);
  border: 1px solid rgba(255, 99, 132, 0.55);
  border-radius: 50%;
  font: 800 16px/1 'Inter', system-ui, sans-serif;
  color: rgba(255, 99, 132, 0.95);
}
.v2-cutout-image-error-text {
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.v2-cutout-image-error-text strong {
  font: 700 13px/1 'Inter', system-ui, sans-serif;
  color: rgba(255, 99, 132, 0.95);
}
.v2-cutout-image-error-text span {
  font: 500 11px/1.4 'Inter', system-ui, sans-serif;
  color: var(--text-3);
}
.v2-cutout-image-actions {
  display: flex;
  gap: 8px;
}
.v2-cutout-image-replace,
.v2-cutout-image-remove {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 14px;
  border-radius: 999px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text-2);
  font: 700 11px/1 'Inter', system-ui, sans-serif;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
}
.v2-cutout-image-replace:hover { background: var(--surface-3); color: var(--text); }
.v2-cutout-image-remove {
  color: rgba(255, 99, 132, 0.85);
  border-color: rgba(255, 99, 132, 0.25);
}
.v2-cutout-image-remove:hover {
  background: rgba(255, 99, 132, 0.10);
  color: rgba(255, 99, 132, 1);
  border-color: rgba(255, 99, 132, 0.45);
}

/* v2.0.302 — Auto cleanup chip sits inline-right inside the Edge
   cleanup section label. Compact pill (matching the v300 suggest-
   color button visual language), reinforces that it's a one-tap
   smart action vs. the manual sliders below. */
.v2-cutout-auto-cleanup {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-left: auto;
  padding: 4px 10px;
  border-radius: 999px;
  background: transparent;
  border: 1px dashed rgba(163, 255, 18, 0.45);
  color: var(--accent);
  font: 700 10px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
  touch-action: manipulation;
}
.v2-cutout-auto-cleanup:hover {
  background: rgba(163, 255, 18, 0.10);
  border-style: solid;
}
.v2-cutout-auto-cleanup:active { transform: scale(0.95); }
/* The .v2-tool-sec-lbl that hosts Auto cleanup needs flexbox so the
   chip sits at the right edge. Existing rule sets it block; override
   only when an Auto cleanup chip is present (descendant selector). */
.v2-tool-sec-lbl:has(.v2-cutout-auto-cleanup) {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;        /* v2.0.303 — Show mask + Reset + Auto all sit here */
}

/* v2.0.303 — Show mask toggle. Compact pill (matching auto-cleanup
   visual rhythm), but in a neutral monochrome by default — when ON
   it flips to the accent color + animated pulse so the user
   ALWAYS knows their preview is in mask mode (otherwise easy to
   forget you're looking at grayscale and panic about export). */
.v2-cutout-show-mask {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 10px;
  border-radius: 999px;
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-2);
  font: 700 10px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
  touch-action: manipulation;
}
.v2-cutout-show-mask:hover {
  background: var(--surface-3);
  color: var(--text);
}
.v2-cutout-show-mask.is-on {
  background: rgba(163, 255, 18, 0.14);
  color: var(--accent);
  border-color: var(--accent);
  animation: v2-cutout-show-mask-pulse 1.6s ease-in-out infinite;
}
@keyframes v2-cutout-show-mask-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(163, 255, 18, 0.55); }
  50%      { box-shadow: 0 0 0 6px rgba(163, 255, 18, 0); }
}
.v2-cutout-show-mask:active { transform: scale(0.95); }

/* v2.0.303 — Reset cleanup chip. Neutral monochrome until hover;
   tap → zeros all three sliders. Conditionally rendered only when
   at least one slider is non-zero so the empty-default state stays
   visually quiet. */
.v2-cutout-cleanup-reset {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 10px;
  border-radius: 999px;
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-3);
  font: 700 10px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
  touch-action: manipulation;
}
.v2-cutout-cleanup-reset:hover {
  background: rgba(255, 255, 255, 0.05);
  color: var(--text-2);
  border-color: var(--text-3);
}
.v2-cutout-cleanup-reset:active { transform: scale(0.95); }
.v2-cutout-backdrop-lbl {
  font: 600 10px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.02em;
}

/* Suggest-color button */
.v2-cutout-suggest {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: 10px;
  padding: 8px 14px;
  border-radius: 999px;
  background: transparent;
  border: 1px dashed rgba(163, 255, 18, 0.45);
  color: var(--accent);
  font: 600 11px/1 'Inter', system-ui, sans-serif;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease);
}
.v2-cutout-suggest:hover {
  background: rgba(163, 255, 18, 0.08);
}

/* Toggle + remove actions at the bottom of the panel */
.v2-cutout-toggle {
  display: block;
  width: 100%;
  padding: 12px;
  margin-top: 8px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  color: var(--text-2);
  font: 700 12px/1 'Inter', system-ui, sans-serif;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.v2-cutout-toggle:hover { background: var(--surface-3); color: var(--text); }
.v2-cutout-toggle--enable {
  background: rgba(163, 255, 18, 0.10);
  border-color: var(--accent);
  color: var(--accent);
}
.v2-cutout-clear {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  width: 100%;
  padding: 10px;
  margin-top: 8px;
  background: transparent;
  border: 1px solid rgba(255, 99, 132, 0.35);
  border-radius: var(--r-sm);
  color: rgba(255, 99, 132, 0.85);
  font: 700 11px/1 'Inter', system-ui, sans-serif;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
}
.v2-cutout-clear:hover {
  background: rgba(255, 99, 132, 0.08);
  color: rgba(255, 99, 132, 1);
  border-color: rgba(255, 99, 132, 0.55);
}

/* =========================================================
   v2.0.310 — Mask brush.
   Touch up section in CutoutPanel + canvas brush surface +
   live cursor ring. Beats CapCut on multiple axes: live cursor
   preview, pinch-to-resize on the canvas (no slider trip),
   per-stroke undo, Restore from AI.
   ========================================================= */

/* =========================================================
   v2.0.366 — VoiceRecorderSheet. Mic capture for voiceovers.
   Big circular record button (red when recording), live audio
   level meter, elapsed timer. Sheet stays open across takes
   so users can re-record without re-prompting for mic
   permission.
   ========================================================= */
.v2-voice-rec {
  width: 320px;
  max-width: 92vw;
}
.v2-voice-rec-body {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 18px;
  padding: 28px 20px 24px;
}
/* Level meter — 18 vertical bars, each lit when level threshold reached.
   Animated via reactive lit-state, not CSS animation, so it tracks the
   actual mic input frame-by-frame. */
.v2-voice-rec-meter {
  display: flex;
  gap: 3px;
  align-items: flex-end;
  height: 36px;
}
.v2-voice-rec-meter-bar {
  width: 5px;
  height: 100%;
  background: rgba(255,255,255,0.08);
  border-radius: 2px;
  transition: background 60ms linear;
}
.v2-voice-rec-meter-bar.is-lit {
  background: #a3ff12;
  box-shadow: 0 0 6px rgba(163,255,18,0.45);
}
.v2-voice-rec-time {
  font: 600 28px/1 'SF Mono', 'Menlo', monospace;
  color: rgba(255,255,255,0.78);
  letter-spacing: 0.04em;
}
.v2-voice-rec-time.is-recording {
  color: #ff4848;
}
.v2-voice-rec-error {
  font-size: 12px;
  color: #ff8a8a;
  text-align: center;
  max-width: 260px;
  line-height: 1.35;
}
/* Big circle record button. Idle = lime ring; recording = red filled
   with white square inner; uploading = spinner. 44px Apple HIG min ×
   gracefully — 88px so the user can't possibly mis-tap. */
.v2-voice-rec-btn {
  width: 88px; height: 88px;
  border-radius: 50%;
  background: rgba(163,255,18,0.12);
  color: #a3ff12;
  border: 2px solid rgba(163,255,18,0.55);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background 160ms var(--ease),
              border-color 160ms var(--ease),
              transform 120ms var(--ease);
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}
.v2-voice-rec-btn:active { transform: scale(0.96); }
.v2-voice-rec-btn.is-recording {
  background: #ff4848;
  border-color: rgba(255,72,72,0.85);
  color: #fff;
  /* Pulse so the user has unmistakable "you're recording" feedback even
     if they look away from the elapsed counter. */
  animation: v2-voice-rec-pulse 1.3s ease-in-out infinite;
}
@keyframes v2-voice-rec-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(255,72,72,0.45); }
  50%      { box-shadow: 0 0 0 12px rgba(255,72,72,0); }
}
.v2-voice-rec-btn.is-uploading {
  background: rgba(255,255,255,0.06);
  border-color: rgba(255,255,255,0.20);
  color: rgba(255,255,255,0.7);
  cursor: default;
  animation: none;
}
.v2-voice-rec-spinner {
  width: 28px; height: 28px;
  border-radius: 50%;
  border: 3px solid rgba(255,255,255,0.18);
  border-top-color: rgba(255,255,255,0.85);
  animation: v2-voice-rec-spin 0.8s linear infinite;
}
@keyframes v2-voice-rec-spin {
  to { transform: rotate(360deg); }
}
.v2-voice-rec-hint {
  font-size: 13px;
  color: rgba(255,255,255,0.55);
  text-align: center;
  letter-spacing: 0.01em;
}
/* Permission-denied state — bigger icon, explanatory copy, no record
   button (user can't proceed until they enable mic in browser). */
.v2-voice-rec-denied {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  padding: 16px 4px;
}
.v2-voice-rec-denied-icon {
  color: rgba(255,138,138,0.85);
  opacity: 0.85;
}
.v2-voice-rec-denied-msg {
  font-size: 14px;
  color: rgba(255,255,255,0.85);
  text-align: center;
  font-weight: 500;
}
.v2-voice-rec-denied-hint {
  font-size: 12px;
  color: rgba(255,255,255,0.55);
  text-align: center;
  line-height: 1.4;
  max-width: 240px;
}

/* v2.0.367 — Headphone hint banner. Shows above the body on first
   open per device (localStorage flag). One tap on "Got it" dismisses
   forever. Lime accent so it reads as helpful, not warning. */
.v2-voice-rec-hint-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 14px;
  background: rgba(163,255,18,0.08);
  border-bottom: 1px solid rgba(163,255,18,0.18);
  font-size: 12px;
  color: rgba(255,255,255,0.92);
  line-height: 1.35;
}
.v2-voice-rec-hint-emoji {
  font-size: 14px;
  margin-right: 4px;
}
.v2-voice-rec-hint-banner button {
  flex-shrink: 0;
  background: none;
  border: 1px solid rgba(163,255,18,0.55);
  color: #a3ff12;
  padding: 4px 10px;
  border-radius: 12px;
  font-size: 12px;
  cursor: pointer;
  font-weight: 600;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}
.v2-voice-rec-hint-banner button:active {
  background: rgba(163,255,18,0.15);
}

/* v2.0.367 — Countdown phase. Giant 3 → 2 → 1 numeral, popping in
   with a quick scale+fade so each tick has presence. Cancel button
   below for the user to bail out before the mic engages. */
.v2-voice-rec-countdown {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 32px 16px 24px;
  gap: 12px;
  min-height: 240px;
  justify-content: center;
}
.v2-voice-rec-countdown-num {
  font: 800 96px/1 'SF Pro Display', 'Inter', system-ui, sans-serif;
  color: #a3ff12;
  text-shadow: 0 0 30px rgba(163,255,18,0.55);
  animation: v2-voice-rec-cd-pop 0.8s cubic-bezier(.2,.8,.2,1);
  will-change: transform, opacity;
}
@keyframes v2-voice-rec-cd-pop {
  0%   { transform: scale(1.5); opacity: 0; }
  20%  { transform: scale(1.0); opacity: 1; }
  100% { transform: scale(1.0); opacity: 1; }
}
.v2-voice-rec-cancel {
  margin-top: 10px;
  background: none;
  border: 1px solid rgba(255,255,255,0.20);
  color: rgba(255,255,255,0.85);
  padding: 8px 20px;
  border-radius: 14px;
  font: 600 13px/1 'Inter', sans-serif;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}
.v2-voice-rec-cancel:active {
  background: rgba(255,255,255,0.06);
}

/* v2.0.367 — Recording-position chip. Visible only while recording;
   shows current playhead position vs project total so the user knows
   "I'm at 0:03 of a 0:18 clip." Red to match the recording state. */
.v2-voice-rec-pos {
  font: 600 12px/1 'SF Mono', 'Menlo', monospace;
  color: rgba(255,72,72,0.85);
  letter-spacing: 0.05em;
  padding: 4px 10px;
  background: rgba(255,72,72,0.10);
  border-radius: 12px;
  border: 1px solid rgba(255,72,72,0.25);
}

/* v2.0.367 — Preview phase. Triad of actions: Discard / Retake / Keep
   with a play button above. Same width/spacing as CapCut's voiceover
   review screen. */
.v2-voice-rec-preview {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 20px 16px 20px;
  gap: 16px;
}
.v2-voice-rec-preview-meta {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
}
.v2-voice-rec-play {
  width: 72px; height: 72px;
  border-radius: 50%;
  background: rgba(255,255,255,0.10);
  color: #fff;
  border: 1.5px solid rgba(255,255,255,0.30);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background 140ms var(--ease), transform 100ms var(--ease);
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}
.v2-voice-rec-play:active {
  background: rgba(255,255,255,0.18);
  transform: scale(0.96);
}
.v2-voice-rec-actions {
  display: flex;
  gap: 8px;
  margin-top: 2px;
  width: 100%;
  justify-content: center;
}
.v2-voice-rec-action {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  padding: 10px 8px;
  border-radius: 14px;
  font: 600 12px/1 'Inter', system-ui, sans-serif;
  cursor: pointer;
  border: 1px solid transparent;
  background: rgba(255,255,255,0.06);
  color: rgba(255,255,255,0.85);
  min-width: 72px;
  transition: background 120ms var(--ease), transform 100ms var(--ease);
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}
.v2-voice-rec-action:active { transform: scale(0.95); }
.v2-voice-rec-discard {
  background: rgba(255,72,72,0.10);
  color: #ff6b6b;
  border-color: rgba(255,72,72,0.30);
}
.v2-voice-rec-discard:active { background: rgba(255,72,72,0.18); }
.v2-voice-rec-retake {
  background: rgba(255,255,255,0.06);
  color: rgba(255,255,255,0.88);
  border-color: rgba(255,255,255,0.18);
}
.v2-voice-rec-retake:active { background: rgba(255,255,255,0.12); }
.v2-voice-rec-keep {
  background: rgba(163,255,18,0.15);
  color: #a3ff12;
  border-color: rgba(163,255,18,0.55);
}
.v2-voice-rec-keep:active { background: rgba(163,255,18,0.25); }

/* =========================================================
   v2.0.368 — Existing-sessions list in CaptionsSheet
   Surfaces prior caption runs as primary action when present
   so users edit/translate instead of always regenerating.
   ========================================================= */
.v2-captions-existing {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.v2-captions-existing-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 14px;
  background: rgba(163,255,18,0.08);
  border: 1px solid rgba(163,255,18,0.30);
  border-radius: 12px;
  cursor: pointer;
  text-align: left;
  width: 100%;
  transition: background 120ms var(--ease);
  -webkit-tap-highlight-color: transparent;
}
.v2-captions-existing-row:active { background: rgba(163,255,18,0.15); }
.v2-captions-existing-meta { display: flex; flex-direction: column; gap: 2px; }
.v2-captions-existing-count {
  font: 600 13px/1.2 'Inter', system-ui, sans-serif;
  color: #fff;
}
.v2-captions-existing-src {
  font-size: 11px;
  color: rgba(255,255,255,0.55);
}
.v2-captions-existing-edit {
  font: 600 11px/1 'Inter', sans-serif;
  color: #a3ff12;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* =========================================================
   v2.0.368 — TranscriptEditorSheet
   ========================================================= */
.v2-transcript {
  width: 480px;
  max-width: 96vw;
  max-height: 84dvh;
  display: flex;
  flex-direction: column;
}
.v2-transcript-working {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 10px 16px;
  background: rgba(163,255,18,0.10);
  border-bottom: 1px solid rgba(163,255,18,0.25);
  font-size: 12px;
  color: rgba(255,255,255,0.9);
}
.v2-transcript-spinner {
  width: 16px; height: 16px;
  border-radius: 50%;
  border: 2px solid rgba(255,255,255,0.18);
  border-top-color: #a3ff12;
  animation: v2-voice-rec-spin 0.8s linear infinite;
}
.v2-transcript-error {
  padding: 8px 14px;
  background: rgba(255,72,72,0.10);
  border-bottom: 1px solid rgba(255,72,72,0.30);
  color: #ff8a8a;
  font-size: 12px;
  text-align: center;
}
.v2-transcript-actions {
  display: flex;
  gap: 6px;
  align-items: center;
  padding: 10px 14px;
  border-bottom: 1px solid rgba(255,255,255,0.08);
}
.v2-transcript-actions-spacer { flex: 1; }
.v2-transcript-mode {
  background: rgba(255,255,255,0.06);
  border: 1px solid rgba(255,255,255,0.12);
  color: rgba(255,255,255,0.78);
  padding: 6px 12px;
  border-radius: 10px;
  font: 600 12px/1 'Inter', sans-serif;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.v2-transcript-mode.is-active {
  background: rgba(163,255,18,0.18);
  border-color: rgba(163,255,18,0.55);
  color: #a3ff12;
}
.v2-transcript-mode:disabled,
.v2-transcript-translate:disabled,
.v2-transcript-regen:disabled,
.v2-transcript-bulk-save:disabled,
.v2-transcript-row-del:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
.v2-transcript-translate {
  background: rgba(168,85,247,0.15);
  border: 1px solid rgba(168,85,247,0.55);
  color: #c4a3f7;
  padding: 6px 12px;
  border-radius: 10px;
  font: 600 12px/1 'Inter', sans-serif;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.v2-transcript-translate:active { background: rgba(168,85,247,0.25); }
.v2-transcript-regen {
  background: rgba(255,255,255,0.06);
  border: 1px solid rgba(255,255,255,0.12);
  color: rgba(255,255,255,0.78);
  width: 32px; height: 32px;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
/* v2.0.388 — Words tab in TranscriptEditor. Each caption is a row with
   per-word chips; chips are tap-edit / two-tap-split / long-press-delete.
   The "+" between rows merges adjacent captions. */
.v2-twords-scroll {
  flex: 1;
  overflow-y: auto;
  padding: 8px 14px 16px;
  -webkit-overflow-scrolling: touch;
}
.v2-twords-row {
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(255,255,255,0.08);
  border-radius: 10px;
  padding: 10px 12px;
  margin-bottom: 8px;
  position: relative;
}
.v2-twords-row-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 6px;
}
.v2-twords-row-num {
  background: rgba(255,255,255,0.10);
  color: rgba(255,255,255,0.68);
  font: 600 10px/1 'Inter', sans-serif;
  padding: 3px 7px;
  border-radius: 999px;
}
.v2-twords-row-time {
  font: 500 11px/1 'Inter', sans-serif;
  color: rgba(255,255,255,0.45);
  font-variant-numeric: tabular-nums;
}
.v2-twords-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: center;
}
/* v2.0.389 — Per-caption word-duration strip with drag handles. Each
   .v2-twstrip-seg is positioned absolutely as a percentage of the strip
   width (which equals the caption duration). Handles sit between
   adjacent segments at the boundary x-coordinate and are 24px wide
   centered so the touch target is comfortable on mobile while the
   visible line stays slim. */
.v2-twstrip {
  position: relative;
  height: 18px;
  margin: 0 0 6px 0;
  background: rgba(255,255,255,0.04);
  border-radius: 4px;
  /* v2.0.391 — CRITICAL fix from v390 audit: was `overflow: hidden`,
     which silently clipped the v390 [1/5] live drag tooltip (positioned
     `bottom: calc(100% + 6px)` i.e. above the strip box) AND the handle
     visible-line extension (`top:-4px bottom:-4px`) — shrinking the
     visible hit-band from 26px to 18px. Headline polish feature was
     invisible since v390. Segments stay inside the strip box because
     their left/width are always within 0..100% (totalDur is the
     clamped upper bound for word.end). */
  /* v2.0.391 — audit: was `touch-action: none` which swallowed
     vertical scroll on long transcripts whenever a finger landed on
     a strip row. Now `pan-y` so vertical scroll passes through the
     strip surface; handles still need `touch-action: none` for their
     own horizontal drag and keep that rule below. */
  touch-action: pan-y;
}
.v2-twstrip-seg {
  position: absolute;
  top: 3px; bottom: 3px;
  background: rgba(34,211,238,0.22);
  border-radius: 2px;
  /* v2.0.389 — pointer-events: auto so taps seek the playhead. Handles
     sit on top with their own 24px hit-band; clicks landing on bare
     segment pixels reach the segment's onClick. */
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.v2-twstrip-seg:hover { background: rgba(34,211,238,0.42); }
.v2-twstrip-seg:nth-child(odd) {
  background: rgba(34,211,238,0.32);
}
.v2-twstrip-seg:nth-child(odd):hover { background: rgba(34,211,238,0.52); }
.v2-twstrip-handle {
  position: absolute;
  top: -4px; bottom: -4px;
  width: 24px;
  transform: translateX(-50%);
  background: transparent;
  border: 0;
  padding: 0;
  cursor: ew-resize;
  -webkit-tap-highlight-color: transparent;
  touch-action: none;
}
.v2-twstrip-handle::before {
  /* The visible thin line, drawn as a pseudo-element so the hit-target
     stays generous while the visual is precise. */
  content: '';
  position: absolute;
  top: 0; bottom: 0;
  left: 50%;
  width: 2px;
  background: rgba(255,255,255,0.55);
  transform: translateX(-50%);
  border-radius: 1px;
  transition: background 0.10s ease, box-shadow 0.10s ease;
}
.v2-twstrip-handle:hover::before,
.v2-twstrip-handle:active::before {
  background: #22d3ee;
  box-shadow: 0 0 0 2px rgba(34,211,238,0.20);
}
.v2-twstrip-handle:disabled { cursor: not-allowed; opacity: 0.4; }
/* v2.0.390 — Live drag tooltip floating above the strip during boundary
   drag. Signature CapCut affordance — shows the exact landing time so
   the user can stop at the right moment without trial-and-error. */
.v2-twstrip-tip {
  position: absolute;
  bottom: calc(100% + 6px);
  transform: translateX(-50%);
  background: #fef08a;
  color: #1a1a1a;
  font: 700 11px/1 'Inter', system-ui, sans-serif;
  font-variant-numeric: tabular-nums;
  padding: 4px 8px;
  border-radius: 6px;
  box-shadow: 0 4px 12px rgba(0,0,0,0.35), 0 0 0 1px rgba(0,0,0,0.18);
  pointer-events: none;
  z-index: 10;
  white-space: nowrap;
  animation: v2-twstrip-tip-in 0.08s ease-out;
}
.v2-twstrip-tip::after {
  /* Down-pointing chevron so the tooltip "anchors" to the handle. */
  content: '';
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  border: 4px solid transparent;
  border-top-color: #fef08a;
}
@keyframes v2-twstrip-tip-in {
  from { opacity: 0; transform: translateX(-50%) translateY(2px); }
  to   { opacity: 1; transform: translateX(-50%) translateY(0); }
}
.v2-tword-wrap {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
/* v2.0.390 — Word + duration stacked vertically inside the chip wrap.
   Confirm-delete pills still sit horizontally beside the column. */
.v2-tword-col {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}
.v2-tword-dur {
  /* v2.0.391 — audit A1.F5: bumped 9px → 10px + opacity 0.42 → 0.65.
     Was failing WCAG AA contrast (4.5:1) for body text — effective
     ratio was ~3.4:1 on the sheet bg, unreadable in outdoor light
     or for low-vision users. 0.65 white on the dark sheet bg gives
     ~7.5:1 (passes AAA at 18pt, AA at any size). */
  font: 500 10px/1 'Inter', 'SF Mono', Menlo, monospace;
  color: rgba(255,255,255,0.65);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  user-select: none;
}
.v2-tword.is-active + .v2-tword-dur,
.v2-tword-col:has(.v2-tword.is-active) .v2-tword-dur {
  color: rgba(254,240,138,0.85);
}
.v2-tword {
  display: inline-block;
  background: rgba(34,211,238,0.10);
  border: 1px solid rgba(34,211,238,0.36);
  color: #d8f4fb;
  border-radius: 8px;
  /* v2.0.391 — audit A1.F2 (HIGH): bumped padding 6/9 → 9/12 + added
     min-height 32px so the chip clears the 44px effective touch
     target with the wrap + bleed-out below. Was ~31px tall = under
     the iOS HIG floor; tiny single-letter words like "I" / "a" were
     fragile to tap and long-press routed to scroll instead of arm. */
  padding: 9px 12px;
  min-height: 32px;
  position: relative;
  font: 600 13px/1.2 'Inter', sans-serif;
  cursor: text;
  -webkit-tap-highlight-color: transparent;
  outline: none;
  white-space: pre;
  user-select: text;
  /* v2.0.391 — audit A1.F10: suppress iOS double-tap-to-zoom on the
     chip surface so the two-tap-to-split gesture lands without the
     browser swallowing the second tap as a zoom-in. */
  touch-action: manipulation;
  /* v2.0.390 — Smoother active-state crossfade + subtle press response. */
  transition:
    background   0.18s cubic-bezier(0.16, 1.0, 0.3, 1.0),
    border-color 0.18s cubic-bezier(0.16, 1.0, 0.3, 1.0),
    box-shadow   0.18s cubic-bezier(0.16, 1.0, 0.3, 1.0),
    transform    0.10s ease;
}
/* v2.0.391 — audit A1.F2: invisible 6px bleed-out enlarges the
   effective tap target to ~44px without changing visible chip size. */
.v2-tword::after {
  content: '';
  position: absolute;
  inset: -6px;
  pointer-events: auto;
}
.v2-tword:active {
  transform: scale(0.96);
}
.v2-tword:focus {
  background: rgba(34,211,238,0.20);
  border-color: #22d3ee;
}
/* v2.0.389 — Active-word visual: chip lit up while playhead is inside
   the word's time window. Yellow (CapCut karaoke convention) so it reads
   distinct from the cyan focus state. */
.v2-tword.is-active {
  background: rgba(254,240,138,0.22);
  border-color: rgba(254,240,138,0.85);
  color: #fff;
  box-shadow: 0 0 0 1px rgba(254,240,138,0.35) inset;
}
.v2-tword.is-active:focus {
  /* Focus wins visually but keep the yellow tint as a subtle accent. */
  background: rgba(34,211,238,0.25);
  border-color: #22d3ee;
  box-shadow: 0 0 0 1px rgba(254,240,138,0.45) inset;
}
/* Playhead indicator inside the WordTimeStrip. 2px vertical line at the
   playhead's percent position; transition softens the per-frame jump. */
.v2-twstrip-playhead {
  position: absolute;
  top: -2px; bottom: -2px;
  width: 2px;
  background: #fef08a;
  box-shadow: 0 0 0 1px rgba(254,240,138,0.45);
  transform: translateX(-50%);
  pointer-events: none;
  z-index: 1;
  transition: left 0.06s linear;
}
.v2-tword-del-confirm,
.v2-tword-del-cancel {
  font: 600 11px/1 'Inter', sans-serif;
  border-radius: 6px;
  padding: 6px 8px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  border: 1px solid transparent;
}
.v2-tword-del-confirm {
  background: rgba(220,38,38,0.85);
  color: white;
}
.v2-tword-del-cancel {
  background: rgba(255,255,255,0.06);
  color: rgba(255,255,255,0.72);
  border-color: rgba(255,255,255,0.12);
}
.v2-twords-empty {
  font: 500 12px/1.3 'Inter', sans-serif;
  color: rgba(255,255,255,0.45);
  font-style: italic;
}
.v2-twords-merge {
  position: absolute;
  /* v2.0.391 — audit A1.F7: was 28x28 below iOS HIG. Bumped to 36x36
     + inset from right via safe-area-inset-right to avoid the iOS
     home-bar gesture zone on the bottom row. */
  right: max(14px, env(safe-area-inset-right, 14px));
  bottom: -18px;
  width: 36px; height: 36px;
  border-radius: 50%;
  background: rgba(168,85,247,0.20);
  border: 1px solid rgba(168,85,247,0.55);
  color: #d4b3fa;
  font: 700 16px/1 'Inter', sans-serif;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  /* v2.0.390 — Drop shadow so the merge button reads as a layer above
     the rows rather than glued to them. Hover brightens; active depresses. */
  box-shadow: 0 2px 6px rgba(0,0,0,0.35);
  transition:
    background  0.18s ease,
    border-color 0.18s ease,
    color       0.18s ease,
    transform   0.10s ease,
    box-shadow  0.18s ease;
}
.v2-twords-merge:hover {
  background: rgba(168,85,247,0.32);
  border-color: rgba(168,85,247,0.75);
  color: #ffffff;
  box-shadow: 0 4px 12px rgba(168,85,247,0.35);
}
.v2-twords-merge:active {
  transform: scale(0.90);
  box-shadow: 0 1px 3px rgba(0,0,0,0.35);
}
.v2-twords-merge:disabled { opacity: 0.4; cursor: not-allowed; }
.v2-twords-hint {
  font: 500 11px/1.4 'Inter', sans-serif;
  color: rgba(255,255,255,0.45);
  padding: 14px 4px 4px;
  text-align: center;
}

/* v2.0.388 — "Animation" chip in the transcript toolbar. Sibling of
   .v2-transcript-translate; cyan accent to differentiate at a glance. */
.v2-transcript-anim {
  background: rgba(34,211,238,0.12);
  border: 1px solid rgba(34,211,238,0.50);
  color: #7cdef1;
  padding: 6px 12px;
  border-radius: 10px;
  font: 600 12px/1 'Inter', sans-serif;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.v2-transcript-anim:active { background: rgba(34,211,238,0.22); }
.v2-transcript-anim:disabled { opacity: 0.5; cursor: not-allowed; }

/* v2.0.388 — CaptionAnimationSheet. Half-sheet bottom modal layered above
   the TranscriptEditorSheet (so closing the picker drops back to the
   transcript editor without re-opening it). */
.v2-capanim-backdrop { z-index: 200; }
.v2-capanim {
  max-width: 520px;
  display: flex;
  flex-direction: column;
  max-height: 80dvh;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  /* v2.0.390 — Springy slide-up entrance. Cubic-bezier mimics CapCut's
     bottom-sheet feel (slight overshoot at the top, settles in). Honors
     prefers-reduced-motion via @media block below. */
  animation: v2-capanim-in 0.28s cubic-bezier(0.16, 1.0, 0.3, 1.0);
}
@keyframes v2-capanim-in {
  from { transform: translateY(24px); opacity: 0; }
  to   { transform: translateY(0);    opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .v2-capanim { animation: none; }
}
/* v2.0.390 — Reset button in the modal head. Sits left of "Done".
   Subtle until tapped — not destructive enough to warrant red, but
   present enough to find when needed. */
.v2-capanim-reset-btn {
  background: transparent;
  border: 1px solid rgba(255,255,255,0.14);
  color: rgba(255,255,255,0.62);
  font: 600 12px/1 'Inter', sans-serif;
  padding: 7px 12px;
  border-radius: 8px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  margin-right: 8px;
}
.v2-capanim-reset-btn:active {
  background: rgba(255,255,255,0.08);
  color: rgba(255,255,255,0.85);
}
/* v2.0.390 — Hero preview canvas (280x60). Sits between modal head and
   the Style section. Inset background so the karaoke text contrast
   matches the on-canvas rendering convention. */
.v2-capanim-hero {
  margin: 4px 14px 0;
  padding: 8px;
  background: linear-gradient(180deg, rgba(0,0,0,0.55), rgba(0,0,0,0.30));
  border: 1px solid rgba(255,255,255,0.06);
  border-radius: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
.v2-capanim-hero canvas { display: block; }
.v2-capanim-section { padding: 14px; }
.v2-capanim-section + .v2-capanim-section {
  border-top: 1px solid rgba(255,255,255,0.06);
}
.v2-capanim-section-label {
  font: 600 11px/1 'Inter', sans-serif;
  color: rgba(255,255,255,0.55);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-bottom: 10px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.v2-capanim-chips {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}
.v2-capanim-chip {
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(255,255,255,0.10);
  border-radius: 12px;
  padding: 8px 6px 6px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  /* v2.0.390 — Smoother selection transition: 220ms cubic-bezier so the
     selected glow eases in rather than snapping. box-shadow added to the
     transition list so the inset cyan ring fades in cleanly. */
  transition:
    border-color 0.22s cubic-bezier(0.16, 1.0, 0.3, 1.0),
    background   0.22s cubic-bezier(0.16, 1.0, 0.3, 1.0),
    box-shadow   0.22s cubic-bezier(0.16, 1.0, 0.3, 1.0),
    transform    0.10s ease;
}
.v2-capanim-chip:hover {
  background: rgba(255,255,255,0.07);
  border-color: rgba(255,255,255,0.18);
}
.v2-capanim-chip:active { transform: scale(0.96); }
.v2-capanim-chip.is-active {
  border-color: #22d3ee;
  background: rgba(34,211,238,0.10);
  box-shadow: 0 0 0 1px rgba(34,211,238,0.40) inset,
              0 0 14px -2px rgba(34,211,238,0.30);
}
.v2-capanim-chip-preview {
  width: 72px;
  height: 40px;
  border-radius: 6px;
  background: linear-gradient(180deg, rgba(0,0,0,0.45), rgba(0,0,0,0.22));
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
.v2-capanim-chip-preview canvas { display: block; }
.v2-capanim-chip-label {
  font: 600 11px/1 'Inter', sans-serif;
  color: rgba(255,255,255,0.78);
}
.v2-capanim-chip.is-active .v2-capanim-chip-label { color: #b8ecf6; }
.v2-capanim-colors {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}
.v2-capanim-color {
  /* v2.0.391 — audit A1.F6: bumped 32x32 → 40x40 hit area; visible
     swatch stays 32px via background-clip:content-box + 4px padding.
     Keeps the dense visual layout but clears 40x40 mis-tap zone. */
  width: 40px; height: 40px;
  background-clip: content-box;
  padding: 4px;
  border-radius: 50%;
  border: 2px solid rgba(255,255,255,0.18);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition:
    transform    0.10s ease,
    border-color 0.18s ease,
    box-shadow   0.18s ease;
  position: relative;
}
.v2-capanim-color:hover { border-color: rgba(255,255,255,0.45); }
.v2-capanim-color:active { transform: scale(0.92); }
.v2-capanim-color.is-active {
  border-color: #ffffff;
  /* v2.0.390 — Dual ring: dark inner separator + white outer halo so
     light swatches read distinct against light backgrounds and dark
     swatches against dark. */
  box-shadow:
    0 0 0 2px rgba(0,0,0,0.55),
    0 0 0 4px rgba(255,255,255,0.18);
}
/* v2.0.389 — "More" toggle in the Active color section. Inline expand
   reveals the full ColorPicker component (same one used by Text /
   Stroke / Shadow / BG / Scene). */
.v2-capanim-more-btn {
  background: rgba(255,255,255,0.06);
  border: 1px solid rgba(255,255,255,0.14);
  color: rgba(255,255,255,0.78);
  font: 600 10px/1 'Inter', sans-serif;
  text-transform: none;
  letter-spacing: 0;
  padding: 4px 9px;
  border-radius: 999px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.v2-capanim-more-btn:active { background: rgba(255,255,255,0.14); }
.v2-capanim-more-btn.is-open {
  background: rgba(34,211,238,0.18);
  border-color: rgba(34,211,238,0.55);
  color: #b8ecf6;
}
.v2-capanim-picker-wrap {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px dashed rgba(255,255,255,0.10);
}
.v2-capanim-speed-slider {
  width: 100%;
  -webkit-appearance: none;
  appearance: none;
  height: 6px;
  background: rgba(255,255,255,0.10);
  border-radius: 3px;
  outline: none;
}
.v2-capanim-speed-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 22px; height: 22px;
  border-radius: 50%;
  background: #22d3ee;
  border: 2px solid #ffffff;
  cursor: pointer;
}
.v2-capanim-speed-slider::-moz-range-thumb {
  width: 22px; height: 22px;
  border-radius: 50%;
  background: #22d3ee;
  border: 2px solid #ffffff;
  cursor: pointer;
}
.v2-capanim-speed-val {
  font: 600 11px/1 'Inter', sans-serif;
  color: #b8ecf6;
  text-transform: none;
  letter-spacing: 0;
}
.v2-capanim-speed-ticks {
  display: flex;
  justify-content: space-between;
  font: 500 10px/1 'Inter', sans-serif;
  color: rgba(255,255,255,0.45);
  margin-top: 6px;
}
.v2-transcript-rows {
  flex: 1;
  overflow-y: auto;
  padding: 8px 14px 16px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  -webkit-overflow-scrolling: touch;
}
.v2-transcript-row {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px;
  background: rgba(255,255,255,0.04);
  border-radius: 10px;
  border: 1px solid transparent;
  transition: background 120ms var(--ease), border-color 120ms var(--ease);
}
.v2-transcript-row:focus-within {
  background: rgba(163,255,18,0.06);
  border-color: rgba(163,255,18,0.30);
}
.v2-transcript-row-meta {
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  min-width: 48px;
}
.v2-transcript-row-num {
  font: 700 11px/1 'Inter', sans-serif;
  color: rgba(255,255,255,0.40);
}
.v2-transcript-row-time {
  font: 500 11px/1 'SF Mono', 'Menlo', monospace;
  color: rgba(255,255,255,0.62);
  letter-spacing: 0.04em;
}
.v2-transcript-row-text {
  flex: 1;
  min-height: 24px;
  padding: 4px 6px;
  background: transparent;
  color: #fff;
  font: 500 14px/1.4 'Inter', sans-serif;
  border-radius: 6px;
  outline: none;
  word-wrap: break-word;
  -webkit-user-select: text;
  user-select: text;
  caret-color: #a3ff12;
}
.v2-transcript-row-text:focus { background: rgba(0,0,0,0.25); }
.v2-transcript-row-del {
  flex-shrink: 0;
  background: none;
  border: none;
  color: rgba(255,255,255,0.40);
  width: 28px; height: 28px;
  border-radius: 8px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  -webkit-tap-highlight-color: transparent;
  transition: color 120ms ease, background 120ms ease;
}
.v2-transcript-row-del:active {
  color: #ff6b6b;
  background: rgba(255,72,72,0.10);
}

/* Bulk-edit textarea */
.v2-transcript-bulk {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px 14px 16px;
}
.v2-transcript-bulk-area {
  width: 100%;
  background: rgba(0,0,0,0.30);
  border: 1px solid rgba(255,255,255,0.12);
  border-radius: 10px;
  color: #fff;
  font: 500 14px/1.5 'Inter', sans-serif;
  padding: 12px;
  resize: vertical;
  min-height: 240px;
  outline: none;
  -webkit-user-select: text;
  user-select: text;
}
.v2-transcript-bulk-area:focus {
  border-color: rgba(163,255,18,0.55);
}
.v2-transcript-bulk-hint {
  font-size: 11px;
  color: rgba(255,255,255,0.55);
  padding: 0 2px;
}
.v2-transcript-bulk-save {
  background: rgba(163,255,18,0.15);
  border: 1px solid rgba(163,255,18,0.55);
  color: #a3ff12;
  padding: 10px;
  border-radius: 10px;
  font: 600 13px/1 'Inter', sans-serif;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.v2-transcript-bulk-save:active { background: rgba(163,255,18,0.25); }

/* Translate-language picker — slides up over the editor body */
.v2-transcript-translate-sheet {
  position: absolute;
  inset: 56px 0 0 0;   /* below modal head */
  background: var(--bg, #0b0b0b);
  border-top: 1px solid rgba(255,255,255,0.10);
  display: flex;
  flex-direction: column;
  z-index: 5;
}
.v2-transcript-translate-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 14px;
  border-bottom: 1px solid rgba(255,255,255,0.08);
  font: 600 13px/1 'Inter', sans-serif;
  color: rgba(255,255,255,0.92);
}
.v2-transcript-langs {
  overflow-y: auto;
  padding: 8px;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 6px;
  -webkit-overflow-scrolling: touch;
}
.v2-transcript-lang {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(255,255,255,0.10);
  border-radius: 10px;
  color: rgba(255,255,255,0.88);
  font: 500 13px/1 'Inter', sans-serif;
  cursor: pointer;
  text-align: left;
  -webkit-tap-highlight-color: transparent;
  transition: background 120ms ease, border-color 120ms ease;
}
.v2-transcript-lang:active { background: rgba(168,85,247,0.12); }
.v2-transcript-lang.is-selected {
  background: rgba(168,85,247,0.15);
  border-color: rgba(168,85,247,0.55);
}
.v2-transcript-lang-code {
  font: 700 11px/1 'SF Mono', 'Menlo', monospace;
  color: rgba(168,85,247,0.85);
  letter-spacing: 0.06em;
  min-width: 28px;
}
.v2-transcript-lang-label {
  flex: 1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.v2-transcript-langs-loading {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 30px;
  color: rgba(255,255,255,0.65);
  font-size: 13px;
}

/* v2.0.487 (#12) — masked-words ("Censor") manager */
.v2-censor-body {
  overflow-y: auto;
  padding: 12px 14px 18px;
  -webkit-overflow-scrolling: touch;
}
.v2-censor-hint {
  margin: 0 0 12px;
  color: rgba(255,255,255,0.55);
  font: 400 12px/1.45 'Inter', sans-serif;
}
.v2-censor-add {
  display: flex;
  gap: 8px;
  margin-bottom: 10px;
}
.v2-censor-input {
  flex: 1;
  min-width: 0;
  padding: 10px 12px;
  background: rgba(255,255,255,0.05);
  border: 1px solid rgba(255,255,255,0.12);
  border-radius: 10px;
  color: rgba(255,255,255,0.92);
  font: 500 13px/1 'Inter', sans-serif;
  outline: none;
}
.v2-censor-input:focus { border-color: rgba(168,85,247,0.55); }
.v2-censor-add-btn {
  padding: 10px 16px;
  background: rgba(168,85,247,0.18);
  border: 1px solid rgba(168,85,247,0.55);
  border-radius: 10px;
  color: rgba(255,255,255,0.92);
  font: 600 13px/1 'Inter', sans-serif;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.v2-censor-add-btn:disabled { opacity: 0.4; cursor: default; }
.v2-censor-group-label {
  margin: 14px 0 8px;
  color: rgba(255,255,255,0.5);
  font: 600 11px/1 'Inter', sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.v2-censor-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.v2-censor-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 10px;
  background: rgba(255,255,255,0.05);
  border: 1px solid rgba(255,255,255,0.12);
  border-radius: 999px;
  color: rgba(255,255,255,0.85);
  font: 500 12px/1 'Inter', sans-serif;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.v2-censor-chip--allow {
  background: rgba(34,197,94,0.10);
  border-color: rgba(34,197,94,0.35);
}
.v2-censor-x { color: rgba(255,255,255,0.45); font-size: 14px; line-height: 1; }
.v2-censor-allow-add {
  display: block;
  width: 100%;
  margin-top: 16px;
  padding: 10px 12px;
  background: transparent;
  border: 1px dashed rgba(255,255,255,0.18);
  border-radius: 10px;
  color: rgba(255,255,255,0.62);
  font: 500 12px/1 'Inter', sans-serif;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.v2-censor-allow-add:disabled { opacity: 0.4; cursor: default; }

.v2-transcript {
  position: relative;
}

/* =========================================================
   v2.0.372 — AI Music Studio sheet
   Mood + prompt + duration → MusicGen via /api/v2/gen/music.
   Three phases share the modal shell: compose, generating, preview.
   ========================================================= */
.v2-aimusic {
  width: 460px;
  max-width: 96vw;
  max-height: 84dvh;
  display: flex;
  flex-direction: column;
}
.v2-aimusic .v2-modal-title {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: #a3ff12;
}
.v2-aimusic-body {
  overflow-y: auto;
  padding: 14px 16px 18px;
  display: flex;
  flex-direction: column;
  gap: 16px;
  -webkit-overflow-scrolling: touch;
}
.v2-aimusic-error {
  background: rgba(255,72,72,0.10);
  border: 1px solid rgba(255,72,72,0.30);
  color: #ff8a8a;
  border-radius: 10px;
  padding: 8px 12px;
  font-size: 12px;
  line-height: 1.4;
}
.v2-aimusic-section {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.v2-aimusic-section-lbl {
  font: 700 11px/1 'Inter', sans-serif;
  color: rgba(255,255,255,0.55);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* Mood chips — same grid pattern as audio library mood chips */
.v2-aimusic-moods {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 6px;
}
@media (max-width: 380px) {
  .v2-aimusic-moods { grid-template-columns: repeat(3, 1fr); }
}
.v2-aimusic-mood {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 10px 6px;
  border-radius: 10px;
  background: rgba(255,255,255,0.05);
  border: 1px solid rgba(255,255,255,0.10);
  color: rgba(255,255,255,0.85);
  font: 600 11px/1 'Inter', sans-serif;
  cursor: pointer;
  transition: background 120ms var(--ease), border-color 120ms var(--ease);
  -webkit-tap-highlight-color: transparent;
}
.v2-aimusic-mood:active { transform: scale(0.96); }
.v2-aimusic-mood.is-active {
  background: rgba(163,255,18,0.16);
  border-color: rgba(163,255,18,0.55);
  color: #a3ff12;
}
.v2-aimusic-mood-emoji { font-size: 20px; line-height: 1; }
.v2-aimusic-mood-label { line-height: 1; }

/* Prompt textarea — pre-filled with the active mood's expanded prompt
   as placeholder so the user sees what'll be sent if they don't type. */
.v2-aimusic-prompt {
  width: 100%;
  min-height: 60px;
  resize: vertical;
  padding: 10px 12px;
  background: rgba(0,0,0,0.30);
  border: 1px solid rgba(255,255,255,0.12);
  border-radius: 10px;
  color: #fff;
  font: 500 13px/1.4 'Inter', system-ui, sans-serif;
  outline: none;
}
.v2-aimusic-prompt:focus { border-color: rgba(163,255,18,0.45); }
.v2-aimusic-prompt::placeholder { color: rgba(255,255,255,0.35); }

/* Duration chip strip */
.v2-aimusic-durations {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}
.v2-aimusic-dur {
  padding: 8px 14px;
  border-radius: 10px;
  background: rgba(255,255,255,0.06);
  border: 1px solid rgba(255,255,255,0.10);
  color: rgba(255,255,255,0.85);
  font: 600 13px/1 'Inter', sans-serif;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.v2-aimusic-dur.is-active {
  background: rgba(163,255,18,0.16);
  border-color: rgba(163,255,18,0.55);
  color: #a3ff12;
}

/* Generate CTA — primary lime button with spark icon */
.v2-aimusic-generate {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 14px;
  background: linear-gradient(135deg, rgba(163,255,18,0.20), rgba(168,85,247,0.18));
  border: 1px solid rgba(163,255,18,0.55);
  border-radius: 12px;
  color: #fff;
  font: 700 14px/1 'Inter', sans-serif;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: transform 100ms var(--ease);
}
.v2-aimusic-generate:active { transform: scale(0.98); }

/* Recent takes — one-tap re-add from localStorage history */
.v2-aimusic-recent {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.v2-aimusic-recent-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(255,255,255,0.08);
  border-radius: 10px;
  color: rgba(255,255,255,0.88);
  font: 500 12px/1.2 'Inter', sans-serif;
  cursor: pointer;
  text-align: left;
  -webkit-tap-highlight-color: transparent;
}
.v2-aimusic-recent-row:active { background: rgba(255,255,255,0.10); }
.v2-aimusic-recent-mood { font-size: 18px; line-height: 1; }
.v2-aimusic-recent-label { flex: 1; }
.v2-aimusic-recent-add {
  font: 700 10px/1 'Inter', sans-serif;
  color: #a3ff12;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* Generating state — animated equalizer + "elapsed Xs" timer */
.v2-aimusic-generating {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 36px 20px 28px;
  gap: 14px;
  min-height: 280px;
  justify-content: center;
}
.v2-aimusic-eq {
  display: flex;
  align-items: flex-end;
  gap: 4px;
  height: 48px;
}
.v2-aimusic-eq-bar {
  width: 6px;
  height: 100%;
  background: #a3ff12;
  border-radius: 3px;
  animation: v2-aimusic-eq-pulse 0.9s ease-in-out infinite;
  transform-origin: bottom;
}
@keyframes v2-aimusic-eq-pulse {
  0%, 100% { transform: scaleY(0.30); opacity: 0.5; }
  50%      { transform: scaleY(1.00); opacity: 1; }
}
.v2-aimusic-gen-title {
  font: 700 16px/1 'Inter', sans-serif;
  color: #fff;
}
.v2-aimusic-gen-sub {
  font: 500 12px/1 'SF Mono', monospace;
  color: rgba(255,255,255,0.62);
}
.v2-aimusic-gen-prompt {
  font-size: 12px;
  color: rgba(255,255,255,0.72);
  font-style: italic;
  text-align: center;
  max-width: 340px;
  line-height: 1.4;
  padding: 10px 14px;
  background: rgba(0,0,0,0.20);
  border-radius: 10px;
  border: 1px solid rgba(255,255,255,0.08);
}

/* Preview state — play button + meta + progress bar + actions triad */
.v2-aimusic-preview {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 22px 18px;
  gap: 14px;
}
.v2-aimusic-preview-meta {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  text-align: center;
}
.v2-aimusic-preview-mood {
  font: 700 16px/1 'Inter', sans-serif;
  color: #fff;
}
.v2-aimusic-preview-dur {
  font: 500 11px/1 'Inter', sans-serif;
  color: rgba(255,255,255,0.55);
}
.v2-aimusic-play {
  width: 72px; height: 72px;
  border-radius: 50%;
  background: rgba(163,255,18,0.15);
  border: 1.5px solid rgba(163,255,18,0.55);
  color: #a3ff12;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: transform 100ms var(--ease), background 140ms var(--ease);
}
.v2-aimusic-play:active { transform: scale(0.96); background: rgba(163,255,18,0.25); }
.v2-aimusic-preview-progress {
  width: 100%;
  max-width: 340px;
  height: 4px;
  background: rgba(255,255,255,0.10);
  border-radius: 2px;
  overflow: hidden;
}
.v2-aimusic-preview-progress-fill {
  height: 100%;
  background: #a3ff12;
  border-radius: 2px;
  transition: width 100ms linear;
}
.v2-aimusic-actions {
  display: flex;
  gap: 8px;
  margin-top: 4px;
  justify-content: center;
  width: 100%;
}
.v2-aimusic-action {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 10px 8px;
  border-radius: 12px;
  font: 600 11px/1 'Inter', sans-serif;
  cursor: pointer;
  border: 1px solid transparent;
  background: rgba(255,255,255,0.06);
  color: rgba(255,255,255,0.85);
  min-width: 72px;
  -webkit-tap-highlight-color: transparent;
  transition: transform 100ms var(--ease);
}
.v2-aimusic-action:active { transform: scale(0.95); }
.v2-aimusic-discard {
  background: rgba(255,72,72,0.10); color: #ff6b6b;
  border-color: rgba(255,72,72,0.30);
}
.v2-aimusic-retake {
  background: rgba(255,255,255,0.06); color: rgba(255,255,255,0.88);
  border-color: rgba(255,255,255,0.18);
}
.v2-aimusic-keep {
  background: rgba(163,255,18,0.15); color: #a3ff12;
  border-color: rgba(163,255,18,0.55);
}

/* Touch up section header — wrap-friendly for the action chips */
.v2-cutout-brush .v2-tool-sec-lbl {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

/* Arm button — primary CTA. Lime accent when armed + pulse so the
   user always knows their finger taps will paint, not scroll. */
.v2-cutout-brush-arm {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  padding: 12px;
  margin-top: 8px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--text);
  font: 700 13px/1 'Inter', system-ui, sans-serif;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
  touch-action: manipulation;
}
.v2-cutout-brush-arm:hover { background: var(--surface-3); }
.v2-cutout-brush-arm.is-armed {
  background: rgba(163, 255, 18, 0.14);
  color: var(--accent);
  border-color: var(--accent);
  animation: v2-cutout-eyedrop-pulse 1.6s ease-in-out infinite;
}

/* Mode segmented control — Add / Erase */
.v2-cutout-brush-mode {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px;
  margin-top: 12px;
}
.v2-cutout-brush-mode-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 10px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  color: var(--text-2);
  font: 700 11px/1 'Inter', system-ui, sans-serif;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
}
.v2-cutout-brush-mode-btn:hover { background: var(--surface-3); color: var(--text); }
.v2-cutout-brush-mode-btn.is-on {
  background: rgba(163, 255, 18, 0.10);
  border-color: var(--accent);
  color: var(--accent);
}

/* Edge snap checkbox row */
.v2-cutout-brush-snap {
  display: grid;
  grid-template-columns: 18px 1fr;
  gap: 8px;
  align-items: start;
  margin-top: 12px;
  cursor: pointer;
  user-select: none;
}
.v2-cutout-brush-snap input[type="checkbox"] {
  margin: 2px 0 0;
  accent-color: var(--accent);
}
.v2-cutout-brush-snap > span:first-of-type {
  font: 600 12px/1.2 'Inter', system-ui, sans-serif;
  color: var(--text);
}
.v2-cutout-brush-snap-hint {
  grid-column: 2;
  font: 500 11px/1.3 'Inter', system-ui, sans-serif;
  color: var(--text-3);
}

/* =========================================================
   v2.0.313 — Symmetric brush controls + canvas axis overlay +
   mirrored cursor. CapCut: vertical-center-only mirror with a
   single cursor. Ours: 3 axis modes + draggable axis in custom
   mode + face-center auto-set + mirrored cursor preview.
   ========================================================= */
.v2-cutout-brush-symmetry-axis {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 6px;
  margin: 10px 0 8px;
}
.v2-cutout-brush-axis-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 8px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  color: var(--text-2);
  font: 700 11px/1 'Inter', system-ui, sans-serif;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
}
.v2-cutout-brush-axis-btn:hover { background: var(--surface-3); color: var(--text); }
.v2-cutout-brush-axis-btn.is-on {
  background: rgba(163, 255, 18, 0.10);
  border-color: var(--accent);
  color: var(--accent);
}
.v2-cutout-brush-symmetry-pos {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 8px 0;
  font: 500 11px/1 'Inter', system-ui, sans-serif;
  color: var(--text-3);
}
.v2-cutout-brush-symmetry-pos strong {
  color: var(--text);
  font-weight: 700;
}
.v2-cutout-brush-symmetry-pos .v2-slider {
  flex: 1;
}

/* Canvas axis-line overlay. Lime dashed line spanning the full
   canvas dimension. Custom mode adds a draggable handle indicator
   + a cursor change so the user knows they can grab it. */
.v2-cutout-brush-axis {
  position: absolute;
  z-index: 70;
  pointer-events: none;
}
.v2-cutout-brush-axis--v {
  border-left: 1.5px dashed rgba(163, 255, 18, 0.85);
  filter: drop-shadow(0 0 4px rgba(163, 255, 18, 0.45));
}
.v2-cutout-brush-axis--h {
  border-top: 1.5px dashed rgba(163, 255, 18, 0.85);
  filter: drop-shadow(0 0 4px rgba(163, 255, 18, 0.45));
}
.v2-cutout-brush-axis.is-draggable {
  pointer-events: auto;
  cursor: ew-resize;
}
.v2-cutout-brush-axis--h.is-draggable {
  cursor: ns-resize;
}
/* Grip handle: middle of the line, indicates draggability in custom
   mode. v2.0.314 — bumped 16→22 for better visibility + added an
   ::after invisible hit-pad below so the EFFECTIVE touch target meets
   WCAG 2.5.5 / Apple HIG 44×44 minimum without the visual handle
   growing into a thumb-sized blob. The pseudo-element pattern keeps
   the visual pinned to sym.pos while extending hit area ±22px in the
   drag direction. */
.v2-cutout-brush-axis.is-draggable::before {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: 22px; height: 22px;
  background: var(--accent);
  border: 2px solid #0a0b10;
  border-radius: 50%;
  box-shadow: 0 0 0 1.5px var(--accent), 0 2px 8px rgba(0, 0, 0, 0.6);
  pointer-events: none;
  z-index: 1;
}
/* Invisible hit-pad. Vertical axis: expand ±22px horizontally so a
   sloppy finger tap anywhere within ~45px of the line still lands.
   Horizontal axis: expand ±22px vertically. ::after inherits
   pointer-events:auto from the parent (set inline when is-draggable)
   and bubbles taps to the parent's onPointerDown handler. */
.v2-cutout-brush-axis--v.is-draggable::after {
  content: '';
  position: absolute;
  top: 0; bottom: 0;
  left: -22px; right: -22px;
  z-index: 0;
}
.v2-cutout-brush-axis--h.is-draggable::after {
  content: '';
  position: absolute;
  left: 0; right: 0;
  top: -22px; bottom: -22px;
  z-index: 0;
}

/* Mirrored cursor — half opacity + dashed border so it visually
   reads as "secondary preview", not as a real cursor. Same color
   tint as the primary cursor (lime for add, rose for erase). */
.v2-brush-cursor--mirror {
  border-style: dashed;
}
.v2-brush-cursor--mirror::before,
.v2-brush-cursor--mirror::after {
  opacity: 0.5;
}

/* Action chips row */
.v2-cutout-brush-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 12px;
}
.v2-cutout-brush-actions .v2-vol-kf-pin:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
.v2-cutout-brush-restore {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 14px;
  border-radius: 999px;
  background: transparent;
  border: 1px dashed rgba(163, 255, 18, 0.45);
  color: var(--accent);
  font: 700 11px/1 'Inter', system-ui, sans-serif;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease), border-style var(--t-fast) var(--ease);
}
.v2-cutout-brush-restore:hover {
  background: rgba(163, 255, 18, 0.08);
  border-style: solid;
}

/* While-armed hint copy */
.v2-cutout-brush-armed-hint {
  margin-top: 10px;
  padding: 8px 12px;
  background: rgba(163, 255, 18, 0.06);
  border: 1px solid rgba(163, 255, 18, 0.18);
  border-radius: var(--r-sm);
  color: var(--text-2);
  font: 500 11px/1.4 'Inter', system-ui, sans-serif;
}

/* =========================================================
   Canvas brush surface + cursor ring.
   The .v2-cutout-brush-surface mounts inside .v2-canvas-frame
   when the user arms brush mode. Fills the frame, captures all
   pointer events, hides ALL other canvas overlay hit-targets
   via the parent .is-brush-armed class.
   ========================================================= */
.v2-canvas-frame.is-brush-armed > *:not(.v2-canvas-paint):not(.v2-cutout-brush-surface) {
  pointer-events: none;
}
.v2-canvas-frame.is-brush-armed {
  cursor: crosshair;
}
/* v2.0.498 (#6) — Consistent armed-mode hint. The eyedropper + subject-pick
   modes show a top pill ("Tap to pick key color" / "Tap the subject"); the brush
   only had the ring cursor, so the armed state read differently. Give it the
   SAME pill so every canvas-arming tool signals the same way. z-index 70 keeps
   it above the brush surface (z-60); pointer-events:none so it never blocks a
   stroke. The pinned Brush/Erase chip highlight already says add-vs-erase. */
.v2-canvas-frame.is-brush-armed::after {
  content: 'Paint to refine the cutout';
  position: absolute;
  top: 12px; left: 50%;
  transform: translateX(-50%);
  padding: 6px 12px;
  background: rgba(10, 11, 16, 0.85);
  color: var(--accent);
  font: 700 11px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  border-radius: 999px;
  border: 1px solid rgba(163, 255, 18, 0.55);
  pointer-events: none;
  z-index: 70;
  animation: v2-cutout-eyedrop-pulse 1.4s ease-in-out infinite;
}
.v2-cutout-brush-surface {
  position: absolute;
  inset: 0;
  z-index: 60;
  background: transparent;
  touch-action: none;
  cursor: crosshair;
}
.v2-brush-cursor {
  position: absolute;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  border: 2px solid var(--accent);
  background: rgba(163, 255, 18, 0.08);
  pointer-events: none;
  transition: width 60ms ease-out, height 60ms ease-out;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.45), 0 0 12px rgba(163, 255, 18, 0.35);
}
.v2-brush-cursor--erase {
  border-color: rgba(255, 99, 132, 0.95);
  background: rgba(255, 99, 132, 0.08);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.45), 0 0 12px rgba(255, 99, 132, 0.35);
}
/* Inner crosshair tick so the user sees the brush center exactly */
.v2-brush-cursor::before,
.v2-brush-cursor::after {
  content: '';
  position: absolute;
  background: currentColor;
  opacity: 0.7;
}
.v2-brush-cursor::before {
  top: 50%; left: 50%;
  width: 8px; height: 1px;
  transform: translate(-50%, -50%);
}
.v2-brush-cursor::after {
  top: 50%; left: 50%;
  width: 1px; height: 8px;
  transform: translate(-50%, -50%);
}
.v2-brush-cursor--erase::before,
.v2-brush-cursor--erase::after {
  background: rgba(255, 99, 132, 0.95);
}

/* =========================================================
   v2.0.317 — TrackPanel (subject tracking for stickers + text)
   Inside TextStylePanel + StickerStylePanel under "Track" tab.
   Shared component, shared CSS.
   ========================================================= */
.v2-track-panel {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.v2-track-panel-hint {
  font: 500 12px/1.4 'Inter', system-ui, sans-serif;
  color: var(--text-3);
  letter-spacing: 0.01em;
  margin: -4px 2px 0;
}
.v2-track-panel-warn {
  padding: 10px 12px;
  background: rgba(245, 158, 11, 0.10);
  border: 1px solid rgba(245, 158, 11, 0.40);
  border-radius: var(--r-sm);
  color: rgba(252, 211, 77, 1);
  font: 500 12px/1.4 'Inter', system-ui, sans-serif;
}
.v2-track-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 10px 14px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  color: var(--text);
  font: 600 13px/1 'Inter', system-ui, sans-serif;
  cursor: pointer;
  user-select: none;
  transition: background 120ms ease, border-color 120ms ease, transform 80ms ease;
}
.v2-track-btn:hover  { background: var(--surface-3); }
.v2-track-btn:active { transform: scale(0.98); }
.v2-track-btn--primary {
  background: rgba(163, 255, 18, 0.10);
  border-color: var(--accent);
  color: var(--accent);
  padding: 14px 18px;
  font-size: 14px;
  align-self: stretch;
}
.v2-track-btn--primary:hover { background: rgba(163, 255, 18, 0.18); }
.v2-track-btn--retry {
  background: rgba(255, 99, 132, 0.10);
  border-color: rgba(255, 99, 132, 0.65);
  color: rgba(255, 145, 165, 1);
}
.v2-track-btn--retry:hover { background: rgba(255, 99, 132, 0.18); }
.v2-track-btn--danger { color: rgba(255, 145, 165, 1); }
.v2-track-btn--danger:hover {
  background: rgba(255, 99, 132, 0.10);
  border-color: rgba(255, 99, 132, 0.50);
}
.v2-track-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.v2-track-progress {
  position: relative;
  height: 8px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 4px;
  overflow: hidden;
  margin-top: 22px;  /* room for the floating % label above the bar */
}
.v2-track-progress-fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: 0%;
  background: linear-gradient(90deg, var(--accent), rgba(163, 255, 18, 0.65));
  transition: width 140ms ease-out;
}
.v2-track-progress-label {
  position: absolute;
  top: -22px;
  right: 2px;
  font: 600 11px/1 'Inter', system-ui, sans-serif;
  color: var(--accent);
  letter-spacing: 0.01em;
}

/* =========================================================
   v2.0.320 — Object-tracking UI (mode toggle + bbox selector)
   ========================================================= */

/* Face / Object mode chips in TrackPanel */
.v2-track-mode {
  display: flex;
  gap: 6px;
  padding: 4px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
}
.v2-track-mode-btn {
  flex: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 8px 10px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: calc(var(--r-sm) - 2px);
  color: var(--text-2);
  font: 600 12px/1 'Inter', system-ui, sans-serif;
  cursor: pointer;
  user-select: none;
  transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
}
.v2-track-mode-btn:hover { color: var(--text); }
.v2-track-mode-btn.is-on {
  background: rgba(163, 255, 18, 0.10);
  border-color: var(--accent);
  color: var(--accent);
}

/* Track panel warn — "active" variant has the lime accent (during
   bbox draw) vs the default amber (general warnings). */
.v2-track-panel-warn--active {
  background: rgba(163, 255, 18, 0.10);
  border-color: rgba(163, 255, 18, 0.40);
  color: var(--accent);
  animation: v2TrackHintPulse 1.6s ease-in-out infinite;
}
@keyframes v2TrackHintPulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.65; }
}

/* Bbox selector — canvas-frame overlay mounted while user is in
   "Mark subject" mode. z-index above all other overlays so the user's
   drag isn't intercepted by sticker hit-targets. */
.v2-track-bbox-surface {
  position: absolute;
  inset: 0;
  z-index: 65;          /* above brush surface (60), below sheets */
  background: rgba(0, 0, 0, 0.18);
  cursor: crosshair;
  touch-action: none;
  user-select: none;
}
.v2-track-bbox-hint {
  position: absolute;
  top: 12px;
  left: 50%;
  transform: translateX(-50%);
  padding: 8px 14px;
  background: rgba(0, 0, 0, 0.75);
  border: 1px solid var(--accent);
  border-radius: var(--r-sm);
  color: var(--accent);
  font: 600 12px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.02em;
  white-space: nowrap;
  pointer-events: none;
  animation: v2TrackHintPulse 1.6s ease-in-out infinite;
}
.v2-track-bbox-preview {
  position: absolute;
  border: 2px dashed var(--accent);
  background: rgba(163, 255, 18, 0.10);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.55), 0 0 12px rgba(163, 255, 18, 0.35);
  pointer-events: none;
  border-radius: 2px;
}

/* Committed bbox — solid lime border, kept on screen until user
   either confirms ("Track this") or re-selects. */
.v2-track-bbox-committed {
  position: absolute;
  z-index: 64;
  border: 2px solid var(--accent);
  background: transparent;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.55), 0 0 14px rgba(163, 255, 18, 0.42);
  pointer-events: none;
  border-radius: 2px;
}
.v2-track-bbox-committed-label {
  position: absolute;
  top: -22px;
  left: 0;
  padding: 3px 8px;
  background: var(--accent);
  color: #0a0b10;
  border-radius: 4px;
  font: 700 10px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* =========================================================
   v2.0.321 — Tracking polish: resizable bbox editor + live
   tracking preview overlay during compute + cancel button.
   ========================================================= */

/* Bbox editor — replaces v320's draw-from-scratch surface. The
   surface itself is a transparent layer; the interactive bbox sits
   inside with corner handles + a whole-body drag area. */
.v2-track-bbox-editor-surface {
  position: absolute;
  inset: 0;
  z-index: 65;
  background: rgba(0, 0, 0, 0.22);
  touch-action: none;
  user-select: none;
}
.v2-track-bbox-editor {
  position: absolute;
  border: 2px solid var(--accent);
  background: rgba(163, 255, 18, 0.10);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.55), 0 0 14px rgba(163, 255, 18, 0.42);
  border-radius: 2px;
  cursor: move;
  touch-action: none;
}
.v2-track-bbox-handle {
  position: absolute;
  width: 16px;
  height: 16px;
  background: var(--accent);
  border: 2px solid #0a0b10;
  border-radius: 50%;
  box-shadow: 0 0 0 1px var(--accent), 0 2px 6px rgba(0, 0, 0, 0.55);
  touch-action: none;
}
/* Invisible hit-pad around each handle — 48×48 target hit area so
   thumbs on phone can grab corners without missing. Same pattern as
   v314 axis grip. */
.v2-track-bbox-handle::after {
  content: '';
  position: absolute;
  inset: -16px;          /* 16 visual + 16+16 = 48 total */
}
.v2-track-bbox-handle--tl { top: -8px;  left: -8px;  cursor: nwse-resize; }
.v2-track-bbox-handle--tr { top: -8px;  right: -8px; cursor: nesw-resize; }
.v2-track-bbox-handle--bl { bottom: -8px; left: -8px; cursor: nesw-resize; }
.v2-track-bbox-handle--br { bottom: -8px; right: -8px; cursor: nwse-resize; }

/* Live tracking preview overlay — shows the tracker walking the clip
   in real time. Surface is non-interactive (pointer-events:none) so
   the user can still see the underlying canvas. */
.v2-track-preview-surface {
  position: absolute;
  inset: 0;
  z-index: 67;
  pointer-events: none;
}
.v2-track-preview-trail {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
.v2-track-preview-trail polyline {
  stroke: rgba(163, 255, 18, 0.55);
  stroke-width: 0.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  fill: none;
  filter: drop-shadow(0 0 3px rgba(163, 255, 18, 0.6));
}
.v2-track-preview-bbox {
  position: absolute;
  border: 2px solid var(--accent);
  background: transparent;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.55), 0 0 14px rgba(163, 255, 18, 0.55);
  border-radius: 2px;
  animation: v2TrackPreviewPulse 0.8s ease-in-out infinite;
}
.v2-track-preview-bbox.is-lost {
  border-color: rgba(255, 99, 132, 0.85);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.55), 0 0 14px rgba(255, 99, 132, 0.55);
  animation: v2TrackPreviewLost 1.2s ease-in-out infinite;
}
.v2-track-preview-pulse {
  position: absolute;
  width: 14px;
  height: 14px;
  margin-left: -7px;
  margin-top: -7px;
  background: var(--accent);
  border: 2px solid #0a0b10;
  border-radius: 50%;
  box-shadow: 0 0 0 1px var(--accent), 0 0 12px rgba(163, 255, 18, 0.7);
  animation: v2TrackPreviewPulse 0.8s ease-in-out infinite;
}
.v2-track-preview-pulse.is-lost {
  background: rgba(255, 99, 132, 0.95);
  border-color: #0a0b10;
  box-shadow: 0 0 0 1px rgba(255, 99, 132, 0.95), 0 0 12px rgba(255, 99, 132, 0.7);
}
.v2-track-preview-lost-label {
  position: absolute;
  bottom: 14px;
  left: 50%;
  transform: translateX(-50%);
  padding: 4px 10px;
  background: rgba(255, 99, 132, 0.95);
  color: #0a0b10;
  border-radius: 4px;
  font: 700 10px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
@keyframes v2TrackPreviewPulse {
  0%, 100% { transform: scale(1);   opacity: 1; }
  50%      { transform: scale(1.08); opacity: 0.75; }
}
@keyframes v2TrackPreviewLost {
  0%, 100% { transform: scale(1);   opacity: 0.75; }
  50%      { transform: scale(1.04); opacity: 0.5; }
}
/* v2.0.322 — Dead override removed. The previous rule disabled the
   pulse animation citing a transform/margin conflict that doesn't
   actually exist (negative margins are layout offsets; transforms
   operate independently). The dot now pulses in sync with the bbox.
   See v321 audit finding #7. */

/* Cancel button while computing — wider than the chip buttons in the
   done-state actions row so it's an unambiguous target. */
.v2-track-btn--cancel {
  align-self: stretch;
}

/* =========================================================
   v2.0.327 — "Scale with subject" toggle inside TrackPanel.
   Mirrors the .v2-cutout-brush-snap (v311) pattern for visual
   consistency across the inspector — checkbox + bold label +
   muted hint below.
   ========================================================= */
.v2-track-snap {
  display: grid;
  grid-template-columns: 18px 1fr;
  gap: 8px;
  align-items: start;
  padding: 8px 4px;
  cursor: pointer;
  user-select: none;
}
.v2-track-snap input[type="checkbox"] {
  margin: 2px 0 0;
  accent-color: var(--accent);
}
.v2-track-snap > span:first-of-type {
  font: 600 12px/1.2 'Inter', system-ui, sans-serif;
  color: var(--text);
}
.v2-track-snap-hint {
  grid-column: 2;
  font: 500 11px/1.3 'Inter', system-ui, sans-serif;
  color: var(--text-3);
}

/* =========================================================
   v2.0.332 — Re-acquisition visuals.
   Three new bbox states (is-searching, is-recovered, is-lost
   sharpened) + the expanding search-radius halo + the recovery
   stat chip in TrackPanel. CapCut Pro just snaps; ours
   communicates the recovery dance to the user.
   ========================================================= */
.v2-track-preview-bbox.is-searching {
  border-color: rgba(255, 99, 132, 0.95);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.55),
              0 0 20px rgba(255, 99, 132, 0.65);
  animation: v2TrackPreviewSearching 1.1s ease-in-out infinite;
}
@keyframes v2TrackPreviewSearching {
  0%, 100% {
    transform: scale(1);
    opacity: 0.85;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.55), 0 0 20px rgba(255, 99, 132, 0.65);
  }
  50% {
    transform: scale(1.10);
    opacity: 0.55;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.55), 0 0 36px rgba(255, 99, 132, 0.85);
  }
}

/* Expanding search halo — a separate element rendered as a
   centered circle around the bbox. Animates outward to communicate
   "search radius has expanded." Pure visual flourish; no semantic
   meaning beyond "tracker is looking wider for the subject." */
.v2-track-preview-search-halo {
  position: absolute;
  width: 14%;
  height: 14%;
  margin-left: -7%;
  margin-top: -7%;
  border-radius: 50%;
  border: 2px solid rgba(255, 99, 132, 0.55);
  pointer-events: none;
  animation: v2TrackPreviewHalo 1.4s ease-out infinite;
}
@keyframes v2TrackPreviewHalo {
  0%   { transform: scale(0.5); opacity: 0.9; border-color: rgba(255, 99, 132, 0.9); }
  100% { transform: scale(2.4); opacity: 0;   border-color: rgba(255, 99, 132, 0); }
}

/* Recovered state — brief green flash on the first post-recovery
   sample. Drives ONE animation cycle then fades on the next sample. */
.v2-track-preview-bbox.is-recovered {
  border-color: rgba(80, 240, 120, 0.95);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.55),
              0 0 24px rgba(80, 240, 120, 0.9);
  animation: v2TrackPreviewRecovered 0.6s ease-out 1;
}
@keyframes v2TrackPreviewRecovered {
  0%   { transform: scale(0.85); opacity: 0.4; }
  35%  { transform: scale(1.18); opacity: 1;   }
  100% { transform: scale(1);    opacity: 1;   }
}
.v2-track-preview-pulse.is-searching {
  background: rgba(255, 99, 132, 0.95);
  border-color: #0a0b10;
  box-shadow: 0 0 0 1px rgba(255, 99, 132, 0.95), 0 0 12px rgba(255, 99, 132, 0.8);
}
.v2-track-preview-pulse.is-recovered {
  background: rgba(80, 240, 120, 0.95);
  border-color: #0a0b10;
  box-shadow: 0 0 0 1px rgba(80, 240, 120, 0.95), 0 0 14px rgba(80, 240, 120, 0.9);
}

.v2-track-preview-search-label {
  position: absolute;
  bottom: 14px;
  left: 50%;
  transform: translateX(-50%);
  padding: 4px 10px;
  background: rgba(255, 99, 132, 0.95);
  color: #0a0b10;
  border-radius: 4px;
  font: 700 10px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
  animation: v2TrackPreviewSearching 1.1s ease-in-out infinite;
}
.v2-track-preview-recovered-label {
  position: absolute;
  bottom: 14px;
  left: 50%;
  transform: translateX(-50%);
  padding: 4px 10px;
  background: rgba(80, 240, 120, 0.95);
  color: #0a0b10;
  border-radius: 4px;
  font: 700 10px/1 'Inter', system-ui, sans-serif;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
  animation: v2TrackPreviewRecovered 0.6s ease-out 1;
}

/* TrackPanel "auto-recovered" GOOD-news chip — distinct from the
   amber warning. Green-tinted to signal success. */
.v2-track-panel-good {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 10px 12px;
  background: rgba(80, 240, 120, 0.10);
  border: 1px solid rgba(80, 240, 120, 0.40);
  border-radius: var(--r-sm);
  color: rgba(150, 246, 175, 1);
  font: 500 12px/1.4 'Inter', system-ui, sans-serif;
}
.v2-track-panel-good svg {
  margin-top: 1px;
  flex-shrink: 0;
}

/* v2.0.333 — Reduced-motion support for the new v332 tracking
   preview animations. Users with vestibular sensitivity / motion
   sickness need to be able to silence pulsing UI per OS preference.
   Disables the three keyframe loops + the recovered flash; the
   bbox + halo + labels still RENDER (their state info is still
   useful) — they just don't animate. WCAG 2.3.3 + 2.3.1 compliance.
   Covers v320 bbox pulses + lost states too for consistency. */
@media (prefers-reduced-motion: reduce) {
  .v2-track-preview-bbox,
  .v2-track-preview-bbox.is-searching,
  .v2-track-preview-bbox.is-recovered,
  .v2-track-preview-bbox.is-lost,
  .v2-track-preview-search-halo,
  .v2-track-preview-search-label,
  .v2-track-preview-recovered-label,
  .v2-track-preview-lost-label {
    animation: none !important;
  }
  /* The searching halo's whole purpose is the expand animation; with
     animation disabled it'd just be a static small circle. Hide it
     entirely under reduced-motion — the bbox color + label still
     communicate the searching state without the spinning visual. */
  .v2-track-preview-search-halo {
    display: none;
  }
  /* v2.0.339 — Global motion suppression. Pre-v339, the only
     reduced-motion block covered tracking previews. Sheet slide-ins,
     fade-ins, backdrop animations, bbox entry animations, press-pulses,
     header-saved chip — all still ran at full intensity for users with
     vestibular sensitivity. Now we clamp every animation and transition
     to ~instant while preserving end-states (color/transform still
     APPLY, they just don't ANIMATE there). */
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* v2.0.339 — Canvas overlay press feedback. The gesture.js
   makeTapAndDrag handler applies .is-pressing on pointerdown so the
   user gets a visible ack within one frame during the long-press wait.
   Without this, canvas overlays felt dead for 450-500ms after the
   user's finger landed — they'd lift before the long-press fired and
   think the editor was broken. Subtle brightness lift + scale bump;
   not loud enough to feel like a press confirmation, just enough to
   communicate "I'm registering this."
   v2.0.340 — Extended to .v2-iconbtn so toolbar long-presses get the
   same ack feedback. Keyframe dots + ribbon items also covered. */
.v2-canvas-text.is-pressing,
.v2-canvas-sticker.is-pressing,
.v2-canvas-pip.is-pressing,
.v2-iconbtn.is-pressing,
.v2-kf-dot.is-pressing,
.v2-ribbon-btn.is-pressing {
  filter: brightness(1.08);
  transition: filter 80ms var(--ease);
}

/* v2.0.340 — Press confirmation on every category / preset chip.
   Audit found these fire HAPTIC_LIFT on tap but had no visible
   within-frame ack — the chip changed only AFTER value swap. Subtle
   scale-down communicates the press registered immediately. */
.v2-chip:active,
[class*="-chip"]:active,
[class*="-cat"]:active,
.v2-preset:active,
[class*="-preset"]:active {
  transform: scale(0.94);
  transition: transform 90ms var(--ease);
}

/* v2.0.340 — Global keyboard focus ring. Audit found `outline: 0` /
   `outline: none` stripped in 12+ places but only 7 `:focus-visible`
   restorations. Keyboard users navigating the editor hit elements
   with no visible focus. Global rule + per-site overrides where the
   ring would clash visually. Mouse/touch users don't see this
   (`:focus-visible` only fires for keyboard nav). */
*:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}

