    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

    :root {
      /* ── Liquid Glass palette ──
         Same variable names as default.css so most existing rules
         pick up the new look automatically via var(...). */
      --bg:        #0c0c10;
      --surface:   rgba(20, 18, 22, 0.55);
      --border:    #404040;
      --border-strong: rgba(255, 255, 255, 0.14);
      --accent:    #f5c842;
      --accent-dim:#d4a838;
      --accent-soft:rgba(245, 200, 66, 0.14);
      --text:      #f0eee8;
      --muted:     rgba(240, 238, 232, 0.45);
      --danger:    #ff6b6b;
      --success:   #5fd17e;

      /* Glass tokens (new, used by panels/dialogs) */
      --glass-bg:    rgba(20, 18, 22, 0.30);
      --glass-blur:  blur(7px) saturate(150%);
      --input-bg:    rgba(0, 0, 0, 0.30);
      --btn-bg:      rgba(255, 255, 255, 0.04);

      /* Fonts: same DM Sans / DM Mono / Bebas Neue family as the
         original index.html — visually warmer and more "designed" than
         the system stack. Bebas Neue powers the LAYRD wordmark. */
      --mono:      'DM Mono', ui-monospace, monospace;
      --sans:      'DM Sans', system-ui, sans-serif;
      --display:   'Bebas Neue', 'DM Sans', sans-serif;

      /* ── Phase color families ──
         Four kf phases (Entry / Exit / Hover / Mask) each have a
         family of tones used across the whole UI — kf-diamond rings,
         track-bar fills, layer badges, ease curves, etc. Defining
         them once at :root means changing a phase's hue tomorrow is
         a single edit instead of hand-tweaking 20+ rgba() literals.

         Naming convention:
           --color-<phase>           hex / solid value
           --color-<phase>-soft      ~8% bg tint (chips, glow halos)
           --color-<phase>-border    ~40% border tint (chip outlines)
           --color-<phase>-ring      ~85% kf-diamond resting ring
           --color-<phase>-bright    ~95-100% active / selected emphasis

         Entry's family aliases to --accent so the existing 143+
         var(--accent) references keep working — they're now the
         canonical "Entry phase" token by another name. */
      --color-entry:        var(--accent);
      --color-entry-soft:   var(--accent-soft);
      --color-entry-border: rgba(245, 200, 66, 0.4);
      --color-entry-ring:   rgba(245, 200, 66, 0.85);
      --color-entry-bright: rgba(245, 200, 66, 0.95);

      --color-exit:         #ffaa3c;
      --color-exit-soft:    rgba(255, 170, 60, 0.08);
      --color-exit-border:  rgba(255, 170, 60, 0.4);
      --color-exit-ring:    rgba(255, 170, 60, 0.9);
      --color-exit-bright:  rgba(255, 170, 60, 1);

      --color-hover:        #5fb8c8;
      --color-hover-soft:   rgba(95, 184, 200, 0.08);
      --color-hover-border: rgba(95, 184, 200, 0.4);
      --color-hover-ring:   rgba(95, 184, 200, 0.85);
      --color-hover-bright: rgba(95, 184, 200, 0.95);

      --color-mask:         #5ec684;
      --color-mask-soft:    rgba(94, 198, 132, 0.08);
      --color-mask-border:  rgba(94, 198, 132, 0.4);
      --color-mask-ring:    rgba(45, 130, 80, 0.85);
      --color-mask-bright:  rgba(94, 198, 132, 0.95);
      --color-mask-fill:    rgba(94, 198, 132, 0.95);

      /* Group violet — Phase 7 introduced grouped layers as a first-
         class concept; the chrome shares the same family layout the
         entry / exit / hover / mask tokens use so future tweaks
         touch one place. The -rgb component lets per-rule alpha
         values stay precise without forking a token per shade
         (`rgba(var(--color-group-rgb), 0.45)` etc.). */
      --color-group-rgb:    160, 130, 240;
      --color-group:        rgb(var(--color-group-rgb));
      --color-group-soft:   rgba(var(--color-group-rgb), 0.10);
      --color-group-border: rgba(var(--color-group-rgb), 0.4);
      --color-group-ring:   rgba(var(--color-group-rgb), 0.85);
      --color-group-bright: rgb(190, 165, 250);
      --color-group-text:   rgb(200, 180, 255);

      /* Backwards-compat aliases — the existing --kf-*-ring family
         that .track-keyframe-{type} reads from. New code should
         prefer the --color-<phase>-* family above. */
      --kf-entry-ring:        var(--color-entry-ring);
      --kf-entry-ring-bright: var(--color-entry-bright);
      --kf-exit-ring:         var(--color-exit-ring);
      --kf-exit-ring-bright:  var(--color-exit-bright);
      --kf-hover-ring:        var(--color-hover-ring);
      --kf-hover-ring-bright: var(--color-hover-bright);
      --kf-mask-ring:         var(--color-mask-ring);
      --kf-mask-ring-bright:  var(--color-mask-bright);
      --kf-mask-fill:         var(--color-mask-fill);
    }

    /* Body: warm radial-wash + dark gradient only (no checker).
       The checkerboard now lives behind #canvas-container so it pans with
       the artboard when the user space-drags. Panels' backdrop-filter
       blurs this layer so it reads as frosted noise through glass.
       Body has small padding + gap so panels detach from each other and
       the viewport edges — the floating-cards layout from the mock. */
    body {
      color: var(--text);
      font-family: var(--sans);
      min-height: 100vh;
      display: flex;
      flex-direction: column;
      padding: 5px;
      gap: 5px;
      /* Plain near-black surface — quiet neutral background that
         doesn't compete with the canvas + UI chrome. */
      background-color: #08080b;
    }

    /* Checker pattern that lives behind the artboard. It's a child of
       #canvas-container, which is the element the pan-drag JS transforms
       — so when you space-drag the canvas, this checker translates with
       it (Figma-style infinite-canvas feel). The inset extends 4000px in
       every direction so even a wide pan never reveals an edge; the parent
       #canvas-area's overflow:hidden clips anything past the visible area. */
    #canvas-container::before {
      content: '';
      position: absolute;
      inset: -4000px;
      z-index: -1;
      pointer-events: none;
      background-color: transparent;
      background-image:
        linear-gradient(45deg, rgba(255, 255, 255, 0.06) 25%, transparent 25%, transparent 75%, rgba(255, 255, 255, 0.06) 75%),
        linear-gradient(45deg, rgba(255, 255, 255, 0.06) 25%, transparent 25%, transparent 75%, rgba(255, 255, 255, 0.06) 75%);
      background-size: 30px 30px, 30px 30px;
      background-position: 0 0, 15px 15px;
    }

    /* ── Shared glass-card treatment ──
       Floating panels: flat dark surface + heavy backdrop blur. The
       warm gradient was visually noisy; a flat near-black with the
       blur layer reads cleaner while still feeling like glass.
       Alpha is deliberately 0.78 (not browser-default-low) so panels
       stay visibly demarcated against the canvas-area body — at lower
       alpha the body checkerboard reads through enough that adjacent
       panels start to blend visually. Don't drop below ~0.75 without
       a deliberate redesign of the panel/body contrast.
       Note: #canvas-area is INTENTIONALLY excluded — the rendered banner
       must sit directly on the body checkerboard with no card behind it. */
    header,
    .panel-left,
    #vars-panel,
    .panel-right.has-manifest .vars-panel-main,
    #transport,
    #track-editor {
      background: rgba(12, 11, 16, 0.78);
      backdrop-filter: var(--glass-blur);
      -webkit-backdrop-filter: var(--glass-blur);
      border: 1px solid rgba(255, 255, 255, 0.05);
      border-radius: 12px;
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.04),
        0 6px 20px -8px rgba(0, 0, 0, 0.5),
        0 1px 3px rgba(0, 0, 0, 0.3);
    }

    /* ── Header ── frosted glass card */
    header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 12px 18px;
      flex-shrink: 0;
      /* glass treatment from shared block above */
      /* The shared glass block applies backdrop-filter, which
         creates a stacking context. Without an explicit z-index,
         that context defaults to auto (≈ 0) in the parent stack —
         so absolute-positioned children of the header (notably the
         View dropdown panel, and the centered editor-mode toggle)
         render BELOW any sibling that has its own positive z-index
         (rulers at 30, ruler-corner at 31, etc.). Lift the header
         itself above those so its dropdown panel can paint over
         the canvas-area chrome. */
      position: relative;
      z-index: 100;
    }

    /* Disabled state for the editor-mode pills. Set via the native
       `disabled` attribute (toggled by initManifestGatedButtons when
       body.has-manifest flips), so the browser handles cursor: not-
       allowed and click-blocking the same way it does for undo/redo. */
    .editor-mode-pill:disabled {
      cursor: not-allowed;
    }

    .logo {
      display: inline-flex;
      align-items: center;
      line-height: 1;
      cursor: pointer;
      /* GPU layer hint so the GSAP hover animation runs smooth without
         restyling neighboring header chrome. */
      will-change: transform;
    }
    /* SVG sized to match the prior text wordmark's optical height. The
       SVG ships its own colors via internal <style>, so no fill rules
       are needed here. */
    .logo svg {
      height: 32px;
      width: auto;
      display: block;
      /* transform-origin on the bars + letters is managed per-element
         by the GSAP setup in index.html; this just lets the SVG itself
         transform cleanly inside the inline-flex container. */
      overflow: visible;
    }
    /* Each letter group + bar gets transform-origin centered on its
       own bounding box so the GSAP scale/rotate/translate composes
       around the visual center rather than the SVG (0,0). transform-
       box: fill-box scopes the origin to the SVG element's local box. */
    .logo svg #letter-L,
    .logo svg #letter-A,
    .logo svg #letter-Y,
    .logo svg #letter-R,
    .logo svg #letter-D,
    .logo svg #layer-bars > rect {
      transform-box: fill-box;
      transform-origin: center;
    }
    /* Background rounded square stays anchored at its own center too
       so a subtle hover rotation pivots through the middle. */
    .logo svg #background {
      transform-box: fill-box;
      transform-origin: center;
    }

    .logo span { color: var(--accent); }

    /* Active job-name chip beside the logo. Subtle — leads with a divider
       so the logo stays the primary brand mark; the name reads as a
       "you are here" indicator rather than a competing title.
       Inline-flex with a status icon + ellipsis-truncated label so a long
       PSD name doesn't wrap the header to two lines.
       Font stack: DM Mono is preferred for the techy aesthetic, but it
       lacks a U+2026 (…) glyph on some platforms — without a fallback
       the browser renders the ellipsis as ":". The system fallbacks all
       have a proper ellipsis glyph. */
    .job-name-display {
      display: inline-flex;
      align-items: center;
      gap: 6px;
      margin-left: 14px;
      padding-left: 14px;
      border-left: 1px solid var(--border, rgba(255,255,255,0.12));
      font-family: var(--mono), -apple-system, BlinkMacSystemFont, system-ui, 'Segoe UI', sans-serif;
      font-size: 12.5px;
      font-weight: 500;
      letter-spacing: 0.02em;
      line-height: 1;
      min-width: 0;
      /* No max-width: the chip now holds label + save-icon + RETINA
         badge + canvas-info, so the label needs room to breathe.
         Header's flex layout pushes the action buttons to the right;
         the chip grows to fit its content. */
    }
    /* Both states share the same save-disk icon to its right; only color
       changes. Unsaved → red disk + red label = "you haven't saved this
       yet" (loud, unmistakable). Saved → green disk + green label = "this
       is safely stored". The label and icon match by state so the chip
       reads as one unified status indicator. */
    .job-name-display .job-name-label {
      color: var(--danger);
      overflow: hidden;
      white-space: nowrap;
      text-overflow: ellipsis;
      min-width: 0;
    }
    /* Icon doubles as a save-shortcut button — clicking it routes to the
       same save/update flow as the sidebar's "Save to your library" /
       "Update saved job". Reset native button chrome so it visually
       matches the surrounding chip. */
    .job-name-display .job-name-icon {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      flex-shrink: 0;
      width: 14px;
      height: 14px;
      color: var(--danger);
      background: none;
      border: 0;
      padding: 0;
      margin: 0;
      cursor: pointer;
      border-radius: 3px;
      transition: filter 0.12s ease, background-color 0.12s ease;
    }
    .job-name-display .job-name-icon:hover {
      filter: brightness(1.25);
      background: rgba(255, 255, 255, 0.06);
    }
    .job-name-display .job-name-icon:focus-visible {
      outline: 2px solid currentColor;
      outline-offset: 2px;
    }
    .job-name-display .job-name-icon svg {
      width: 100%;
      height: 100%;
      display: block;
    }
    /* Saved state: green label + matching green disk icon. */
    .job-name-display.is-saved .job-name-label {
      color: var(--success);
    }
    .job-name-display.is-saved .job-name-icon {
      color: var(--success);
    }

    /* Legacy in-header retina + canvas-info spans — kept in the DOM
       (updateJobNameDisplay / buildUI still write to them) but never
       rendered. The visible affordance moved into the Project Settings
       panel's #project-info-strip chip group. !important wins against
       the inline style.display toggle in updateJobNameDisplay. */
    .job-name-display .retina-badge,
    .job-name-display .canvas-info-header {
      display: none !important;
    }

    /* Primary "PSD → Layer Exporter · Beta" chip on the right of the header.
       Yellow gradient pill — same treatment as the save button to read as
       a proud product-identity chip. */
    .badge {
      font-family: var(--mono);
      font-size: 11.5px;
      font-weight: 600;
      color: #2a1d00;
      background: linear-gradient(180deg, #ffd87a, #f5c842);
      border: 1px solid rgba(245, 200, 66, 0.6);
      padding: 6px 11px;
      border-radius: 8px;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      box-shadow: 0 4px 14px rgba(245, 200, 66, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.3);
      white-space: nowrap;
    }

    .header-actions {
      display: flex;
      align-items: center;
      gap: 4px;
      /* Take all available width to the right of the logo. The buttons
         inside grow proportionally (see .header-btn flex below) so the
         row fills the header without overflowing — chips spread cleanly
         instead of crowding the right edge or getting clipped. */
      flex: 1;
      min-width: 0;
      justify-content: flex-end;
    }

    /* Leading menus (File, Edit) — sit immediately after the logo,
       before the active job name. Styled lighter than the right-side
       action chips so the header reads as: identity (logo) → menu bar
       (File / Edit) → active job → centered toggle → actions. */
    .header-menu-leading {
      margin-left: 8px;
    }
    .header-menu-leading + .header-menu-leading {
      margin-left: 2px;
    }
    .header-btn-menu {
      background: transparent !important;
      border-color: transparent !important;
      box-shadow: none !important;
      letter-spacing: 0.06em;
      color: var(--text);
      opacity: 0.85;
    }
    .header-btn-menu:hover,
    .header-btn-menu[aria-expanded="true"] {
      background: rgba(255, 255, 255, 0.06) !important;
      border-color: rgba(255, 255, 255, 0.08) !important;
      opacity: 1;
    }
    .header-menu-leading .header-menu-panel {
      /* File / Edit dropdowns sit on the left edge of the header, so
         align their dropdown panels left-edge to the trigger. */
      left: 0;
      right: auto;
    }
    /* Right-aligned panel — used by the profile menu (last item in
       the right-side cluster); without this the panel would overflow
       off the right edge of the header. */
    .header-menu-panel-end {
      left: auto !important;
      right: 0;
    }

    /* Profile-menu identity strip — shows the signed-in email above
       the action items. Not interactive, just a "you are here" label. */
    .auth-menu-identity {
      padding: 8px 10px 10px;
      border-bottom: 1px solid var(--border, rgba(255, 255, 255, 0.08));
      margin-bottom: 4px;
    }
    .auth-menu-identity-label {
      font-family: var(--mono);
      font-size: 9px;
      letter-spacing: 0.12em;
      text-transform: uppercase;
      color: var(--muted);
      margin-bottom: 3px;
    }
    .auth-menu-identity-email {
      font-family: var(--sans);
      font-size: 12.5px;
      color: var(--text);
      font-weight: 600;
      letter-spacing: -0.005em;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      max-width: 220px;
    }

    .header-btn {
      display: inline-flex;
      align-items: center;
      gap: 5px;
      background: var(--btn-bg);
      color: var(--text);
      border: 1px solid rgba(255, 255, 255, 0.08);
      padding: 0 10px;
      /* Fixed height + line-height so every chip in the header row
         renders at the same vertical size regardless of icon/text mix.
         white-space: nowrap prevents the label from wrapping when the
         viewport gets tight. flex-shrink: 0 keeps each chip at its
         natural content width (text + icon + padding) instead of
         stretching to fill the row. */
      height: 32px;
      line-height: 1;
      white-space: nowrap;
      flex-shrink: 0;
      font-family: var(--mono);
      font-weight: 500;
      font-size: 11.5px;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      border-radius: 8px;
      cursor: pointer;
      transition: all 0.12s;
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
      backdrop-filter: blur(12px);
      -webkit-backdrop-filter: blur(12px);
    }
    .header-btn .hb-icon {
      flex-shrink: 0;
      opacity: 0.85;
    }
    .header-btn:hover .hb-icon { opacity: 1; }

    .header-btn:hover:not(:disabled) {
      background: rgba(255, 255, 255, 0.08);
      border-color: rgba(245, 200, 66, 0.4);
      color: var(--accent);
    }

    .header-btn:disabled {
      opacity: 0.35;
      cursor: not-allowed;
    }


    /* Header actions that no-op without a loaded project are gated via
       the native `disabled` attribute now (toggled by initManifest-
       GatedButtons in the inline script), not via a body-class CSS
       rule. The native :disabled state gives us cursor: not-allowed
       and click-blocking for free — same browser-handled behavior as
       undo/redo — and doesn't require pointer-events: none, which
       would otherwise nullify the cursor styling. The .header-btn:
       disabled rule above handles the visual. */

    /* Primary action variant — used for the top-right Export ad
       button so it reads as the main export call-to-action without
       being lost in the row of secondary undo/redo/copy/paste chips.
       Same dark chip body as the siblings; differentiated only by an
       accent (gold) stroke + accent text color so it doesn't shout. */
    .header-btn.header-btn-primary {
      /* Soft gold tint by default so the CTA reads as the
         "do this next" action even without hover. Stays subtle
         enough not to look out of place in the glass toolbar. */
      background: rgba(245, 200, 66, 0.06);
      border-color: rgba(245, 200, 66, 0.65);
      color: var(--accent);
    }
    .header-btn.header-btn-primary .hb-icon { opacity: 1; color: var(--accent); }
    .header-btn.header-btn-primary:hover:not(:disabled) {
      background: rgba(245, 200, 66, 0.14);
      border-color: rgba(245, 200, 66, 0.9);
      color: var(--accent);
    }

    /* ── Main layout ── floating panels with 5px gap */
    main {
      flex: 1;
      display: grid;
      grid-template-columns: 310px 1fr;
      gap: 5px;
      min-height: 0;
      /* Hold the editor at a usable minimum width. Below this, the page
         scrolls horizontally rather than collapsing the layout — keeps
         the canvas, transport, and timeline tracks intact and reachable
         instead of wrapping/overlapping. The 1180 floor matches the
         header-actions row's natural width (logo + 7 chips at 32px
         height) so the Export ad button is never clipped. */
      min-width: 1180px;
    }


    /* ── Left panel ── floating glass card */
    .panel-left {
      display: flex;
      flex-direction: column;
      gap: 0;
      overflow-y: auto;
      /* glass treatment from shared block above; border-right replaced
         by the all-around card border. */
    }

    .panel-section {
      padding: 14px 16px;
      border-bottom: 1px solid var(--border);
    }
    /* Once a banner is loaded, reorder the left panel so editing
       surfaces sit at the top and project-loading surfaces drop to
       the bottom (auto-collapsed via JS — see buildUI). Order vs
       default 0:
         Layers          → -2   (primary edit surface)
         Canvas Settings → -1   (per-project edit surface)
         Upload PSD      →  0   (used once per project)
         Your saved jobs →  0   (used once per project)
       Empty-state (no manifest) keeps the original DOM order so the
       user lands on Upload PSD as the entry point. */
    body.has-manifest .panel-left #layers-section          { order: -2; }
    body.has-manifest .panel-left #canvas-settings-section { order: -1; }

    .panel-label {
      font-family: var(--mono);
      font-weight: 600;
      font-size: 12px;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      color: var(--text);
      opacity: 0.85;
      margin-bottom: 14px;
    }

    /* Collapsible sections — click the label to collapse/expand */
    .panel-label-toggle {
      width: 100%;
      background: transparent;
      border: none;
      text-align: left;
      cursor: pointer;
      display: flex;
      align-items: center;
      gap: 8px;
      padding: 0;
      transition: color 0.12s;
    }

    .panel-label-toggle:hover { color: var(--text); }
    /* Multi-select count chip inside the Layers section header.
       Sits inline after the label so the header reads
       "▾ Layers · 3 selected". Gold accent so it pops without
       feeling like a destructive badge. */
    .layers-selection-count {
      margin-left: 8px;
      padding: 1px 7px;
      font-family: var(--mono);
      font-weight: 500;
      font-size: 9.5px;
      letter-spacing: 0.06em;
      text-transform: uppercase;
      color: var(--muted);
      background: rgba(255, 255, 255, 0.04);
      border: 1px solid rgba(255, 255, 255, 0.08);
      border-radius: 999px;
    }

    .collapse-icon {
      display: inline-block;
      font-size: 11px;
      line-height: 1;
      width: 10px;
      transition: transform 0.15s;
      color: var(--muted);
    }

    .panel-label-toggle:hover .collapse-icon { color: var(--accent); }

    .collapsible-section.collapsed .collapse-icon {
      transform: rotate(-90deg);
    }

    .collapsible-section.collapsed {
      flex: 0 0 auto !important;
      padding-bottom: 14px;
    }

    .collapsible-section.collapsed .panel-content { display: none; }
    .collapsible-section.collapsed .panel-label    { margin-bottom: 0; }

    /* ── Drop zone ── frosted dark with warm dashed border */
    #dropzone {
      border: 1.5px dashed rgba(245, 200, 66, 0.35);
      border-radius: 12px;
      padding: 40px 24px;
      text-align: center;
      cursor: pointer;
      transition: all 0.2s;
      position: relative;
      background: rgba(255, 255, 255, 0.025);
      backdrop-filter: blur(14px);
      -webkit-backdrop-filter: blur(14px);
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      gap: 18px;
    }

    /* Intro context — make the dropzone fill the full height of the
       New project tab card so the upload affordance is the dominant
       surface. The intro-col has min-height: 460px; we size the
       dropzone explicitly to match the available space inside the
       card (minus the col's 28px top + 24px bottom padding) so the
       dashed border reaches the bottom of the card. This is more
       reliable than chaining flex:1 through the panel-section +
       panel-content wrappers. */
    .intro-col > .panel-section[data-section-id="upload"] {
      flex: 1;
      display: flex;
      flex-direction: column;
      min-height: 0;
    }
    .intro-col > .panel-section[data-section-id="upload"] > .panel-content {
      flex: 1;
      display: flex;
      flex-direction: column;
      min-height: 0;
    }
    .intro-col #dropzone {
      flex: 1;
      min-height: 400px;
    }

    #dropzone:hover, #dropzone.drag-over {
      border-color: var(--accent);
      background: rgba(245, 200, 66, 0.08);
    }

    /* Telegraph "drop your file here" when a file is being dragged
       anywhere over the window — not just over the dropzone itself.
       Adds a soft pulse + scale + glow so the user's eye is pulled to
       the target before they need to look for it. */
    .intro-screen.drag-incoming #dropzone {
      border-color: var(--accent);
      background: rgba(245, 200, 66, 0.10);
      box-shadow: 0 0 0 0 rgba(245, 200, 66, 0.45);
      animation: dropzonePulse 1.4s ease-in-out infinite;
    }
    .intro-screen.drag-incoming .drop-icon {
      transform: scale(1.08);
      transition: transform 0.25s ease;
    }
    .intro-screen.drag-incoming #dropzone.drag-over {
      /* Drag actually entered the dropzone — stop the pulse and switch
         to the existing solid hover state so the user gets clear
         "release here" confirmation. */
      animation: none;
      box-shadow: 0 0 28px rgba(245, 200, 66, 0.35);
      transform: scale(1.01);
    }
    @keyframes dropzonePulse {
      0%, 100% {
        box-shadow: 0 0 0 0 rgba(245, 200, 66, 0.35);
        transform: scale(1);
      }
      50% {
        box-shadow: 0 0 0 14px rgba(245, 200, 66, 0);
        transform: scale(1.015);
      }
    }

    #dropzone input[type="file"] {
      position: absolute;
      inset: 0;
      opacity: 0;
      cursor: pointer;
      width: 100%;
      height: 100%;
    }

    .drop-icon {
      margin: 0;
      display: inline-flex;
      width: 96px;
      height: 96px;
      align-items: center;
      justify-content: center;
      background: linear-gradient(135deg, #ffe27a, #f5c842);
      color: #2a1d00;
      border-radius: 24px;
      box-shadow:
        0 16px 40px -10px rgba(245, 200, 66, 0.45),
        inset 0 2px 0 rgba(255, 255, 255, 0.4);
      transition: transform 0.2s ease, box-shadow 0.2s ease;
    }
    .drop-icon svg {
      width: 48px;
      height: 48px;
      display: block;
    }
    #dropzone:hover .drop-icon,
    #dropzone.drag-over .drop-icon {
      transform: translateY(-2px) scale(1.03);
      box-shadow:
        0 20px 48px -10px rgba(245, 200, 66, 0.6),
        inset 0 2px 0 rgba(255, 255, 255, 0.5);
    }

    .drop-title {
      font-family: var(--sans);
      font-weight: 600;
      font-size: 18px;
      letter-spacing: -0.005em;
      color: var(--text);
      margin: 0;
    }

    .drop-sub {
      font-family: var(--mono);
      font-size: 10.5px;
      letter-spacing: 0.12em;
      text-transform: uppercase;
      color: var(--muted);
      margin: -8px 0 0;
    }

    /* Pair of format chips inside the dropzone — makes the dual-source
       acceptance (.psd from Photoshop, .zip from Figma) explicit without
       cramming both extensions into the title line. */
    .drop-formats {
      display: flex;
      justify-content: center;
      gap: 8px;
      margin: 4px 0 0;
    }
    .drop-format-chip {
      display: inline-flex;
      align-items: center;
      gap: 7px;
      padding: 5px 11px;
      border-radius: 999px;
      border: 1px solid rgba(245, 200, 66, 0.22);
      background: rgba(245, 200, 66, 0.05);
      font-family: var(--mono);
      font-size: 10px;
      letter-spacing: 0.08em;
      color: var(--muted);
      transition: all 0.15s ease;
    }
    .drop-format-ext {
      color: var(--accent);
      font-weight: 700;
      letter-spacing: 0.1em;
      text-transform: uppercase;
    }
    .drop-format-src {
      color: var(--muted);
      text-transform: uppercase;
      font-size: 9px;
      letter-spacing: 0.14em;
    }
    #dropzone:hover .drop-format-chip,
    #dropzone.drag-over .drop-format-chip {
      border-color: rgba(245, 200, 66, 0.45);
      background: rgba(245, 200, 66, 0.10);
    }

    /* ── Progress ── */
    #progress-wrap {
      display: none;
    }

    .progress-label {
      font-family: var(--mono);
      font-size: 11px;
      color: var(--muted);
      margin-bottom: 10px;
    }

    .progress-bar-track {
      background: rgba(255, 255, 255, 0.08);
      height: 3px;
      border-radius: 999px;
      overflow: hidden;
    }

    .progress-bar-fill {
      height: 100%;
      background: var(--accent);
      width: 0%;
      transition: width 0.3s;
      animation: indeterminate 1.2s ease-in-out infinite;
    }

    @keyframes indeterminate {
      0%   { transform: translateX(-100%); width: 60%; }
      100% { transform: translateX(200%);  width: 60%; }
    }

    /* ── Layer list ──
       Caps at 800px and scrolls internally past that — keeps the
       sidebar from getting unwieldy on banners with many layers
       without forcing the rest of the sidebar (Canvas Settings,
       collapsibles, etc.) to scroll alongside. */
    #layer-list {
      display: flex;
      flex-direction: column;
      gap: 6px;
      max-height: 800px;
      overflow-y: auto;
      overflow-x: hidden;
      /* position: relative so the .layer-drop-indicator (set during
         drag-to-reorder) anchors to this container's top edge. */
      position: relative;
      /* Slight inset so the scrollbar doesn't kiss the right border of
         each .layer-item card. */
      padding-right: 4px;
    }

    .layer-item {
      display: flex;
      flex-direction: row;
      align-items: center;
      gap: 8px;
      padding: 4px 8px;
      background: rgba(255, 255, 255, 0.025);
      border: 1px solid var(--border);
      border-radius: 7px;
      cursor: pointer;
      transition: all 0.15s;
      /* Suppress browser text selection — shift-click in this list is for
         multi-selecting layers, not for text highlighting. */
      user-select: none;
      -webkit-user-select: none;
      min-width: 0;     /* lets descendant ellipsis-truncation actually fire */
    }

    .layer-item:hover  { border-color: rgba(245, 200, 66, 0.35); background: rgba(245, 200, 66, 0.04); }
    .layer-item.active { border-color: var(--accent); background: rgba(245, 200, 66, 0.10); }

    /* Mask layers — invisible in preview, but selectable in this list to
       move/scale/rotate the clipping window. Green accent to distinguish
       from regular content layers. The thumbnail uses mask-image to
       stencil the mask's own shape: the div is filled with a green
       gradient, then the SVG/PNG silhouette is used as a CSS mask so
       only the shape itself shows colored — the rest is transparent.
       Result: the panel shows the mask's actual silhouette in the
       accent color, instantly identifying it as a stencil instead of
       a generic white blob. */
    .layer-thumb-mask-fill {
      background: linear-gradient(135deg, rgba(94, 198, 132, 0.95), rgba(94, 198, 132, 0.65));
      /* mask-image is set inline on the element (JS) so we don't go
         through a CSS variable indirection that some browsers don't
         resolve cleanly. The properties below are the parameters that
         apply to whatever mask-image the JS sets. */
      -webkit-mask-size: contain;
              mask-size: contain;
      -webkit-mask-position: center;
              mask-position: center;
      -webkit-mask-repeat: no-repeat;
              mask-repeat: no-repeat;
      /* Subtle outer glow so the thumb's bounding box is still
         perceptible even before the silhouette renders / when the
         mask shape is very small relative to the thumb area. */
      filter: drop-shadow(0 0 1px rgba(94, 198, 132, 0.5));
    }
    .layer-item-mask:hover  { border-color: rgba(94, 198, 132, 0.45); background: rgba(94, 198, 132, 0.05); }
    .layer-item-mask.active { border-color: rgb(94, 198, 132); background: rgba(94, 198, 132, 0.10); }
    /* Sized to match the .layer-badge family (anim / exit / rollover /
       video) so they all read as the same kind of chip — same font
       size, padding, radius, line-height. Only the green color theme
       distinguishes MASK from the others. */
    .layer-mask-badge {
      font-family: var(--mono);
      font-weight: 600;
      font-size: 9.5px;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      padding: 2px 6px;
      border-radius: 6px;
      line-height: 1.4;
      background: #2a2a2a;
      color: rgb(150, 235, 185);
      border: 1px solid rgba(94, 198, 132, 0.4);
      align-self: flex-start;
    }

    /* Group selection disables the Animation + Rollover tabs (Static
       only — see activateTab guard at index.html:16426). Dim them and
       set the not-allowed cursor; activateTab itself rejects the click
       and surfaces a toast. The Static tab stays fully interactive. */
    /* Groups are Static-only — block both visual interaction (cursor +
       dim) AND actual click delivery on the Animation / Rollover tabs.
       Previously only opacity + cursor were styled, so the JS click
       listener still fired activateTab() which then went through its
       redirect-to-static path with side effects (overlay marker reset,
       hover preview kill, _liveHover zeroing, renderTrackEditor, etc.).
       Those side effects shifted the right-panel variables for the
       primary group member, which read as "variables changed on a
       click that should have been a no-op." pointer-events: none kills
       the click delivery so the tab is truly inert. */
    body.group-selected .vars-tab[data-tab="anim"],
    body.group-selected .vars-tab[data-tab="rollover"] {
      opacity: 0.32;
      cursor: not-allowed;
      pointer-events: none;
    }

    /* Group track row in the timeline — visual parity with
       .layer-item-group in the layers panel: neutral by default,
       violet tint + inset border only on hover / active. box-shadow
       inset (instead of a real border) keeps the row's left edge
       flush with the ruler ticks above. */
    .track-row.track-row-group {
      position: relative;
      background: transparent;
      border-radius: 7px;
      transition: background 0.12s, box-shadow 0.12s;
    }
    /* The hover/active highlight is rendered via this overlay so the
       rounded outline sits 4px inside the row's left+right edges and
       doesn't visually bleed against the timeline scroll boundaries.
       z-index: 5 lifts the overlay ABOVE the sticky track-name (z:4)
       and the track-track — without it, the left+right vertical edges
       of the outline were getting covered by those children's
       backgrounds, so only the top/bottom horizontal lines were visible.
       --grp-hl-bg / --grp-hl-shadow are set per-variant below. */
    .track-row.track-row-group::before {
      content: '';
      position: absolute;
      top: 0; bottom: 0;
      left: 4px; right: 0;
      border-radius: 7px;
      pointer-events: none;
      z-index: 5;
      background: var(--grp-hl-bg, transparent);
      box-shadow: var(--grp-hl-shadow, none);
      transition: background 0.12s, box-shadow 0.12s;
    }
    /* The ::before above is constrained to the row's CSS box width
       (= visible viewport at zoom > 1), so it can't stretch to the
       full scrolled timeline. The highlight now lives entirely on the
       .track-track-group rule below (which has explicit width =
       getInnerTimelineWidth()), for both :hover and .active states.
       Suppress the ::before unconditionally so we don't double-render. */
    .track-row.track-row-group::before {
      background: transparent !important;
      box-shadow: none !important;
    }
    .track-row.track-row-group:hover {
      --grp-hl-bg: rgba(var(--color-group-rgb), 0.05);
      --grp-hl-shadow: inset 0 0 0 1px rgba(var(--color-group-rgb), 0.35);
    }
    .track-row.track-row-group:hover .track-name {
      background: transparent;
    }
    .track-row.track-row-group .track-name {
      cursor: pointer;
    }
    .track-row.track-row-group.active {
      --grp-hl-bg: rgba(var(--color-group-rgb), 0.10);
      --grp-hl-shadow: inset 0 0 0 1px var(--color-group);
    }
    .track-row.track-row-group.active .track-name {
      background: transparent;
    }
    .track-group-badge {
      font-family: var(--mono);
      font-weight: 600;
      font-size: 9.5px;
      letter-spacing: 0.06em;
      text-transform: none;
      padding: 1px 6px;
      border-radius: 4px;
      line-height: 1.4;
      background: rgba(var(--color-group-rgb), 0.10);
      color: rgb(200, 180, 255);
      border: 1px solid rgba(var(--color-group-rgb), 0.4);
      /* Tight to the dot on the left, but allowed to shrink with
         ellipsis when the group name is long — beats the old
         flex-shrink:0 behavior which forced the pill to its full
         width and ellipsised the rest of the row instead. */
      margin-left: 2px;
      min-width: 0;
      max-width: 100%;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    /* Spacer to the right of the group-name pill — fills the remainder
       of the name column so the pill stays anchored next to the dot
       rather than centering or floating right. Mirrors the way regular
       layer rows reserve the right edge for solo + kf toggles. */
    .track-name-group-trailing {
      flex: 1;
      min-width: 0;
    }

    /* Group summary bar — one consolidated bar on the master-timeline
       group row showing the aggregate of every member's entry + exit
       span. Dragging this bar shifts every member's animation in
       lockstep, so a designer can retime an entire group from a
       single handle without entering drill-in mode. */
    .track-bar.track-bar-group-summary {
      background: linear-gradient(180deg, rgba(var(--color-group-rgb), 0.45), rgba(140, 110, 220, 0.55));
      border: 1px solid rgba(190, 160, 250, 0.6);
      color: rgb(240, 230, 255);
      font-family: var(--mono);
      font-size: 10px;
      letter-spacing: 0.04em;
      cursor: grab;
    }
    .track-bar.track-bar-group-summary:hover {
      background: linear-gradient(180deg, rgba(170, 140, 245, 0.55), rgba(150, 120, 230, 0.65));
      border-color: rgba(200, 170, 255, 0.75);
    }
    .track-bar.track-bar-group-summary.selected {
      box-shadow: 0 0 0 1px rgba(200, 170, 255, 0.9), 0 0 8px rgba(var(--color-group-rgb), 0.35);
    }
    .track-bar.track-bar-group-summary.dragging {
      cursor: grabbing;
    }

    /* Group layers — single panel row representing N member layers.
       Single click selects all members (multi-select via the
       existing selectedIds set); double click drills into the
       group's sub-timeline editor. Violet accent distinguishes
       groups from regular layers (yellow) and masks (green). */
    .layer-thumb-group {
      display: flex;
      align-items: center;
      justify-content: center;
      background: linear-gradient(135deg, rgba(var(--color-group-rgb), 0.18), rgba(var(--color-group-rgb), 0.06));
      color: rgb(190, 165, 250);
      border: 1px solid rgba(var(--color-group-rgb), 0.35);
    }
    .layer-thumb-group svg {
      width: 18px;
      height: 18px;
    }
    .layer-item-group:hover  { border-color: rgba(var(--color-group-rgb), 0.45); background: rgba(var(--color-group-rgb), 0.05); }
    .layer-item-group.active { border-color: var(--color-group); background: rgba(var(--color-group-rgb), 0.10); }
    /* Same chip shape as MASK badge — only the violet theme changes. */
    .layer-group-badge {
      font-family: var(--mono);
      font-weight: 600;
      font-size: 9.5px;
      letter-spacing: 0.05em;
      text-transform: uppercase;
      padding: 2px 5px;
      border-radius: 6px;
      line-height: 1.4;
      background: #2a2a2a;
      color: rgb(200, 180, 255);
      border: 1px solid rgba(var(--color-group-rgb), 0.4);
      align-self: flex-start;
    }

    /* ── Masked-group variants ─────────────────────────────────────
       When a group contains a mask layer, the whole row + summary
       bar swap to the mask-green theme so the group reads as part
       of the mask system instead of as a generic violet container.
       Applied as parallel `-mask` class variants on top of the
       regular .layer-item-group / .track-row-group / etc. classes
       so the layout still inherits from the violet base. */
    .layer-thumb-group.layer-thumb-group-mask {
      background: linear-gradient(135deg, rgba(94, 198, 132, 0.18), rgba(94, 198, 132, 0.06));
      color: var(--color-mask);
      border-color: var(--color-mask-border);
    }
    .layer-item-group.layer-item-group-mask:hover  {
      border-color: rgba(94, 198, 132, 0.45);
      background: rgba(94, 198, 132, 0.05);
    }
    .layer-item-group.layer-item-group-mask.active {
      border-color: var(--color-mask);
      background: rgba(94, 198, 132, 0.10);
    }
    .layer-group-badge.layer-group-badge-mask {
      background: var(--color-mask-soft);
      color: rgb(170, 240, 200);
      border-color: var(--color-mask-border);
    }
    /* Mask group track row — same hover / active treatment as the
       layer panel's mask-group entry. Neutral by default, green tint
       only on hover / active. */
    .track-row.track-row-group.track-row-group-mask {
      background: transparent;
    }
    .track-row.track-row-group.track-row-group-mask:hover {
      --grp-hl-bg: rgba(94, 198, 132, 0.05);
      --grp-hl-shadow: inset 0 0 0 1px rgba(94, 198, 132, 0.35);
    }
    .track-row.track-row-group.track-row-group-mask .track-name {
      color: rgb(170, 240, 200);
    }
    .track-row.track-row-group.track-row-group-mask.active {
      --grp-hl-bg: rgba(94, 198, 132, 0.10);
      --grp-hl-shadow: inset 0 0 0 1px var(--color-mask);
    }
    .track-row.track-row-group.track-row-group-mask.active .track-name {
      color: rgb(190, 250, 215);
    }
    .track-group-badge.track-group-badge-mask {
      background: var(--color-mask-soft);
      color: rgb(170, 240, 200);
      border-color: var(--color-mask-border);
    }
    .track-group-badge.track-group-badge-video {
      background: rgba(255, 123, 227, 0.10);
      color: rgb(255, 195, 240);
      border-color: rgba(255, 123, 227, 0.5);
    }
    .track-bar.track-bar-group-summary.track-bar-group-summary-mask {
      background: linear-gradient(180deg, rgba(94, 198, 132, 0.45), rgba(70, 175, 110, 0.55));
      border-color: rgba(140, 220, 170, 0.6);
      color: rgb(230, 255, 240);
    }
    .track-bar.track-bar-group-summary.track-bar-group-summary-mask:hover {
      background: linear-gradient(180deg, rgba(110, 210, 145, 0.55), rgba(85, 190, 125, 0.65));
      border-color: rgba(160, 235, 185, 0.75);
    }
    .track-bar.track-bar-group-summary.track-bar-group-summary-mask.selected {
      box-shadow: 0 0 0 1px rgba(160, 235, 185, 0.9), 0 0 8px rgba(94, 198, 132, 0.35);
    }

    /* ── Singleton group variants ──────────────────────────────────
       A group with only one member reads more like a layer than a
       container — the violet group palette overstates it. These
       overrides swap to the regular accent (yellow) palette in both
       the layers panel and the timeline row + summary bar so single-
       member groups visually match the per-layer chrome the canvas
       already shows for them. Mask groups keep their green theme;
       these only apply when the group is not also a mask. */
    .layer-thumb-group.layer-thumb-group-singleton {
      background: linear-gradient(135deg, rgba(245, 200, 66, 0.18), rgba(245, 200, 66, 0.06));
      color: var(--accent);
      border-color: rgba(245, 200, 66, 0.35);
    }
    .layer-item-group.layer-item-group-singleton:hover {
      border-color: rgba(245, 200, 66, 0.35);
      background: rgba(245, 200, 66, 0.04);
    }
    .layer-item-group.layer-item-group-singleton.active {
      border-color: var(--accent);
      background: rgba(245, 200, 66, 0.10);
    }
    .layer-group-badge.layer-group-badge-singleton {
      background: rgba(245, 200, 66, 0.10);
      color: rgb(245, 220, 150);
      border-color: rgba(245, 200, 66, 0.4);
    }
    /* ── Video group variants ──────────────────────────────────────
       A group flagged as a video container (group.isVideo = true)
       reads in hot-pink so the user can spot at a glance which group
       on the timeline / layers panel will export as an HTML5 video
       element. Pink wins over the mask-green and singleton-yellow
       variants when set. */
    .layer-thumb-group.layer-thumb-group-video {
      background: linear-gradient(135deg, rgba(255, 123, 227, 0.20), rgba(255, 123, 227, 0.06));
      color: #ff7be3;
      border-color: rgba(255, 123, 227, 0.45);
    }
    .layer-item-group.layer-item-group-video:hover {
      border-color: rgba(255, 123, 227, 0.45);
      background: rgba(255, 123, 227, 0.05);
    }
    .layer-item-group.layer-item-group-video.active {
      border-color: #ff7be3;
      background: rgba(255, 123, 227, 0.10);
    }
    .layer-group-badge.layer-group-badge-video {
      background: #2a2a2a;
      color: rgb(255, 195, 240);
      border-color: rgba(255, 123, 227, 0.5);
    }
    .track-row.track-row-group.track-row-group-video {
      background: transparent;
    }
    .track-row.track-row-group.track-row-group-video:hover {
      --grp-hl-bg: rgba(255, 123, 227, 0.05);
      --grp-hl-shadow: inset 0 0 0 1px rgba(255, 123, 227, 0.35);
    }
    .track-row.track-row-group.track-row-group-video .track-name {
      color: rgb(255, 195, 240);
    }
    .track-row.track-row-group.track-row-group-video.active {
      --grp-hl-bg: rgba(255, 123, 227, 0.10);
      --grp-hl-shadow: inset 0 0 0 1px #ff7be3;
    }
    .track-row.track-row-group.track-row-group-video.active .track-name {
      color: rgb(255, 215, 247);
    }
    .track-bar.track-bar-group-summary.track-bar-group-summary-video {
      background: linear-gradient(180deg, rgba(255, 123, 227, 0.50), rgba(230, 90, 200, 0.60));
      border-color: rgba(255, 165, 235, 0.65);
      color: rgb(255, 235, 250);
    }
    .track-bar.track-bar-group-summary.track-bar-group-summary-video:hover {
      background: linear-gradient(180deg, rgba(255, 145, 235, 0.60), rgba(245, 110, 215, 0.70));
      border-color: rgba(255, 185, 240, 0.8);
    }
    .track-bar.track-bar-group-summary.track-bar-group-summary-video.selected {
      box-shadow: 0 0 0 1px rgba(255, 185, 240, 0.9), 0 0 8px rgba(255, 123, 227, 0.35);
    }

    /* Singleton group track row — yellow accent matches the layer
       panel's singleton entry. Neutral by default, accent tint on
       hover / active. */
    .track-row.track-row-group.track-row-group-singleton {
      background: transparent;
    }
    .track-row.track-row-group.track-row-group-singleton:hover {
      --grp-hl-bg: rgba(245, 200, 66, 0.04);
      --grp-hl-shadow: inset 0 0 0 1px rgba(245, 200, 66, 0.35);
    }
    .track-row.track-row-group.track-row-group-singleton .track-name {
      color: rgb(245, 220, 150);
    }
    .track-row.track-row-group.track-row-group-singleton.active {
      --grp-hl-bg: rgba(245, 200, 66, 0.10);
      --grp-hl-shadow: inset 0 0 0 1px var(--accent);
    }
    .track-row.track-row-group.track-row-group-singleton.active .track-name {
      color: rgb(255, 235, 175);
    }
    .track-group-badge.track-group-badge-singleton {
      background: rgba(245, 200, 66, 0.10);
      color: rgb(245, 220, 150);
      border-color: rgba(245, 200, 66, 0.4);
    }
    .track-bar.track-bar-group-summary.track-bar-group-summary-singleton {
      background: linear-gradient(180deg, rgba(245, 200, 66, 0.45), rgba(215, 175, 55, 0.55));
      border-color: rgba(245, 215, 130, 0.6);
      color: rgb(50, 40, 10);
    }
    .track-bar.track-bar-group-summary.track-bar-group-summary-singleton:hover {
      background: linear-gradient(180deg, rgba(255, 215, 95, 0.55), rgba(225, 185, 70, 0.65));
      border-color: rgba(255, 225, 155, 0.75);
    }
    .track-bar.track-bar-group-summary.track-bar-group-summary-singleton.selected {
      box-shadow: 0 0 0 1px rgba(255, 225, 155, 0.9), 0 0 8px rgba(245, 200, 66, 0.35);
    }

    /* Phase 3 — group-edit drill-in.
       The "← Back to all layers" header row at the top of the panel
       while drilled in. Stays violet to keep the visual association
       with the group it represents, but pops harder than a normal
       group row so the user reads it as "you're in a sub-mode". */
    .layer-item-group-back {
      background: linear-gradient(135deg, rgba(var(--color-group-rgb), 0.12), rgba(var(--color-group-rgb), 0.04));
      border-color: rgba(var(--color-group-rgb), 0.45);
      cursor: pointer;
    }
    .layer-item-group-back:hover {
      background: linear-gradient(135deg, rgba(var(--color-group-rgb), 0.20), rgba(var(--color-group-rgb), 0.08));
      border-color: rgba(var(--color-group-rgb), 0.65);
    }
    .layer-thumb-group-back {
      display: flex;
      align-items: center;
      justify-content: center;
      background: rgba(var(--color-group-rgb), 0.15);
      color: rgb(200, 180, 255);
      border: 1px solid rgba(var(--color-group-rgb), 0.45);
    }
    .layer-thumb-group-back svg { width: 18px; height: 18px; }
    /* Variant overrides: when drilled into a mask group OR a singleton
       group, the back header takes that group's color so the panel
       reads as one cohesive context (instead of "always violet"). */
    .layer-item-group-back.layer-item-group-back-mask {
      background: linear-gradient(135deg, rgba(94, 198, 132, 0.14), rgba(94, 198, 132, 0.04));
      border-color: rgba(94, 198, 132, 0.45);
    }
    .layer-item-group-back.layer-item-group-back-mask:hover {
      background: linear-gradient(135deg, rgba(94, 198, 132, 0.22), rgba(94, 198, 132, 0.08));
      border-color: rgba(94, 198, 132, 0.7);
    }
    .layer-thumb-group-back.layer-thumb-group-back-mask {
      background: rgba(94, 198, 132, 0.16);
      color: rgb(170, 240, 200);
      border-color: rgba(94, 198, 132, 0.45);
    }
    .layer-item-group-back.layer-item-group-back-singleton {
      background: linear-gradient(135deg, rgba(245, 200, 66, 0.14), rgba(245, 200, 66, 0.04));
      border-color: rgba(245, 200, 66, 0.45);
    }
    .layer-item-group-back.layer-item-group-back-singleton:hover {
      background: linear-gradient(135deg, rgba(245, 200, 66, 0.22), rgba(245, 200, 66, 0.08));
      border-color: rgba(245, 200, 66, 0.7);
    }
    .layer-thumb-group-back.layer-thumb-group-back-singleton {
      background: rgba(245, 200, 66, 0.16);
      color: rgb(245, 220, 150);
      border-color: rgba(245, 200, 66, 0.45);
    }

    /* While drilled in, the canvas wraps the focused group's bounds in
       a subtle violet glow so the user can spot at a glance where
       their "scope" sits on the artboard. The glow comes from a
       drop-shadow filter rather than a stroked rectangle so it tracks
       group rotation / scale via composedLive without any extra wiring. */
    body.group-edit-active #render-canvas {
      box-shadow: 0 0 0 1px rgba(var(--color-group-rgb), 0.18), 0 0 24px rgba(var(--color-group-rgb), 0.08);
    }
    /* Group's track rows already have their "track-row-group" violet
       styling. Inside drill-in we lose those rows (group is gone from
       the unified list); the children render as plain track rows.
       Tint the timeline panel chrome so the user still reads the
       overall mode as group-edit. */
    body.group-edit-active #track-editor {
      box-shadow: inset 3px 0 0 rgba(var(--color-group-rgb), 0.55);
    }

    .layer-thumb {
      width: 26px;
      height: 26px;
      object-fit: contain;
      border-radius: 5px;
      background: repeating-conic-gradient(rgba(255,255,255,0.04) 0% 25%, rgba(255,255,255,0.08) 0% 50%) 0 0 / 6px 6px;
      flex-shrink: 0;
    }

    .layer-meta {
      flex: 1 1 auto;
      min-width: 0;
      display: flex;
      flex-direction: column;
      gap: 2px;
    }

    .layer-name {
      font-family: var(--mono);
      font-weight: 500;
      font-size: 11px;
      color: var(--text);
      letter-spacing: 0.08em;
      display: flex;
      align-items: center;
      gap: 5px;
      min-width: 0;
    }
    /* Long layer names ellipsis-truncate so the row never widens past
       the panel. The dot + mask badge are flex siblings that don't
       shrink, so they stay visible. */
    .layer-name-text {
      flex: 1 1 auto;
      min-width: 0;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    .layer-color-dot {
      display: inline-block;
      width: 8px;
      height: 8px;
      border-radius: 50%;
      flex-shrink: 0;
    }

    .layer-badges {
      display: flex;
      gap: 3px;
      /* Keep all badges on one line — even for mask groups that show
         GROUP·N + MASK + ANIM + EXIT / etc. simultaneously. The badges
         themselves are small (9.5px font, ~50–60px each) so 3–4 of
         them fit inside the meta column width. min-width: 0 lets the
         row shrink past its intrinsic width if needed; the badges
         flex-shrink: 0 keeps individual chips at their natural size,
         and overflow-hidden clips gracefully if the row truly can't
         accommodate them. */
      flex-wrap: nowrap;
      min-width: 0;
      overflow: hidden;
    }
    .layer-badges > * {
      flex-shrink: 0;
    }
    .layer-badges:empty { display: none; }
    /* Drag-to-reorder visuals — the row being dragged fades + lifts
       slightly, and a thin gold line drops in to mark where the
       layer will land on release. The line is absolutely positioned
       inside the layer-list container (renderLayerList sets the
       container to position: relative implicitly via flex). */
    .layer-item.layer-item-dragging {
      opacity: 0.45;
      cursor: grabbing;
    }
    .layer-item:not(.layer-item-dragging):not(.locked) {
      cursor: grab;
    }
    .layer-drop-indicator {
      position: absolute;
      left: 0;
      right: 0;
      height: 2px;
      background: var(--color-entry);
      box-shadow: 0 0 8px var(--color-entry-soft);
      pointer-events: none;
      z-index: 10;
      border-radius: 2px;
      transform: translateY(-1px);
    }

    .layer-badge {
      font-family: var(--mono);
      font-weight: 600;
      font-size: 9.5px;
      letter-spacing: 0.05em;
      text-transform: uppercase;
      padding: 2px 5px;
      border-radius: 6px;
      border: 1px solid;
      line-height: 1.4;
    }
    /* Pull the leading phase icon (▸ ▸ ◂ ⟳) flush against the text
       inside ANIM / EXIT / HOVER badges so the chip width stays tight
       on rows that carry the full GROUP·N + MASK + ANIM + EXIT stack.
       The space character we used to set in textContent reads as
       ~3px on a 9.5px mono font; removing it visually keeps the icon
       clearly distinguishable while saving the row width to fit. */
    .layer-badge.anim::first-letter,
    .layer-badge.exit::first-letter,
    .layer-badge.rollover::first-letter,
    .layer-badge.mask-anim::first-letter {
      margin-right: 1px;
    }

    /* Solid dark backgrounds across the whole layer-badge family so
       the labels read clearly over any underlying panel tint or layer
       row state. The phase tint stays in the colored text + border —
       same convention as the canvas overlay pills. The earlier ~8-14%
       alpha fill washed out against the row's hover/active backgrounds
       and made the chips hard to distinguish at a glance. */
    .layer-badge.anim     { color: var(--color-entry); border-color: var(--color-entry-border); background: #2a2a2a; }
    .layer-badge.exit     { color: var(--color-exit);  border-color: var(--color-exit-border);  background: #2a2a2a; }
    /* NOTE: rollover badge uses a slightly different blue than the
       hover kf-diamond cyan (#5cb8ff vs #5fb8c8). Inconsistency
       predates the var system; leaving as-is to avoid a surprise
       visual change. Unify by routing through --color-hover when
       the team decides which hue is canonical. */
    .layer-badge.rollover { color: #5cb8ff;       border-color: rgba(62,193,255,0.4);   background: #2a2a2a; }
    .layer-badge.video    { color: #ff7be3;       border-color: rgba(255,123,227,0.4);  background: #2a2a2a; }
    /* Masked-by chip — points from a clipped content layer back to
       its mask. Green family (matches MASK / mask-anim) so the
       relationship reads as part of the mask system. Built as a
       <button> so it's clickable + keyboard-accessible — reset the
       browser's default button chrome so it visually matches the
       span-based sibling badges. */
    .layer-badge.masked-by {
      appearance: none;
      -webkit-appearance: none;
      color: rgb(150, 235, 185);
      border-color: var(--color-mask-border);
      background: #2a2a2a;
      cursor: pointer;
      font-family: var(--mono);
      font-weight: 600;
      font-size: 9.5px;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      padding: 2px 6px;
      border-radius: 6px;
      border-width: 1px;
      border-style: solid;
      line-height: 1.4;
      max-width: 100%;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      transition: background 0.15s, border-color 0.15s;
    }
    .layer-badge.masked-by:hover {
      background: #333;
      border-color: rgba(94, 198, 132, 0.6);
    }
    .layer-badge.masked-by:focus-visible {
      outline: none;
      box-shadow: 0 0 0 2px rgba(94, 198, 132, 0.4);
    }
    /* Mask path animation — green to match the MASK badge family and
       distinguish from the layer-property ANIM (gold) so a mask row
       can carry both readings without conflict. */
    .layer-badge.mask-anim { color: rgb(170, 240, 200); border-color: rgba(94, 198, 132, 0.45); background: #2a2a2a; }

    /* ── Video layer config in Static panel ── */
    .video-section {
      padding: 12px 14px;
      margin-top: 12px;
      margin-bottom: 14px;
      border: 1px solid rgba(255,123,227,0.25);
      background: rgba(255,123,227,0.05);
      border-radius: 8px;
      backdrop-filter: blur(8px);
      -webkit-backdrop-filter: blur(8px);
    }

    .video-section-title {
      font-family: var(--mono);
      font-weight: 600;
      font-size: 12px;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      color: #ff7be3;
      margin-bottom: 10px;
    }

    /* Make the video URL field visually distinct from a normal var-input
       so it reads as "the thing you fill in" when a video layer is selected. */
    .video-src-input {
      border-color: rgba(255, 123, 227, 0.35) !important;
      background: rgba(255, 123, 227, 0.04);
      font-size: 0.78rem;
    }
    .video-src-input:focus {
      border-color: #ff7be3 !important;
      box-shadow: 0 0 0 2px rgba(255, 123, 227, 0.18);
    }

    .video-options-row {
      display: flex;
      flex-wrap: wrap;
      gap: 12px;
      margin-top: 8px;
    }

    .video-option-label {
      display: flex;
      align-items: center;
      gap: 6px;
      font-family: var(--mono);
      font-weight: 500;
      font-size: 11px;
      color: var(--muted);
      letter-spacing: 0.1em;
      cursor: pointer;
      text-transform: uppercase;
    }

    .video-option-label > input[type="checkbox"] {
      appearance: none;
      -webkit-appearance: none;
      flex-shrink: 0;
      width: 30px;
      height: 18px;
      margin: 0;
      background: var(--border-strong);
      border: 1px solid var(--border-strong);
      border-radius: 999px;
      position: relative;
      cursor: pointer;
      transition: background 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
      vertical-align: middle;
    }
    .video-option-label > input[type="checkbox"]::before {
      content: '';
      position: absolute;
      width: 12px;
      height: 12px;
      left: 2px;
      top: 2px;
      background: #fff;
      border-radius: 50%;
      box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 1px 1px rgba(0, 0, 0, 0.15);
      transition: transform 0.18s ease;
    }
    .video-option-label > input[type="checkbox"]:checked {
      background: linear-gradient(180deg, #ffb0ec, #ff7be3);
      border-color: rgba(255, 123, 227, 0.65);
      box-shadow: 0 0 12px rgba(255, 123, 227, 0.35);
    }
    .video-option-label > input[type="checkbox"]:checked::before {
      transform: translateX(12px);
    }
    .video-option-label > input[type="checkbox"]:focus-visible {
      outline: none;
      box-shadow: 0 0 0 3px rgba(255, 123, 227, 0.35);
    }

    /* Inline canvas text editor — contenteditable overlay positioned
       over the text layer's bbox while the user double-clicks to edit
       in place. Rasterized PNG hides during edit (renderAll skip
       gate); JS sets font / size / leading / tracking / color / align
       / transform / decoration inline on each style pass so the
       overlay reads the same as the rendered text. transform-origin
       and transform are set inline to match the layer's rotation /
       scale. The faint outline+caret tint signals "you're typing"
       without obscuring the text itself. */
    #inline-text-editor {
      position: fixed;
      box-sizing: border-box;
      padding: 0;
      margin: 0;
      border: 0;
      outline: 1px dashed rgba(0, 162, 255, 0.7);
      outline-offset: 1px;
      background: transparent;
      caret-color: currentColor;
      white-space: pre;
      overflow: visible;
      z-index: 9000;
      cursor: text;
      /* No content wrap by default — bbox grows with content (Point
         text). Long lines extend right; Enter creates a new line. */
      word-wrap: normal;
      overflow-wrap: normal;
      user-select: text;
      -webkit-user-select: text;
    }
    #inline-text-editor[hidden] { display: none; }
    #inline-text-editor::selection {
      background: rgba(0, 162, 255, 0.35);
    }

    /* Text-layer editor — sits inside the Static tab, surfaces every
       per-text property the rasterizer reads (content, font, size,
       weight, color, tracking, leading, alignment, transform). Real-
       time rasterize as the user edits each field; the content
       textarea debounces by ~150ms so a paragraph's worth of typing
       doesn't fire dozens of rasterize tasks. Two-tier layout follows
       Photoshop's Character / Paragraph panel split for visual
       familiarity, condensed to fit the editor's right pane width. */
    .text-section .text-edit-field { margin-bottom: 8px; }
    .text-section .text-edit-content {
      width: 100%;
      box-sizing: border-box;
      min-height: 44px;
      max-height: 120px;
      padding: 6px 8px;
      background: var(--surface-low, #1a1a1a);
      color: var(--text, #e8e8e8);
      border: 1px solid var(--border-strong, #3a3a3a);
      border-radius: 4px;
      font-family: var(--mono);
      font-size: 12px;
      line-height: 1.4;
      resize: vertical;
    }
    .text-section .text-edit-content:focus {
      outline: none;
      border-color: var(--accent, #fbbf24);
      box-shadow: 0 0 0 2px rgba(251, 191, 36, 0.2);
    }
    .text-section .text-edit-subhead {
      font-family: var(--mono);
      font-size: 10px;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      color: var(--muted, #888);
      margin: 10px 0 6px;
    }
    .text-section .text-edit-row {
      display: grid;
      gap: 6px;
      margin-bottom: 6px;
    }
    .text-section .text-edit-row-3-2-2 {
      grid-template-columns: 3fr 1.4fr 1.6fr;
    }
    .text-section .text-edit-row-1-2-2 {
      grid-template-columns: 1.2fr 1.4fr 1.4fr;
    }
    .text-section .text-edit-row-3-2 {
      grid-template-columns: 3fr 1.5fr;
    }
    .text-section .text-edit-cell {
      display: flex;
      flex-direction: column;
      gap: 3px;
      min-width: 0;
    }
    .text-section .text-edit-cell-grow { min-width: 0; }
    .text-section .text-edit-label {
      font-family: var(--mono);
      font-size: 10px;
      color: var(--muted, #888);
      letter-spacing: 0.04em;
    }
    .text-section .text-edit-input {
      width: 100%;
      box-sizing: border-box;
      height: 24px;
      padding: 0 6px;
      background: var(--surface-low, #1a1a1a);
      color: var(--text, #e8e8e8);
      border: 1px solid var(--border-strong, #3a3a3a);
      border-radius: 4px;
      font-family: var(--mono);
      font-size: 11px;
    }
    .text-section .text-edit-input:focus {
      outline: none;
      border-color: var(--accent, #fbbf24);
      box-shadow: 0 0 0 2px rgba(251, 191, 36, 0.2);
    }
    .text-section input.text-edit-input[type="number"] {
      -moz-appearance: textfield;
    }
    .text-section input.text-edit-input[type="number"]::-webkit-inner-spin-button,
    .text-section input.text-edit-input[type="number"]::-webkit-outer-spin-button {
      -webkit-appearance: none;
      margin: 0;
    }
    .text-section .text-edit-color {
      padding: 1px 2px;
      cursor: pointer;
    }
    /* Color group — wraps the color swatch + eyedropper pick button.
       Grid layout keeps the swatch flexible and the eyedropper at a
       fixed 24px so the row's vertical rhythm matches the alignment
       and style toggle clusters. */
    .text-section .text-edit-color-group {
      display: grid;
      grid-template-columns: 1fr 24px;
      gap: 4px;
      align-items: stretch;
    }
    .text-section .text-edit-eyedropper {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 24px;
      height: 24px;
      padding: 0;
      background: var(--surface-low, #1a1a1a);
      color: var(--muted, #888);
      border: 1px solid var(--border, rgba(255, 255, 255, 0.12));
      border-radius: 4px;
      cursor: pointer;
      transition: color 0.12s, border-color 0.12s, background 0.12s;
    }
    .text-section .text-edit-eyedropper:hover {
      color: var(--accent, #f5c842);
      border-color: rgba(245, 200, 66, 0.45);
      background: rgba(245, 200, 66, 0.05);
    }
    .text-section .text-edit-eyedropper:active {
      transform: translateY(1px);
    }
    .text-section select.text-edit-input {
      appearance: none;
      -webkit-appearance: none;
      padding-right: 18px;
      background-image: linear-gradient(45deg, transparent 50%, var(--muted, #888) 50%),
                        linear-gradient(135deg, var(--muted, #888) 50%, transparent 50%);
      background-position: calc(100% - 9px) 50%, calc(100% - 5px) 50%;
      background-size: 4px 4px;
      background-repeat: no-repeat;
    }
    .text-section .text-edit-align {
      display: inline-flex;
      gap: 2px;
      height: 24px;
      /* Each button stays at a fixed 24×24 — flex wrapping is off
         intentionally so the 4 alignment buttons always stay on one
         line regardless of column width pressure from siblings. */
      flex-wrap: nowrap;
    }
    .text-section .text-edit-align-btn {
      width: 24px;
      flex: 0 0 auto;
    }
    .text-section .text-edit-align-btn {
      background: var(--surface-low, #1a1a1a);
      color: var(--muted, #888);
      border: 1px solid var(--border-strong, #3a3a3a);
      border-radius: 4px;
      cursor: pointer;
      font-family: var(--mono);
      font-size: 13px;
      line-height: 1;
      padding: 0;
      transition: background 0.12s, color 0.12s, border-color 0.12s;
      display: flex;
      align-items: center;
      justify-content: center;
    }
    .text-section .text-edit-align-btn svg {
      width: 14px;
      height: 12px;
      display: block;
    }

    /* Custom font dropdown — combo of <input> + chevron toggle + an
       absolutely-positioned menu. Replaces the native <datalist>
       because Chrome's datalist UI is inconsistent (sometimes won't
       open without a keystroke) and doesn't render per-option font
       previews — which is the whole point of a font picker. */
    .text-section .text-edit-font-combo {
      position: relative;
      display: flex;
      align-items: stretch;
    }
    .text-section .text-edit-font-input {
      flex: 1 1 auto;
      padding-right: 22px;   /* room for the chevron */
    }
    .text-section .text-edit-font-toggle {
      position: absolute;
      right: 2px;
      top: 50%;
      transform: translateY(-50%);
      width: 18px;
      height: 18px;
      padding: 0;
      background: transparent;
      border: none;
      color: var(--muted, #888);
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: center;
    }
    .text-section .text-edit-font-toggle:hover { color: var(--text, #e8e8e8); }
    .text-section .text-edit-font-toggle svg { width: 10px; height: 6px; display: block; }
    .text-section .text-edit-font-menu {
      position: absolute;
      top: calc(100% + 4px);
      left: 0;
      right: 0;
      max-height: 260px;
      overflow-y: auto;
      background: var(--surface-low, #1a1a1a);
      border: 1px solid var(--border-strong, #3a3a3a);
      border-radius: 4px;
      box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
      z-index: 100;
      padding: 4px 0;
    }
    .text-section .text-edit-font-menu[hidden] { display: none; }
    .text-section .text-edit-font-menu-item {
      padding: 6px 10px;
      font-size: 13px;
      color: var(--text, #e8e8e8);
      cursor: pointer;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      transition: background 0.08s;
    }
    .text-section .text-edit-font-menu-item:hover,
    .text-section .text-edit-font-menu-item.is-active {
      background: rgba(251, 191, 36, 0.14);
      color: var(--accent, #fbbf24);
    }
    .text-section .text-edit-font-menu-empty {
      padding: 10px 12px;
      font-family: var(--mono);
      font-size: 11px;
      color: var(--muted, #888);
      font-style: italic;
    }
    .text-section .text-edit-font-menu-section {
      padding: 6px 10px 3px;
      font-family: var(--mono);
      font-size: 9px;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      color: var(--muted, #888);
    }

    /* Custom stepper for the text-edit numeric fields (Tracking,
       Leading). Native browser spinners were hidden globally for
       consistency, but a small "up / down chevron" pair sitting flush
       to the right of the input gives quick coarse adjustments without
       reaching for the keyboard. Click steps by data-step on the
       container; data-min / data-max clamp. */
    .text-section .text-edit-num {
      position: relative;
      display: flex;
      align-items: stretch;
    }
    .text-section .text-edit-num-input {
      flex: 1 1 auto;
      padding-right: 16px;   /* room for the arrows */
    }
    .text-section .text-edit-num-arrows {
      position: absolute;
      right: 2px;
      top: 1px;
      bottom: 1px;
      width: 12px;
      display: flex;
      flex-direction: column;
      pointer-events: auto;
    }
    .text-section .text-edit-num-arr {
      flex: 1 1 0;
      min-height: 9px;
      background: transparent;
      border: none;
      color: var(--muted, #888);
      cursor: pointer;
      padding: 0;
      font-size: 6px;
      line-height: 1;
      display: flex;
      align-items: center;
      justify-content: center;
      transition: color 0.12s;
    }
    .text-section .text-edit-num-arr:hover {
      color: var(--accent, #fbbf24);
    }
    .text-section .text-edit-num-arr:active {
      color: var(--accent-bright, #fde047);
    }

    /* Paragraph row — Style + Alignment + Transform on a single row
       per user request. Style and Alignment auto-size to their button
       counts (min-content so neither shrinks below the fixed button
       width × button count); Transform shrinks down to its short
       option labels ("None" / "UPPER" / "lower") and only stretches
       if there's leftover room. Order in the grid: Style, Alignment,
       Transform. */
    .text-section .text-edit-row-paragraph {
      grid-template-columns: min-content min-content minmax(70px, 1fr);
      align-items: end;
    }
    .text-section .text-edit-row-paragraph .text-edit-style,
    .text-section .text-edit-row-paragraph .text-edit-align {
      flex-shrink: 0;
    }
    /* Visually narrower Transform select — designer-edit field doesn't
       need to be more than ~90px wide to fit "UPPER" comfortably with
       the chevron. The minmax(70px, 1fr) above lets it grow if there's
       free space but mostly stays compact so Alignment has room. */
    .text-section .text-edit-row-paragraph select#text-edit-transform {
      max-width: 110px;
    }

    /* (Legacy single-column .text-edit-row-style kept for backwards
       compat with any caller still building the old shape.) */
    .text-section .text-edit-row-style {
      grid-template-columns: 1fr;
    }
    .text-section .text-edit-style {
      display: inline-flex;
      gap: 2px;
      height: 24px;
    }
    .text-section .text-edit-style-btn {
      width: 28px;
      background: var(--surface-low, #1a1a1a);
      color: var(--muted, #888);
      border: 1px solid var(--border-strong, #3a3a3a);
      border-radius: 4px;
      cursor: pointer;
      padding: 0;
      transition: background 0.12s, color 0.12s, border-color 0.12s;
      display: flex;
      align-items: center;
      justify-content: center;
      font-family: var(--mono);
    }
    .text-section .text-edit-style-btn:hover {
      color: var(--text, #e8e8e8);
      border-color: rgba(251, 191, 36, 0.4);
    }
    .text-section .text-edit-style-btn.is-active {
      background: rgba(251, 191, 36, 0.16);
      color: var(--accent, #fbbf24);
      border-color: var(--accent, #fbbf24);
    }
    .text-section .text-edit-style-glyph {
      font-size: 13px;
      font-weight: 600;
      line-height: 1;
    }
    .text-section .text-edit-align-btn:hover {
      color: var(--text, #e8e8e8);
      border-color: rgba(251, 191, 36, 0.4);
    }
    .text-section .text-edit-align-btn.is-active {
      background: rgba(251, 191, 36, 0.16);
      color: var(--accent, #fbbf24);
      border-color: var(--accent, #fbbf24);
    }

    /* .layer-info — removed. The row no longer surfaces the file size
       + filename "details" line. Full layer id is available via the
       title attribute on the row when ellipsis-truncated. */

    .layer-z {
      font-family: var(--mono);
      font-size: 10px;
      color: var(--muted);
      background: #2a2a2a;
      padding: 2px 6px;
      border-radius: 6px;
      flex-shrink: 0;
    }

    /* Right-anchored controls cluster — z chip + eye + lock buttons
       sitting inline on a single horizontal row. Previously stacked
       vertically to free meta-column width for badges, but the
       vertical stack forced every layer-item to ~66 px tall regardless
       of thumbnail size. Putting them in a row collapses the layer
       row to a single line; badges wrap to a 2nd line when they
       exceed the available meta width, which is uncommon. */
    .layer-trailing {
      display: flex;
      flex-direction: row;
      align-items: center;
      gap: 4px;
      flex-shrink: 0;
    }

    /* Lock toggle in each layer-list row. Sits between meta and z-index.
       When the row is .locked, the icon (already filled) plus a subtle
       row-level visual treatment communicate the state. The button itself
       stays clickable — that's how the user unlocks. */
    .layer-lock-btn,
    .layer-eye-btn {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 22px;
      height: 22px;
      padding: 0;
      background: transparent;
      border: 1px solid transparent;
      border-radius: 5px;
      color: var(--muted);
      cursor: pointer;
      transition: background 0.1s, color 0.1s, border-color 0.1s;
    }
    .layer-lock-btn:hover,
    .layer-eye-btn:hover {
      background: rgba(255, 255, 255, 0.06);
      color: var(--text);
    }
    /* Hidden state — slashed eye + faded text on the row, similar
       tonal treatment to .layer-item.locked but with a cooler hue so
       the two states are distinguishable when both are on. */
    .layer-eye-btn.is-hidden {
      color: rgba(255, 255, 255, 0.45);
    }

    /* Layer-list filter input. Sits above the row list inside the
       Layers panel content. Slim, dark-themed, with a clear (×)
       button that surfaces only when the input is non-empty. */
    .layer-filter-wrap {
      position: relative;
      margin-bottom: 8px;
    }
    .layer-filter-input {
      width: 100%;
      box-sizing: border-box;
      padding: 6px 28px 6px 10px;
      font-family: var(--mono);
      font-size: 11px;
      letter-spacing: 0.04em;
      background: rgba(255, 255, 255, 0.04);
      border: 1px solid var(--border);
      border-radius: 6px;
      color: var(--text);
      outline: none;
      transition: border-color 0.1s, background 0.1s;
    }
    .layer-filter-input::placeholder {
      color: var(--muted);
      opacity: 0.7;
    }
    .layer-filter-input:focus {
      border-color: var(--accent);
      background: rgba(245, 200, 66, 0.04);
    }
    /* Strip the WebKit search-input clear (we use our own button) */
    .layer-filter-input::-webkit-search-cancel-button,
    .layer-filter-input::-webkit-search-decoration { -webkit-appearance: none; appearance: none; }
    .layer-filter-clear {
      position: absolute;
      top: 50%;
      right: 6px;
      transform: translateY(-50%);
      width: 18px;
      height: 18px;
      padding: 0;
      background: transparent;
      border: 0;
      border-radius: 50%;
      color: var(--muted);
      cursor: pointer;
      font-size: 14px;
      line-height: 1;
      transition: background 0.1s, color 0.1s;
    }
    .layer-filter-clear:hover {
      background: rgba(255, 255, 255, 0.08);
      color: var(--text);
    }
    .layer-filter-empty {
      padding: 14px 10px;
      font-family: var(--mono);
      font-size: 11px;
      color: var(--muted);
      text-align: center;
      opacity: 0.7;
    }
    .layer-item:has(.layer-eye-btn.is-hidden) .layer-name-text,
    .layer-item:has(.layer-eye-btn.is-hidden) .layer-thumb {
      opacity: 0.45;
    }
    .layer-item.locked .layer-lock-btn {
      color: var(--accent);
      background: rgba(245, 200, 66, 0.10);
      border-color: rgba(245, 200, 66, 0.35);
    }
    .layer-item.locked {
      background: rgba(245, 200, 66, 0.04);
      border-left: 2px solid rgba(245, 200, 66, 0.45);
    }
    .layer-item.locked .layer-name,
    .layer-item.locked .layer-thumb {
      opacity: 0.62;
    }

    /* Solo / mute panel buttons removed — solo lives on the timeline
       track row (next to the kf diamond) as .track-solo-toggle; mute
       was removed entirely. */

    /* Lock toggle in the animation-panel header. Same SVG, same toggle
       semantics as the layers-panel button — duplicated so the user can
       lock/unlock from wherever they're looking. */
    .vars-id {
      display: flex;
      align-items: center;
      gap: 8px;
    }
    .vars-id-name {
      flex: 1;
      min-width: 0;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    .vars-id-lock {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 26px;
      height: 22px;
      padding: 0;
      background: transparent;
      border: 1px solid rgba(255, 255, 255, 0.08);
      border-radius: 5px;
      color: var(--muted);
      cursor: pointer;
      flex-shrink: 0;
      transition: background 0.1s, color 0.1s, border-color 0.1s;
    }
    .vars-id-lock:hover {
      background: rgba(255, 255, 255, 0.06);
      color: var(--text);
    }
    .vars-id-lock.locked {
      color: var(--accent);
      background: rgba(245, 200, 66, 0.12);
      border-color: rgba(245, 200, 66, 0.45);
    }

    /* When the selected layer is locked, dim + click-disable the three
       content sections AND the tabs row so EVERY edit affordance reads
       as locked at a glance. The vars-id (layer name + lock icon) and
       the LAYER LOCKED banner stay fully bright as the unlock-path
       targets — anything else is dim and inert. cursor: not-allowed on
       the parent shines through to the dimmed children (their
       pointer-events: none lets mouse events fall through to the
       parent, which sets the not-allowed cursor). */
    #vars-panel.locked .vars-panel-main {
      position: relative;
      cursor: not-allowed;
    }
    #vars-panel.locked .vars-panel-main > #vars-static,
    #vars-panel.locked .vars-panel-main > #vars-anim,
    #vars-panel.locked .vars-panel-main > #vars-rollover {
      pointer-events: none;
      opacity: 0.32;
      transition: opacity 0.12s;
    }
    /* Tabs row dims too when locked — Static tab no longer looks
       "active" when the user can't actually author anything inside
       it. Stays click-blocked so the user can't pivot to Animation /
       Rollover while locked; unlocking is the prerequisite to navigate. */
    #vars-panel.locked .vars-tabs {
      pointer-events: none;
      opacity: 0.32;
      transition: opacity 0.12s;
    }
    /* Block EVERY descendant of the three content sections + the tabs
       row. The parent's `pointer-events: none` doesn't cascade to
       children with their own `pointer-events: auto` (stepper buttons,
       inputs, native form controls, etc.) — the universal selector
       under each section closes that gap. Scoped to the content
       sections specifically so the LAYER LOCKED banner and the header
       lock icon (both outside these sections) stay clickable as the
       unlock paths. cursor: not-allowed forced too so the inputs'
       default cursor: text / buttons' cursor: pointer don't override
       the disabled affordance. */
    #vars-panel.locked #vars-static *,
    #vars-panel.locked #vars-anim *,
    #vars-panel.locked #vars-rollover *,
    #vars-panel.locked .vars-tabs * {
      pointer-events: none !important;
      cursor: not-allowed !important;
    }
    /* LAYER LOCKED banner — real <button> injected by renderVarsPanelHeader
       so the banner itself is clickable to toggle the lock back off.
       Same accent-yellow palette the old ::before pseudo carried; the
       hover state lifts the background a touch to advertise click. */
    .vars-panel-locked-banner {
      appearance: none;
      -webkit-appearance: none;
      display: block;
      width: 100%;
      position: sticky;
      top: 0;
      z-index: 5;
      padding: 6px 10px;
      background: rgba(245, 200, 66, 0.10);
      border: 1px solid rgba(245, 200, 66, 0.35);
      border-radius: 6px;
      color: var(--accent);
      font-family: var(--mono);
      font-size: 10px;
      font-weight: 600;
      letter-spacing: 0.12em;
      text-align: center;
      margin-bottom: 8px;
      cursor: pointer;
      transition: background 0.12s, border-color 0.12s;
    }
    .vars-panel-locked-banner:hover {
      background: rgba(245, 200, 66, 0.18);
      border-color: rgba(245, 200, 66, 0.6);
    }
    .vars-panel-locked-banner:focus-visible {
      outline: none;
      box-shadow: 0 0 0 2px rgba(245, 200, 66, 0.45);
    }

    /* Locked layers in the timeline tracks panel get the same visual cue
       and bar drags ignore them (JS gate at startBarDrag). */
    .track-row.locked {
      opacity: 0.5;
    }
    .track-row.locked .track-name::after {
      content: ' 🔒';
      font-size: 9px;
    }

    /* ── Manifest download ── warm yellow primary buttons */
    #download-btn {
      display: none;
      width: 100%;
      padding: 11px 14px;
      background: linear-gradient(180deg, #ffd87a, #f5c842);
      color: #2a1d00;
      border: 1px solid rgba(245, 200, 66, 0.6);
      border-radius: 8px;
      font-family: var(--mono);
      font-size: 12px;
      font-weight: 600;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      cursor: pointer;
      transition: all 0.15s;
      margin-bottom: 8px;
      box-shadow: 0 4px 14px rgba(245, 200, 66, 0.25), inset 0 1px 0 rgba(255,255,255,0.3);
    }

    #download-btn:hover {
      background: linear-gradient(180deg, #ffe190, #ffd05a);
      color: #2a1d00;
    }

    #download-zip-btn {
      display: none;
      width: 100%;
      padding: 10px 12px;
      background: var(--btn-bg);
      color: var(--accent);
      border: 1px solid rgba(245, 200, 66, 0.4);
      border-radius: 8px;
      font-family: var(--mono);
      font-size: 12px;
      font-weight: 500;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      cursor: pointer;
      transition: all 0.15s;
      backdrop-filter: blur(12px);
      -webkit-backdrop-filter: blur(12px);
    }

    #download-zip-btn:hover { background: rgba(245, 200, 66, 0.10); }

    /* #download-html-btn rule was here when the button lived in the
       Downloads sidebar (full-width vertical-stack chip). Now that it
       lives in the header, .header-btn + .header-btn-primary handle
       all its styling — keeping the id-scoped rule would override the
       header chip styles via specificity. Removed deliberately. */

    .panel-sub-label {
      font-family: var(--mono);
      font-weight: 600;
      font-size: 11px;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      color: var(--muted);
      margin-top: 16px;
      margin-bottom: 10px;
      padding-top: 14px;
      border-top: 1px solid var(--border);
    }

    .preset-io-btn {
      display: block;
      width: 100%;
      padding: 10px 12px;
      margin-bottom: 8px;
      background: var(--btn-bg);
      color: var(--text);
      border: 1px solid var(--border);
      border-radius: 8px;
      font-family: var(--mono);
      font-weight: 500;
      font-size: 12px;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      cursor: pointer;
      text-align: left;
      transition: all 0.15s;
      backdrop-filter: blur(12px);
      -webkit-backdrop-filter: blur(12px);
    }

    .preset-io-btn:hover {
      background: rgba(255, 255, 255, 0.08);
      border-color: rgba(245, 200, 66, 0.4);
      color: var(--accent);
    }

    /* Subtle keyboard-shortcut hint chip inside a preset button */
    .kbd-hint {
      font-family: var(--mono);
      font-size: 10px;
      color: var(--muted);
      letter-spacing: 0.1em;
      margin-left: 6px;
      padding: 2px 6px;
      border: 1px solid var(--border);
      border-radius: 6px;
      vertical-align: middle;
      background: rgba(0, 0, 0, 0.25);
    }
    .preset-io-btn:hover .kbd-hint { color: var(--accent); border-color: rgba(245, 200, 66, 0.4); }

    /* ── Right panel — canvas + variables ── */
    .panel-right {
      display: flex;
      flex-direction: column;
      min-width: 0;
      min-height: 0;
    }
    /* When a banner is loaded, switch to a 2-column grid so the layer
       settings panel sits BESIDE the canvas. The canvas row height is
       controlled by --canvas-row-h (default `auto`) — drag the handle
       between the canvas row and the transport bar to override it with a
       pixel value. */
    .panel-right.has-manifest {
      /* Vars panel runs as a FULL-HEIGHT right column — it spans every
         row from canvas down to export. The other elements (canvas,
         resize, transport, timeline, export) stack as a single column
         on the LEFT. Vars gets the entire viewport height (below the
         top app header) instead of being capped at canvas-row-h or
         shared with the timeline row.
         To revert to the previous "vars beside canvas only" layout,
         change grid-template-areas back to:
           "canvas    vars"
           "resize    resize"
           "transport transport"
           "track     track"
           "export    export"
         and grid-template-rows to:
           var(--canvas-row-h) auto auto minmax(220px, 1fr) auto */
      --canvas-row-h: minmax(360px, auto);
      display: grid;
      grid-template-columns: minmax(0, 1fr) 340px;
      grid-template-rows: var(--canvas-row-h) auto auto minmax(220px, 1fr) auto;
      gap: 5px;
      grid-template-areas:
        "canvas    vars"
        "resize    vars"
        "transport vars"
        "track     vars"
        "export    export";
    }
    .panel-right.has-manifest #canvas-area         { grid-area: canvas; }
    /* vars-panel uses display:contents so its children (.vars-panel-main and
       .vars-panel-export) become direct grid items. That lets the export
       block claim the bottom row independently of the layer settings. */
    .panel-right.has-manifest #vars-panel          { display: contents; }
    .panel-right.has-manifest .vars-panel-main     { grid-area: vars; }
    /* The stack takes the grid cell; each .vars-panel-export inside is a
       free-flowing flex item so multiple collapsible panels can sit side-
       by-side (currently: Global+Clickthrough and Custom code). */
    .panel-right.has-manifest .vars-panel-export-stack {
      grid-area: export;
      display: flex;
      flex-direction: column;
      /* 5px to match the parent grid's row-gap — same visual rhythm as
         the gap between the canvas / transport / track / export rows. */
      gap: 5px;
      min-width: 0;
    }
    .panel-right.has-manifest .vars-panel-export {
      width: auto;
      border-left: 0;
      /* glass card treatment — same flat dark as the rest */
      background: rgba(12, 11, 16, 0.62);
      backdrop-filter: var(--glass-blur);
      -webkit-backdrop-filter: var(--glass-blur);
      border: 1px solid rgba(255, 255, 255, 0.05);
      border-radius: 12px;
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.04),
        0 6px 20px -8px rgba(0, 0, 0, 0.5);
    }
    .panel-right.has-manifest #canvas-resize-handle { grid-area: resize; }
    .panel-right.has-manifest #transport            {
      grid-area: transport;
      /* Fuse with the timeline panel below: drop bottom corners + bottom
         border, eat the row gap so they read as one continuous card. */
      border-bottom-left-radius: 0;
      border-bottom-right-radius: 0;
      border-bottom: none;
      margin-bottom: -5px;
    }
    .panel-right.has-manifest #track-editor         {
      grid-area: track;
      min-height: 0;
      /* Top half of the fused transport+timeline card */
      border-top-left-radius: 0;
      border-top-right-radius: 0;
      border-top: 1px solid rgba(255, 255, 255, 0.04);
      box-shadow:
        0 6px 20px -8px rgba(0, 0, 0, 0.35),
        0 1px 3px rgba(0, 0, 0, 0.18);
    }

    /* Hidden until manifest loaded; shown only inside the grid layout.
       Compact ~140px pill centered in the gap between canvas and the
       fused transport+timeline. Reads as a discrete drag-tab rather
       than a full-width edge of the timeline panel. */
    #canvas-resize-handle { display: none; }
    .panel-right.has-manifest #canvas-resize-handle {
      display: flex;
      align-items: center;
      justify-content: center;
      width: 140px;
      height: 12px;
      margin: 0 auto;               /* centered horizontally in the row */
      cursor: row-resize;
      background: rgba(12, 11, 16, 0.72);
      backdrop-filter: var(--glass-blur);
      -webkit-backdrop-filter: var(--glass-blur);
      border: 1px solid rgba(255, 255, 255, 0.08);
      border-radius: 999px;
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.05),
        0 2px 6px rgba(0, 0, 0, 0.3);
      transition: background 0.12s, border-color 0.12s;
      z-index: 1;
    }
    .panel-right.has-manifest #canvas-resize-handle:hover {
      background: rgba(245, 200, 66, 0.14);
      border-color: rgba(245, 200, 66, 0.4);
    }
    .panel-right.has-manifest #canvas-resize-handle.dragging {
      background: rgba(245, 200, 66, 0.24);
      border-color: rgba(245, 200, 66, 0.6);
    }
    .canvas-resize-grip {
      width: 32px;
      height: 3px;
      border-radius: 999px;
      background: rgba(255, 255, 255, 0.22);
      transition: background 0.12s;
    }
    #canvas-resize-handle:hover .canvas-resize-grip { background: var(--accent); }

    /* ── Rulers + guides ──────────────────────────────────────────────
       Top + left rulers along the canvas-area edges. The corner cell
       sits where they meet. Tick marks rendered as SVG inside each
       ruler; they recompute on zoom + pan changes via renderRulers().
       Hidden until a manifest is loaded so the empty state stays clean. */
    #ruler-h, #ruler-v, #ruler-corner {
      position: absolute;
      /* Darker ruler chrome so ticks + labels (kept at original
         brightness) read with more contrast against the bar. */
      background: #1a1820;
      border: 1px solid var(--border);
      pointer-events: none;
      user-select: none;
      -webkit-user-select: none;
      z-index: 30;
      display: none;
    }
    .panel-right.has-manifest #canvas-area:not(.is-empty) #ruler-h,
    .panel-right.has-manifest #canvas-area:not(.is-empty) #ruler-v,
    .panel-right.has-manifest #canvas-area:not(.is-empty) #ruler-corner {
      display: block;
    }
    #ruler-h {
      top: 0;
      left: 17px;
      right: 0;
      height: 17px;
      cursor: ns-resize;          /* drag down → spawns horizontal guide */
      pointer-events: auto;
      overflow: hidden;
    }
    #ruler-v {
      top: 17px;
      left: 0;
      bottom: 0;
      width: 17px;
      cursor: ew-resize;          /* drag right → spawns vertical guide */
      pointer-events: auto;
      overflow: hidden;
    }
    #ruler-corner {
      top: 0;
      left: 0;
      width: 17px;
      height: 17px;
      z-index: 31;
    }
    #ruler-h svg, #ruler-v svg {
      position: absolute;
      inset: 0;
      width: 100%;
      height: 100%;
      pointer-events: none;
    }
    /* Three-tier tick stroke — short (subtle) → medium → long
       (label-bearing). Matches PS's ruler hierarchy. Slightly
       toned-down alphas vs. the previous pass so the ticks sit a
       half-step quieter against the dark ruler chrome without
       disappearing. */
    .ruler-tick {
      stroke: rgba(255, 255, 255, 0.16);
      stroke-width: 1;
      shape-rendering: crispEdges;
    }
    .ruler-tick-medium { stroke: rgba(255, 255, 255, 0.30); }
    .ruler-tick-long   { stroke: rgba(255, 255, 255, 0.48); }
    .ruler-label {
      font-family: var(--mono);
      font-size: 8px;
      fill: rgba(255, 255, 255, 0.70);
      letter-spacing: 0;
    }
    /* Guide lines drawn on the SVG overlay. .guide-preview is the
       ghost guide that follows the cursor while the user is
       dragging from a ruler. */
    .guide-line {
      position: absolute;
      background: rgba(0, 200, 255, 0.55);
      pointer-events: auto;
      z-index: 25;
    }
    .guide-line.guide-h {
      left: 17px;
      right: 0;
      height: 1px;
      cursor: ns-resize;
    }
    .guide-line.guide-v {
      top: 17px;
      bottom: 0;
      width: 1px;
      cursor: ew-resize;
    }
    .guide-line:hover { background: rgba(0, 200, 255, 0.9); }
    .guide-preview {
      pointer-events: none;
      background: rgba(0, 200, 255, 0.85);
      box-shadow: 0 0 4px rgba(0, 200, 255, 0.5);
    }
    /* Cmd/Ctrl+; toggles this class on #canvas-area to mirror
       Photoshop's "View > Show > Guides" hide/show. Rulers + ticks
       stay visible regardless. */
    #canvas-area.guides-hidden .guide-line:not(.guide-preview) {
      display: none !important;
    }
    /* Push the existing align/zoom toolbars down so the top ruler
       doesn't sit underneath them. Ruler is 17 px tall + a few px
       of breathing room. */
    .panel-right.has-manifest #canvas-area:not(.is-empty) #zoom-toolbar,
    .panel-right.has-manifest #canvas-area:not(.is-empty) #align-toolbar {
      top: 24px;
    }
    /* The left ruler also covers x=[0..17] of the canvas-area, so the
       align-toolbar (top-left) needs to slide right past it. The
       zoom-toolbar lives on the right edge so it's unaffected. */
    .panel-right.has-manifest #canvas-area:not(.is-empty) #align-toolbar {
      left: 24px;
    }

    /* ── View menu (header dropdown) ──────────────────────────────
       Toggle switches for hiding/showing editor chrome (rulers,
       alignment toolbar, etc.). State persists via localStorage. */
    .header-menu {
      position: relative;
      display: inline-flex;
    }
    .header-menu-panel {
      position: absolute;
      top: calc(100% + 4px);
      left: 0;
      min-width: 220px;
      padding: 4px;
      background: rgba(20, 18, 22, 0.95);
      backdrop-filter: var(--glass-blur);
      -webkit-backdrop-filter: var(--glass-blur);
      border: 1px solid var(--border);
      border-radius: 10px;
      box-shadow:
        0 12px 30px -10px rgba(0, 0, 0, 0.6),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
      z-index: 200;
      display: flex;
      flex-direction: column;
      gap: 1px;
    }
    .header-menu-panel[hidden] { display: none; }
    .header-menu-item {
      display: flex;
      align-items: center;
      gap: 12px;
      padding: 8px 10px;
      border-radius: 6px;
      cursor: pointer;
      transition: background 0.1s;
      user-select: none;
      -webkit-user-select: none;
    }
    .header-menu-item:hover {
      background: rgba(255, 255, 255, 0.05);
    }
    .header-menu-label {
      font-family: var(--mono);
      font-size: 11.5px;
      letter-spacing: 0.04em;
      color: var(--text);
      /* Take all leftover width so the hint + toggle anchor right. */
      flex: 1 1 auto;
      min-width: 0;
    }
    /* Keyboard-shortcut hint text in each menu item — sits between the
       label and the toggle, smaller + muted so the label stays the
       primary visual hook. JS populates the text at init based on
       platform (⌘ on Mac, Ctrl on Windows). */
    .header-menu-hint {
      font-family: var(--mono);
      font-size: 9.5px;
      letter-spacing: 0.04em;
      color: var(--text-muted);
      white-space: nowrap;
      flex-shrink: 0;
    }

    /* Action-style menu items (used by the Settings dropdown) — same
       chrome as the toggle items but with a stacked label + small
       muted description on the left, and an optional hotkey hint on
       the right. Click fires through to the original action.
       Anchor to the right so the wide panel doesn't spill past the
       browser viewport when the trigger button sits near the right
       edge of the header. */
    .header-menu-panel-actions {
      min-width: 280px;
      left: auto;
      right: 0;
    }
    .header-menu-action {
      display: flex;
      align-items: center;
      gap: 12px;
      width: 100%;
      padding: 8px 10px;
      background: transparent;
      border: none;
      border-radius: 6px;
      cursor: pointer;
      text-align: left;
      transition: background 0.1s;
      color: var(--text);
      user-select: none;
      -webkit-user-select: none;
    }
    .header-menu-action:hover {
      background: rgba(255, 255, 255, 0.05);
    }
    .header-menu-action:disabled {
      opacity: 0.45;
      cursor: not-allowed;
    }
    .header-menu-action:disabled:hover {
      background: transparent;
    }
    .header-menu-action-icon {
      flex-shrink: 0;
      color: var(--text-muted);
      width: 14px;
      height: 14px;
    }
    .header-menu-action:hover .header-menu-action-icon {
      color: var(--text);
    }
    .header-menu-action-text {
      flex: 1 1 auto;
      display: flex;
      flex-direction: column;
      gap: 2px;
      min-width: 0;
    }
    .header-menu-action-label {
      font-family: var(--mono);
      font-size: 11.5px;
      letter-spacing: 0.04em;
      color: var(--text);
    }
    .header-menu-action-desc {
      font-family: var(--mono);
      font-size: 9.5px;
      color: var(--text-muted);
      letter-spacing: 0.02em;
      line-height: 1.3;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }

    /* Hide the original four header buttons that the Settings menu
       replaces. They stay in the DOM so their existing handlers +
       state-management (disabled flips, label edits) keep firing
       untouched; the menu items dispatch click through to them. */
    .header-btn-hidden { display: none !important; }

    /* When the user toggles the corresponding View-menu switch off,
       hide the matching chrome. Ruler hide also reverses the toolbar
       offset so they reclaim the top-left/top-row space the ruler
       was occupying. */
    #canvas-area.hide-align-tools #align-toolbar { display: none !important; }
    #canvas-area.hide-zoom-tools  #zoom-toolbar  { display: none !important; }
    #canvas-area.hide-rulers #ruler-h,
    #canvas-area.hide-rulers #ruler-v,
    #canvas-area.hide-rulers #ruler-corner {
      display: none !important;
    }
    .panel-right.has-manifest #canvas-area.hide-rulers:not(.is-empty) #zoom-toolbar,
    .panel-right.has-manifest #canvas-area.hide-rulers:not(.is-empty) #align-toolbar {
      top: 10px;
    }
    .panel-right.has-manifest #canvas-area.hide-rulers:not(.is-empty) #align-toolbar {
      left: 12px;
    }

    /* ── Canvas area ── transparent so the body backdrop reads through */
    #canvas-area {
      flex: 1 1 auto;
      max-height: 50vh;            /* default; overridden by has-manifest */
      display: flex;
      align-items: center;
      justify-content: center;
      background: transparent;
      position: relative;
      overflow: hidden;            /* free-pan via canvas-container transform — no scrollbars */
      min-height: 0;
    }
    /* Once a banner is loaded the grid sizes the canvas row to fit its
       content, so the timeline pulls right up under the canvas instead of
       sitting in the middle of a half-empty viewport. */
    .panel-right.has-manifest #canvas-area {
      max-height: none;
    }
    /* The container itself is centered by flex; pan offset is applied as a
       CSS translate (set via JS), so users can drag the artboard anywhere. */
    #canvas-container { will-change: transform; }

    /* When no banner is loaded, let the placeholder fill the whole right panel
       (no need to leave room for transport bar / vars panel — they're hidden). */
    #canvas-area.is-empty {
      max-height: none;
    }

    /* Figma-style spacebar pan: hold space → grab cursor anywhere in the
       canvas area; click-drag panes the scroll. The capture-phase mousedown
       handler intercepts so layer click-to-select doesn't fire while panning. */
    #canvas-area.space-pan-mode               { cursor: grab; }
    #canvas-area.space-pan-mode * { cursor: grab !important; }
    #canvas-area.space-pan-active,
    #canvas-area.space-pan-active *           { cursor: grabbing !important; }

    /* Bring the relevant bbox to the front while its modifier is held.
       The selectors below require origin-overlay to also have the
       matching mod-*-held class so this rule out-specs the
       .kb-exit-simple #from-bounds { display:none !important } lock —
       (2 IDs, 2 classes) > (2 IDs, 1 class). Without the extra class
       on origin-overlay, source-order would let .kb-exit-simple win
       and the FROM bbox would stay hidden on Exit pill even when alt
       is held. */
    #origin-overlay.mod-alt-held #from-bounds.mod-elevated,
    #origin-overlay.mod-ctrl-held #exit-bounds.mod-elevated {
      z-index: 99 !important;
      display: block !important;
    }
    /* The chips need to show alongside their bbox when alt/ctrl is
       held — without these rules the .kb-exit-simple lockout still
       hides #from-label even though the bbox is now visible, leaving
       the user with a "ghost" rectangle and no chip handle. The
       connector + arrow likewise need to come back so the FROM →
       REST visual link reads.

       Specificity matters: .kb-exit-simple #from-label has (2 IDs,
       1 class). To win the source-order tiebreak with same-specificity
       selectors I'd have to put my rule AFTER it in the file, which
       wasn't the case. Adding .kb-exit-simple to the override
       selector here bumps to (2,2,0) and wins outright. The override
       only fires when both modifier + lockout are active, which is
       exactly the scenario that needs un-hiding. */
    #origin-overlay.mod-alt-held.kb-exit-simple #from-label,
    #origin-overlay.mod-alt-held #from-label {
      display: flex !important;
      z-index: 99 !important;
    }
    /* Exit-pill-only override — .kb-exit-simple's lockout sets
       display:none !important on the FROM connector + arrow, and we
       need to beat that when alt is held so the user can see the
       FROM→REST relationship while authoring on Exit pill. The
       previous broader rule (without .kb-exit-simple) fought JS's
       legitimate display:none for the zero-length FROM=REST case
       and could leak a stale arrow position across drags. Now JS
       controls display freely on Static / Entry tabs. */
    #origin-overlay.mod-alt-held.kb-exit-simple #from-connector,
    #origin-overlay.mod-alt-held.kb-exit-simple #from-connector-arrow {
      display: block !important;
    }
    #origin-overlay.mod-ctrl-held #exit-label {
      display: flex !important;
      z-index: 99 !important;
    }

    /* Floating zoom toolbar in the top-right of the canvas-area */
    #zoom-toolbar {
      position: absolute;
      top: 10px;
      right: 12px;
      display: none;
      align-items: center;
      gap: 2px;
      padding: 4px;
      background: rgba(20, 18, 22, 0.55);
      border: 1px solid rgba(255, 255, 255, 0.08);
      border-radius: 8px;
      box-shadow:
        0 4px 14px rgba(0, 0, 0, 0.4),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
      backdrop-filter: var(--glass-blur);
      -webkit-backdrop-filter: var(--glass-blur);
      z-index: 20;
      pointer-events: auto;
      user-select: none;
    }
    #zoom-toolbar.show { display: flex; }

    /* Floating alignment toolbar in the TOP-LEFT of the canvas-area.
       Same chrome as #zoom-toolbar (right side); mirrored position so
       both float at the same height. Hidden by default; shown via
       .show on the same conditions as the zoom toolbar (i.e. when a
       manifest is loaded). */
    #align-toolbar {
      position: absolute;
      top: 10px;
      left: 12px;
      display: none;
      align-items: center;
      gap: 2px;
      padding: 4px;
      background: rgba(20, 18, 22, 0.55);
      border: 1px solid rgba(255, 255, 255, 0.08);
      border-radius: 8px;
      box-shadow:
        0 4px 14px rgba(0, 0, 0, 0.4),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
      backdrop-filter: var(--glass-blur);
      -webkit-backdrop-filter: var(--glass-blur);
      z-index: 20;
      pointer-events: auto;
      user-select: none;
    }
    #align-toolbar.show { display: flex; }
    #align-toolbar button {
      background: transparent;
      border: none;
      color: var(--text);
      padding: 5px 7px;
      cursor: pointer;
      border-radius: 6px;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      transition: background 0.1s, color 0.1s;
    }
    #align-toolbar button:hover {
      background: rgba(245, 200, 66, 0.12);
      color: var(--accent);
    }
    #align-toolbar button:active { background: rgba(245, 200, 66, 0.22); }
    #align-toolbar button:disabled {
      opacity: 0.35;
      cursor: not-allowed;
    }
    #align-toolbar button:disabled:hover {
      background: transparent;
      color: var(--text);
    }
    #align-toolbar .align-divider {
      width: 1px;
      height: 16px;
      background: rgba(255, 255, 255, 0.1);
      margin: 0 3px;
    }
    #zoom-toolbar button {
      background: transparent;
      border: none;
      color: var(--text);
      font-family: var(--mono);
      font-weight: 500;
      font-size: 12px;
      padding: 5px 9px;
      cursor: pointer;
      border-radius: 6px;
      min-width: 24px;
      transition: background 0.1s, color 0.1s;
    }
    #zoom-toolbar button:hover {
      background: rgba(245, 200, 66, 0.12);
      color: var(--accent);
    }
    #zoom-toolbar button:active { background: rgba(245, 200, 66, 0.22); }
    #zoom-toolbar .zoom-label {
      font-family: var(--mono);
      font-weight: 500;
      font-size: 11.5px;
      color: var(--muted);
      padding: 5px 8px;
      min-width: 42px;
      text-align: center;
      cursor: pointer;
      border-radius: 6px;
      transition: background 0.1s, color 0.1s;
    }
    #zoom-toolbar .zoom-label:hover { background: rgba(245, 200, 66, 0.08); color: var(--text); }
    /* Inline-input swap used when the user clicks the zoom % to retype
       a value. Matches the label's typography so the swap is visually
       seamless. */
    #zoom-toolbar .zoom-label .zoom-inline-input {
      width: 36px;
      background: transparent;
      border: none;
      outline: none;
      color: var(--accent);
      font-family: inherit;
      font-weight: inherit;
      font-size: inherit;
      text-align: center;
      padding: 0;
      margin: 0;
    }
    #zoom-toolbar .zoom-divider {
      width: 1px;
      height: 16px;
      background: rgba(255, 255, 255, 0.10);
      margin: 0 2px;
    }

    #canvas-empty {
      text-align: center;
    }

    .empty-label {
      font-family: var(--mono);
      font-weight: 500;
      font-size: 13px;
      color: var(--muted);
      letter-spacing: 0.1em;
      text-transform: uppercase;
    }

    /* The rendered banner. Must NEVER have any visual styling applied —
       the container holds the actual ad output and the export will look
       exactly like what's on screen. No radius, no border, no shadow.
       flex-shrink: 0 is critical — #canvas-area is a flex container, so
       without this the default flex-shrink: 1 would clamp the
       container back to the parent's width whenever the user zooms
       past 100% (the 800% zoom would silently cap at 100%). */
    #canvas-container {
      display: none;
      position: relative;
      flex-shrink: 0;
    }

    #render-canvas {
      display: block;
      width: 100%;
      height: 100%;
    }

    /* Live video preview overlay — full-bleed inside #canvas-container so
       child <video> elements can be positioned in manifest-pixel coords.
       pointer-events:none on the wrapper lets clicks pass through to the
       canvas for selection; individual videos opt back in only when they
       have controls enabled. */
    #video-overlay {
      position: absolute;
      inset: 0;
      width: 100%;
      height: 100%;
      pointer-events: none;
      overflow: hidden;
    }
    #video-overlay > video {
      position: absolute;
      transform-origin: 0 0;
      background: transparent;
      display: block;
    }
    /* Per-target object-fit matches the export bundle's convention:
       group videos cover the mask's rect (no letterboxing on the
       authored video window), lone-layer videos contain inside the
       PSD layer's rect (no stretch / crop). syncVideoOverlay sets a
       `data-fit` attribute on each element based on its target type. */
    #video-overlay > video[data-fit="cover"]   { object-fit: cover; }
    #video-overlay > video[data-fit="contain"] { object-fit: contain; }
    #video-overlay > video.has-controls { pointer-events: auto; }

    /* ── Variables panel ── */
    /* When `show` class is present, the panel becomes a flex column in the
       legacy (non-grid) layout. In the has-manifest grid layout the
       has-manifest rule overrides this with display:contents so the panel's
       children claim grid cells independently. */
    #vars-panel {
      display: none;
      flex-direction: column;
      min-width: 0;
      min-height: 0;
      overflow-y: auto;
      /* glass treatment from the shared floating-panel block at top */
    }
    #vars-panel.show { display: flex; }

    .vars-panel-main {
      flex: 0 0 auto;
      min-width: 0;
      /* No top padding — the sticky .vars-header owns that gutter so
         it can render its solid backdrop all the way to the panel's
         outer top edge. Without this, the panel's top padding leaves
         a 14px transparent strip above the sticky band where scrolled
         content shows through. */
      padding: 0 16px 14px;
    }
    /* In the grid layout the layer settings panel is its own grid item
       sitting next to the canvas as a detached glass card. */
    .panel-right.has-manifest .vars-panel-main {
      overflow-y: auto;
      min-height: 0;
      max-height: 100%;
      border-left: 0;
      /* glass treatment from shared floating-panel block at top */
    }

    .vars-panel-export {
      width: auto;
      flex-shrink: 0;
      padding: 14px 16px;
      border-top: 1px solid var(--border);
      border-left: 0;
      background: rgba(0, 0, 0, 0.22);
      /* Vertical stack: master toggle bar on top, then the 2-column
         content grid below. The toggle hides everything below it
         (.panel-content) when collapsed. */
      display: flex;
      flex-direction: column;
      gap: 12px;
    }
    .vars-panel-export > .panel-content {
      /* Two-column layout: Global settings | Clickthrough.
         Each .export-section column scrolls/grows independently. */
      display: grid;
      grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
      gap: 24px;
      align-items: start;
    }
    /* The Custom code panel only has a single .export-section child, and
       that section runs its own internal 2-column grid for the editors —
       so override the panel-content grid to single-column here, otherwise
       the section would only fill the left half of the panel. */
    [data-section-id="custom-code-panel"] > .panel-content {
      grid-template-columns: minmax(0, 1fr);
    }
    .export-section {
      min-width: 0;
    }

    /* Master toggle bar — single chevron-on-right header that controls
       both Global Settings + Clickthrough visibility together. */
    .vars-export-master-toggle {
      width: 100%;
      background: transparent;
      border: none;
      cursor: pointer;
      text-align: left;
      display: flex;
      align-items: center;
      gap: 8px;
      font-family: var(--mono);
      font-weight: 600;
      font-size: 12px;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      color: var(--text);
      opacity: 0.85;
      padding: 0 0 8px;
      border-bottom: 1px solid var(--border);
      transition: opacity 0.12s;
    }
    .vars-export-master-toggle:hover { opacity: 1; }
    .vars-export-master-toggle .collapse-icon { margin-left: auto; }
    /* Collapsed state — hide the bottom border + padding so the bar
       reads compact when the body is hidden. */
    .vars-panel-export.collapsible-section.collapsed .vars-export-master-toggle {
      padding-bottom: 0;
      border-bottom-color: transparent;
    }
    .vars-panel-export.collapsible-section.collapsed { gap: 0; }

    .vars-export-label {
      font-family: var(--mono);
      font-weight: 600;
      font-size: 12px;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      color: var(--text);
      opacity: 0.85;
      margin-bottom: 14px;
      padding-bottom: 8px;
      border-bottom: 1px solid var(--border);
    }


    /* Stack the layer-id label on top of the Static/Animation/Rollover
       tabs (instead of competing with them on the same row). Reverse the
       flex order so .vars-id renders first regardless of DOM order.
       Spans the full panel width (negative side margins eat the panel's
       16px horizontal padding) and owns the panel's top gutter — see
       .vars-panel-main where padding-top: 0 makes that possible. */
    .vars-header {
      display: flex;
      flex-direction: column;
      align-items: stretch;
      gap: 6px;
      position: sticky;
      top: 0;
      z-index: 10;
      /* Match the parent panel's translucent surface (rgba 0.62) instead
         of a flat black slab. backdrop-filter blurs whatever is scrolling
         beneath the header so content can't bleed through, while still
         letting the body gradient show through faintly — same frosted
         look as the rest of the card. */
      background: rgba(12, 11, 16, 0.62);
      backdrop-filter: blur(18px) saturate(140%);
      -webkit-backdrop-filter: blur(18px) saturate(140%);
      margin: 0 -16px 0;
      padding: 14px 16px 0;
      box-shadow: 0 6px 12px -8px rgba(0, 0, 0, 0.5);
    }

    .vars-title {
      font-family: var(--mono);
      font-weight: 600;
      font-size: 12px;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      color: var(--text);
      opacity: 0.85;
    }

    .vars-id {
      font-family: var(--mono);
      font-size: 11px;
      color: var(--accent);
      order: -1;                /* sits above .vars-tabs visually */
      padding: 4px 0 2px;
      letter-spacing: 0.04em;
    }

    /* Three-column layout — most values are 1–4 chars (rotation degrees,
       opacity 0–1, scale fractions, etc.) so 2-col wasted ~half the
       width on padding. 3 cols keeps the grid scannable without making
       any single field too narrow to read. */
    .vars-grid {
      display: grid;
      grid-template-columns: repeat(3, minmax(0, 1fr));
      gap: 10px 8px;
    }

    .var-field { display: flex; flex-direction: column; gap: 3px; min-width: 0; }

    /* The ease field is taller than its row peers (dropdown + 64px curve
       preview underneath), so let it span TWO grid rows. With 3-col
       vars-grid this packs the field beside Position+Duration AND
       Repeat+Yoyo, killing the empty space that was sitting next to the
       visualizer. */
    .var-field-ease { grid-row: span 2; }

    .var-label {
      font-family: var(--mono);
      font-weight: 500;
      font-size: 10.5px;
      color: var(--muted);
      letter-spacing: 0.1em;
      text-transform: uppercase;
    }

    .var-input {
      background: var(--input-bg);
      border: 1px solid var(--border);
      border-radius: 6px;
      color: var(--text);
      font-family: var(--mono);
      font-weight: 500;
      font-size: 11.5px;
      padding: 5px 7px;
      width: 100%;
      min-width: 0;
      transition: all 0.15s;
      box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2);
    }

    .var-input:focus {
      outline: none;
      background: rgba(0, 0, 0, 0.40);
      border-color: var(--accent);
      box-shadow: 0 0 0 3px var(--accent-soft), inset 0 1px 2px rgba(0, 0, 0, 0.2);
    }
    /* Disabled state — visually mute the field and block all input. The
       Stop-frame number field uses this when "None" is checked so users
       can see at a glance that the value won't apply, and they can't
       type into it without first untoggling None. */
    .var-input:disabled,
    .var-input[disabled] {
      opacity: 0.4;
      cursor: not-allowed;
      background: rgba(0, 0, 0, 0.15);
      color: var(--muted);
    }

    /* Custom dropdown (blend-mode field) — hover previews on the
       canvas before click commits. Built as button + absolute-
       positioned list so the popup floats over neighboring fields
       without reflowing the grid. */
    .preview-select-wrap {
      position: relative;
      width: 100%;
    }
    .preview-select-btn {
      text-align: left;
      cursor: pointer;
    }
    .preview-select-btn::after {
      content: ' ▾';
      color: var(--muted);
      font-size: 9px;
    }
    .preview-select-wrap.open .preview-select-btn {
      border-color: var(--accent);
      background: rgba(0, 0, 0, 0.40);
      box-shadow: 0 0 0 3px var(--accent-soft), inset 0 1px 2px rgba(0, 0, 0, 0.2);
    }
    .preview-select-list {
      position: absolute;
      top: calc(100% + 4px);
      left: 0;
      right: 0;
      background: var(--input-bg);
      border: 1px solid var(--border);
      border-radius: 6px;
      box-shadow: 0 6px 16px rgba(0, 0, 0, 0.55);
      z-index: 50;
      max-height: 280px;
      overflow-y: auto;
      padding: 4px 0;
    }
    .preview-select-item {
      padding: 5px 9px;
      font-family: var(--mono);
      font-size: 11px;
      color: var(--text);
      cursor: pointer;
    }
    .preview-select-item:hover,
    .preview-select-item.previewing {
      background: var(--accent-soft);
      color: var(--accent);
    }
    .preview-select-item.active {
      color: var(--accent);
      font-weight: 600;
    }
    .preview-select-item.active::before {
      content: '✓ ';
    }

    .var-input[type="checkbox"] {
      width: auto;
      accent-color: var(--accent);
      cursor: pointer;
    }

    .var-check-wrap {
      display: flex;
      align-items: center;
      gap: 8px;
      padding-top: 4px;
    }

    .yoyo-hint {
      font-family: var(--mono);
      font-size: 10px;
      letter-spacing: 0.1em;
      color: var(--muted);
      text-transform: uppercase;
      transition: color 0.2s;
      line-height: 1.3;
      margin-top: 4px;
    }

    .yoyo-hint.flash { color: var(--accent); }
    /* Repeat-hint variant for the "∞ loops forever" state — readable
       green so the loop intent stands out from the otherwise-muted
       "plays N times" copy. */
    .yoyo-hint.hint-loop {
      color: rgb(170, 240, 200);
      opacity: 0.95;
    }

    /* ── Error ── */
    #error-msg {
      display: none;
      font-family: var(--mono);
      font-weight: 500;
      font-size: 12px;
      color: var(--danger);
      padding: 10px 12px;
      border: 1px solid rgba(255, 107, 107, 0.35);
      border-radius: 8px;
      background: rgba(255, 107, 107, 0.08);
    }

    /* ── Canvas size info ──
       Sits in the .track-editor-header next to the "Timeline tracks"
       title. Compact footnote — smaller font + lower contrast so it
       reads as supplementary metadata rather than a primary label. */
    /* Banner dimensions / layer count / pixel-ratio badge. Now lives
       in the header beside the job-name-display + RETINA chip — quiet
       footnote on the active banner's identity strip. */
    #canvas-info,
    .canvas-info-header {
      font-family: var(--mono);
      font-weight: 500;
      font-size: 9.5px;
      color: var(--muted);
      letter-spacing: 0.08em;
      text-transform: uppercase;
      white-space: nowrap;
      pointer-events: none;
      margin-left: 6px;
    }

    /* ── Export panel ── */
    .export-field { margin-bottom: 14px; }

    .export-label {
      font-family: var(--mono);
      font-weight: 500;
      font-size: 11px;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      color: var(--muted);
      margin-bottom: 6px;
    }

    .export-label-row {
      display: flex;
      align-items: center;
      justify-content: space-between;
      margin-bottom: 6px;
    }

    .export-label-row .export-label { margin-bottom: 0; }

    .export-hint {
      font-family: var(--mono);
      font-size: 11px;
      color: var(--muted);
      letter-spacing: 0.1em;
      line-height: 1.5;
      margin-top: 6px;
    }

    /* Custom-code injection panel — five textareas in their own
       section so the "advanced" surface is visually grouped and
       harder to fat-finger by accident. */
    .custom-code-section {
      /* No border-top here — the master-toggle's border-bottom already
         separates the section from the panel header. (Was needed back
         when this section sat nested inside the Clickthrough panel.) */
      padding-top: 8px;
    }
    .custom-code-header { margin-bottom: 12px; }
    .custom-code-input {
      font-family: var(--mono);
      font-size: 11px;
      line-height: 1.5;
      letter-spacing: 0;
      tab-size: 2;
      white-space: pre;
      overflow-wrap: normal;
      overflow-x: auto;
      resize: vertical;
      min-height: 64px;
      padding: 8px 10px;
    }
    .custom-code-input::placeholder {
      color: rgba(240, 238, 232, 0.28);
      font-style: italic;
    }
    /* Per-slot indicator badges in the Custom code panel's master-toggle.
       Same visual idiom as .layer-badge in the layers list — each pill
       sits dim by default and lights up (yellow accent fill) when its
       slot has content. The .collapsible-section.collapsed selector
       keeps the badges visible after collapse so the header still
       communicates which hooks are populated. */
    .custom-code-toggle-label { flex: 1; text-align: left; }
    .cc-slot-badges {
      display: inline-flex;
      gap: 4px;
      margin-right: 10px;
      flex-wrap: nowrap;
    }
    .cc-slot-badge {
      font-family: var(--mono);
      font-weight: 600;
      font-size: 9.5px;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      padding: 2px 6px;
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.06);
      background: transparent;
      color: rgba(240, 238, 232, 0.22);
      line-height: 1.4;
      transition: color 100ms ease, background 100ms ease, border-color 100ms ease;
    }
    .cc-slot-badge.active {
      color: var(--accent);
      border-color: rgba(245, 200, 66, 0.4);
      background: rgba(245, 200, 66, 0.08);
    }

    /* Preset badge to the LEFT of the slot badges. Color is driven by
       --cc-preset-color set inline per preset (DCM blue, Sizmek amber,
       GAM green, In-page pink). The colored border + tinted background
       give the same visual weight as the slot badges' .active state. */
    .cc-preset-badge {
      font-family: var(--mono);
      font-weight: 700;
      font-size: 9.5px;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      padding: 2px 8px;
      border-radius: 6px;
      line-height: 1.4;
      color: var(--cc-preset-color, var(--accent));
      border: 1px solid var(--cc-preset-color, var(--accent));
      background: rgba(255, 255, 255, 0.04);
      margin-right: 6px;
    }
    .cc-preset-badge[hidden] { display: none; }

    /* Two-column grid that holds the five code editors. Stacks back to
       one column when the panel is narrow so editors stay readable.
       align-items: stretch lets the (shorter) left column expand to
       match the right column's height — combined with pre-init's flex:1
       below, this absorbs the height difference cleanly. */
    /* Runtime-only hint above the custom-code grid. Calls out that
       the slots execute in the exported bundle, not in the editor —
       prevents the "I pasted my pixel and the editor never fires it"
       confusion. Muted yellow-warn tone, not red, since it's not an
       error condition. */
    .custom-code-runtime-notice {
      display: flex;
      align-items: flex-start;
      gap: 8px;
      padding: 10px 12px;
      margin: 0 0 12px 0;
      border-radius: 8px;
      background: rgba(245, 200, 66, 0.06);
      border: 1px solid rgba(245, 200, 66, 0.22);
      color: var(--muted);
      font-size: 11px;
      line-height: 1.5;
    }
    .custom-code-runtime-notice svg {
      flex-shrink: 0;
      margin-top: 1px;
      color: rgba(245, 200, 66, 0.8);
    }
    .custom-code-runtime-notice strong {
      color: var(--text);
      font-weight: 600;
    }
    .custom-code-runtime-notice code {
      font-family: var(--mono);
      font-size: 10px;
      background: rgba(255, 255, 255, 0.06);
      padding: 1px 4px;
      border-radius: 3px;
    }
    .custom-code-grid {
      display: grid;
      grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
      gap: 16px;
      align-items: stretch;
    }
    .custom-code-col {
      display: flex;
      flex-direction: column;
      gap: 12px;
      min-width: 0;
    }
    @media (max-width: 960px) {
      .custom-code-grid { grid-template-columns: minmax(0, 1fr); }
    }

    /* Empty-clickTag warning shown after a network preset is applied
       while the URL field is still blank. Yellow-tinted to read as a
       caution rather than an error — the banner still exports, the
       user just won't have a fallback destination locally. */
    .clicktag-empty-warning {
      margin-top: 6px;
      padding: 8px 10px;
      border: 1px solid rgba(245, 215, 126, 0.4);
      background: rgba(245, 215, 126, 0.08);
      border-radius: 6px;
      color: rgba(245, 215, 126, 0.9);
      line-height: 1.5;
    }
    .clicktag-empty-warning[hidden] { display: none; }
    .clicktag-empty-warning code {
      background: rgba(245, 215, 126, 0.12);
      border-color: rgba(245, 215, 126, 0.3);
      color: inherit;
    }

    /* Network-preset dropdown row that sits above the five code editors. */
    .code-preset-row {
      padding-bottom: 4px;
      margin-bottom: 12px;
      border-bottom: 1px dashed var(--border);
    }
    .code-preset-controls {
      display: flex;
      gap: 8px;
      align-items: stretch;
      flex-wrap: wrap;
    }
    .code-preset-select {
      flex: 1 1 240px;
      min-width: 0;
    }
    .code-preset-desc {
      margin-top: 6px;
      min-height: 1.4em;
    }
    /* Compact, equal-sized action buttons in the preset row. Override
       .preset-io-btn's default block/100% width and bigger padding so
       Apply / Save / Import all read as a tidy strip next to the
       dropdown rather than three full-width stacked bars. */
    .cc-row-btn {
      display: inline-flex !important;
      align-items: center;
      justify-content: center;
      width: auto !important;
      flex: 0 0 auto;
      padding: 4px 12px !important;
      margin: 0 !important;
      font-size: 11px !important;
      letter-spacing: 0.08em;
      text-align: center !important;
      line-height: 1.4;
      white-space: nowrap;
    }
    /* CodeMirror 5 (material-darker) overrides — match the panel's chrome
       so the editor doesn't look like a stray VS Code window dropped on
       the page. The native textarea sits hidden behind .CodeMirror. */
    .custom-code-section .CodeMirror {
      font-family: var(--mono);
      font-size: 11px;
      line-height: 1.5;
      height: auto;
      min-height: 64px;
      /* Cap at ~5 lines (5 × 1.5em line-height + 8px vertical padding).
         Beyond that, CodeMirror's built-in scroll-container scrolls
         internally so the panel doesn't keep growing vertically. */
      max-height: calc(5 * 1.5em + 8px);
      border: 1px solid var(--border);
      border-radius: 6px;
      padding: 4px 0;
    }
    .custom-code-section .CodeMirror-scroll {
      min-height: 56px;
      max-height: calc(5 * 1.5em + 8px);
    }
    /* Click handler gets a 6-line cap (one taller than the default 5)
       — typical click handlers are SDK-exit + window.open fallback,
       which barely fits in 5 lines. */
    #export-cc-clickhandler + .CodeMirror,
    #export-cc-clickhandler + .CodeMirror .CodeMirror-scroll {
      max-height: calc(6 * 1.5em + 8px);
    }

    /* Pre-init JS sits at the bottom of the left column, which has only
       2 editors vs the right column's 3. Letting pre-init absorb the
       leftover vertical space (instead of capping at 5 lines) keeps the
       two columns visually balanced — typical pre-init content (SDK
       init, polite-load gating) benefits from the extra room anyway.
       The .export-field wrapping pre-init becomes a flex container so
       the CodeMirror inside stretches to fill remaining height. */
    .custom-code-col:first-child {
      align-items: stretch;
    }
    .custom-code-col:first-child .export-field:last-child {
      flex: 1 1 auto;
      display: flex;
      flex-direction: column;
      min-height: 0;
    }
    .custom-code-col:first-child .export-field:last-child > .CodeMirror,
    .custom-code-col:first-child .export-field:last-child > .CodeMirror + textarea + .CodeMirror,
    #export-cc-preinit + .CodeMirror {
      flex: 1 1 auto;
      height: 100%;
      max-height: none;
      /* At least the original 8-line floor so the editor isn't tiny when
         the right column is short; grows beyond that to match the right
         column's full height. */
      min-height: calc(8 * 1.5em + 8px);
    }
    #export-cc-preinit + .CodeMirror .CodeMirror-scroll {
      height: 100%;
      max-height: none;
      min-height: calc(8 * 1.5em + 8px);
    }
    .custom-code-section .CodeMirror-placeholder {
      color: rgba(240, 238, 232, 0.28) !important;
      font-style: italic;
    }

    .export-hint code {
      background: var(--input-bg);
      border: 1px solid var(--border);
      border-radius: 6px;
      padding: 1px 5px;
      font-size: 0.92em;
      color: var(--accent);
      font-family: var(--mono);
    }

    .export-mini-btn {
      background: var(--btn-bg);
      color: var(--accent);
      border: 1px solid rgba(245, 200, 66, 0.4);
      padding: 4px 9px;
      font-family: var(--mono);
      font-weight: 500;
      font-size: 10.5px;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      border-radius: 6px;
      cursor: pointer;
      transition: background 0.12s;
    }

    .export-mini-btn:hover { background: rgba(245, 200, 66, 0.10); }

    .export-params-list {
      display: flex;
      flex-direction: column;
      gap: 6px;
    }

    .export-param-row {
      display: grid;
      grid-template-columns: 1fr 1fr auto;
      gap: 6px;
    }

    .export-param-row .var-input {
      font-size: 0.65rem;
    }

    .export-remove-btn {
      width: 26px;
      height: 26px;
      background: var(--btn-bg);
      color: var(--muted);
      border: 1px solid var(--border);
      border-radius: 6px;
      cursor: pointer;
      font-family: var(--mono);
      font-size: 14px;
      line-height: 1;
      padding: 0;
      align-self: center;
      transition: all 0.12s;
    }

    .export-remove-btn:hover {
      color: var(--danger);
      border-color: rgba(255, 107, 107, 0.5);
      background: rgba(255, 107, 107, 0.08);
    }

    .export-final-url {
      font-family: var(--mono);
      font-size: 11px;
      color: var(--accent);
      letter-spacing: 0.1em;
      word-break: break-all;
      padding: 8px 10px;
      background: var(--input-bg);
      border: 1px solid var(--border);
      border-radius: 6px;
      line-height: 1.45;
      max-height: 80px;
      overflow-y: auto;
    }

    .export-final-url.empty { color: var(--muted); }

    .export-divider {
      height: 1px;
      background: var(--border);
      margin: 14px 0 14px;
    }

    .export-checkbox-label {
      display: flex;
      align-items: center;
      gap: 8px;
      font-family: var(--mono);
      font-weight: 500;
      font-size: 12px;
      color: var(--text);
      letter-spacing: 0.1em;
      cursor: pointer;
      user-select: none;
    }

    .export-checkbox-label input { accent-color: var(--accent); cursor: pointer; }

    /* Tight inline variant used in the Global-settings rows
       (Auto / None toggles next to the length + stop-frame inputs). */
    .export-checkbox-label.export-inline-toggle {
      gap: 6px;
      font-size: 0.55rem;
      letter-spacing: 0.08em;
      color: var(--muted);
    }

    /* Match-animations button + Auto toggle sit on one row, right of label. */
    .export-tl-controls {
      display: flex;
      align-items: center;
      gap: 10px;
    }

    /* ── Canvas settings (banner bg + border) ──
       Two-column layout so Background, Border, and Border-Width pack into
       a tighter ~half-panel grid instead of stacking the full width. */
    /* Project-info chip strip at the top of the Project Settings
       panel — read-only summary of size / layers / pixel ratio.
       Replaces the old canvas-info-header strip in the top toolbar. */
    .project-info-strip {
      display: flex;
      flex-wrap: wrap;
      gap: 6px;
      margin: 0 0 14px;
    }
    .project-info-chip {
      display: inline-flex;
      flex-direction: column;
      gap: 2px;
      padding: 7px 10px;
      background: rgba(255, 255, 255, 0.03);
      border: 1px solid var(--border, rgba(255, 255, 255, 0.08));
      border-radius: 8px;
      min-width: 0;
    }
    .project-info-chip[hidden] { display: none; }
    .project-info-chip-label {
      font-family: var(--mono);
      font-size: 9px;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      color: var(--muted);
    }
    .project-info-chip-value {
      font-family: var(--mono);
      font-size: 11px;
      color: var(--text);
      font-weight: 600;
      letter-spacing: 0.02em;
    }
    /* Retina chip gets a yellow accent so it stays scannable as the
       "this project came from a 2x source" tell — matches the
       semantics of the old RETINA pill in the header. */
    .project-info-chip-retina {
      border-color: rgba(245, 200, 66, 0.35);
      background: rgba(245, 200, 66, 0.08);
    }
    .project-info-chip-retina .project-info-chip-value {
      color: var(--accent, #f5c842);
    }

    .canvas-settings-grid {
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: 10px 8px;
    }
    /* Background field includes the transparent toggle below it, so
       give it the full row to keep the toggle visually grouped. */
    .canvas-settings-grid > .var-field:first-child {
      grid-column: 1 / -1;
    }

    .color-row {
      display: flex;
      align-items: center;
      gap: 8px;
    }

    .color-swatch {
      width: 32px;
      height: 28px;
      padding: 0;
      border: 1px solid var(--border);
      border-radius: 8px;
      background: transparent;
      cursor: pointer;
      flex-shrink: 0;
    }

    .color-swatch::-webkit-color-swatch-wrapper { padding: 2px; }
    .color-swatch::-webkit-color-swatch { border: none; border-radius: 6px; }
    .color-swatch::-moz-color-swatch { border: none; border-radius: 6px; }

    .color-hex {
      flex: 1;
      text-transform: uppercase;
      font-size: 0.7rem;
    }

    .color-transparent {
      display: flex;
      align-items: center;
      gap: 6px;
      font-family: var(--mono);
      font-size: 0.6rem;
      color: var(--muted);
      letter-spacing: 0.1em;
      text-transform: uppercase;
      cursor: pointer;
      margin-top: 6px;
    }

    .color-transparent input { accent-color: var(--accent); }

    /* Empty-state intro screen — shown when no manifest is loaded.
       Two-column centered card; reparented Upload + Library sections
       fill the columns. Body data-app-state="empty" drives the
       hide-the-editor rules below. */
    .intro-screen {
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 48px 24px;
      min-height: calc(100vh - 90px);    /* leave room for header */
      box-sizing: border-box;
      width: 100%;
    }
    .intro-screen[hidden] { display: none; }
    .intro-card {
      width: 100%;
      max-width: 720px;
    }

    /* Tab strip — sits above the active panel. Each tab has a label
       row + a small description under it, so the user understands
       what the tab will surface without a separate page-level title. */
    .intro-tabs {
      display: flex;
      gap: 0;
      margin-bottom: -1px;  /* overlap the panel's top border */
      position: relative;
      z-index: 1;
    }
    .intro-tab {
      flex: 1;
      display: flex;
      flex-direction: column;
      align-items: flex-start;
      gap: 3px;
      padding: 14px 18px 16px;
      background: rgba(20, 18, 22, 0.32);
      border: 1px solid var(--border, rgba(255, 255, 255, 0.08));
      border-bottom: 1px solid transparent;
      border-top-left-radius: 12px;
      border-top-right-radius: 12px;
      cursor: pointer;
      color: var(--muted);
      font-family: var(--sans);
      transition: background 0.15s, color 0.15s, border-color 0.15s;
    }
    .intro-tab + .intro-tab {
      margin-left: 6px;
    }
    .intro-tab:hover {
      background: rgba(20, 18, 22, 0.55);
      color: var(--text);
    }
    .intro-tab.is-active {
      background: rgba(20, 18, 22, 0.55);
      color: var(--text);
      border-color: var(--border, rgba(255, 255, 255, 0.08));
      border-bottom-color: transparent;
    }
    .intro-tab-label {
      font-size: 15px;
      font-weight: 600;
      letter-spacing: -0.005em;
      display: inline-flex;
      align-items: center;
      gap: 8px;
    }
    .intro-tab-count {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      min-width: 22px;
      height: 20px;
      padding: 0 7px;
      box-sizing: border-box;
      font-family: var(--mono);
      font-size: 11px;
      font-weight: 600;
      letter-spacing: 0.02em;
      border-radius: 999px;
      background: rgba(245, 200, 66, 0.18);
      color: var(--accent, #f5c842);
      border: 1px solid rgba(245, 200, 66, 0.35);
    }
    .intro-tab-count[hidden] { display: none; }
    .intro-tab-desc {
      font-size: 12px;
      line-height: 1.3;
      color: var(--muted);
      font-weight: 400;
    }
    .intro-tab.is-active .intro-tab-desc {
      color: var(--muted);
    }

    .intro-tab-panels {
      position: relative;
    }
    .intro-col {
      padding: 28px 28px 24px;
      background: rgba(20, 18, 22, 0.55);
      backdrop-filter: var(--glass-blur, blur(6px));
      -webkit-backdrop-filter: var(--glass-blur, blur(6px));
      border: 1px solid var(--border, rgba(255, 255, 255, 0.08));
      border-radius: 14px;
      border-top-left-radius: 0;  /* connects to the active tab above */
      box-shadow: 0 18px 40px -20px rgba(0, 0, 0, 0.45);
      min-height: 460px;
      display: flex;
      flex-direction: column;
    }
    .intro-col[hidden] { display: none; }
    /* Reparented panel-sections inside the intro lose the sidebar's
       chrome (collapse toggle label, divider styling) and take the
       full column. The existing internal markup (dropzone, library
       list, workspace info) is what we want to display. */
    .intro-col .panel-section {
      margin: 0;
      padding: 0;
      border: 0;
      background: transparent;
    }
    .intro-col .panel-section + .panel-section {
      margin-top: 16px;
      padding-top: 16px;
      border-top: 1px solid var(--border, rgba(255, 255, 255, 0.08));
    }
    /* Hide the section's collapse-toggle label — we already have the
       intro column titles above. */
    .intro-col .panel-section > .panel-label-toggle {
      display: none;
    }
    /* The Upload + Library panel-sections are .collapsible-section
       and share a localStorage-backed collapsed state with the
       sidebar — if the user collapsed them in the editor, the same
       state would hide them inside the intro tab and leave the New
       project / Saved jobs tabs visually empty. Force the content
       visible whenever a section lives inside the intro card,
       regardless of collapse state. */
    .intro-col .collapsible-section.collapsed .panel-content {
      display: block !important;
    }
    /* Editor chrome visibility — hide the entire <main> editor
       region while the intro screen owns the viewport. Intro lives
       OUTSIDE <main> (sibling, between header and main) so this
       rule doesn't touch it. Reparented Upload + Library DOM lives
       inside the intro and is visible regardless. */
    body[data-app-state="empty"] main {
      display: none !important;
    }
    /* "Save to your library" button is meaningless when nothing is
       loaded — hide it (and its sibling hint span) in the empty
       state so the intro's Library column reads as a "what you've
       saved before" list, not a save-action surface. */
    body[data-app-state="empty"] #library-save-btn,
    body[data-app-state="empty"] #library-save-hint {
      display: none !important;
    }

    /* Responsive: narrow viewports — tabs go full-width stacked, panel
       height collapses to fit content. */
    @media (max-width: 880px) {
      .intro-card { max-width: 100%; }
      .intro-col { min-height: 0; }
      .intro-tab-desc { display: none; }
    }

    /* ── Sign-in wall ─────────────────────────────────────────────────
       Full-viewport auth gate shown when LAYRD_AUTH_REQUIRED is true
       and no Supabase session exists. Replaces the old anonymous-mode
       experience: there is no editor surface without an account.

       Visibility model: body[data-auth-state="signed-out"] hides the
       header/main/intro and shows .signin-wall; once signed in, the
       wall sets its own hidden attribute and the editor renders
       normally. */
    .signin-wall {
      position: fixed;
      inset: 0;
      z-index: 9000;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 32px;
      box-sizing: border-box;
      background: #0a0a0c;
      overflow: auto;
    }
    .signin-wall[hidden] { display: none; }

    /* Atmospheric backdrop — soft radial gradients in the LAYRD palette
       (yellow + coral + lime) so the gate feels like the product, not
       a blank lock screen. Pure CSS, no images. */
    .signin-wall-bg {
      position: absolute;
      inset: 0;
      pointer-events: none;
      background:
        radial-gradient(circle at 18% 22%, rgba(245, 200, 66, 0.18), transparent 42%),
        radial-gradient(circle at 82% 78%, rgba(255, 122, 122, 0.14), transparent 45%),
        radial-gradient(circle at 50% 110%, rgba(192, 241, 53, 0.10), transparent 55%);
      filter: blur(4px);
    }

    .signin-card {
      position: relative;
      width: 100%;
      max-width: 420px;
      padding: 36px 32px 28px;
      background: rgba(20, 18, 22, 0.78);
      backdrop-filter: blur(14px);
      -webkit-backdrop-filter: blur(14px);
      border: 1px solid rgba(255, 255, 255, 0.08);
      border-radius: 18px;
      box-shadow:
        0 30px 70px -20px rgba(0, 0, 0, 0.7),
        0 2px 0 0 rgba(255, 255, 255, 0.04) inset;
    }

    .signin-brand {
      display: flex;
      align-items: center;
      margin-bottom: 28px;
    }
    /* Full LAYRD wordmark + mark lockup — same SVG the header uses.
       Source aspect ratio is ~685×173 (≈4:1); pinning height keeps
       the wall card layout stable and lets width auto-scale. */
    .signin-brand-lockup {
      display: block;
      height: 40px;
      width: auto;
      max-width: 100%;
      /* The asset bakes in the dark rounded-square background of the
         layer-bar mark. On the wall's near-black bg that square reads
         as nearly invisible — fine, the colored bars + wordmark are
         what carry the brand. A subtle drop shadow makes the wordmark
         lift off the gradient backdrop. */
      filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.5));
    }

    .signin-title {
      font-family: var(--sans);
      font-size: 22px;
      font-weight: 600;
      line-height: 1.25;
      letter-spacing: -0.01em;
      color: var(--text);
      margin: 0 0 10px;
    }
    .signin-sub {
      font-family: var(--sans);
      font-size: 13px;
      line-height: 1.5;
      color: var(--muted);
      margin: 0 0 22px;
    }

    .signin-label {
      display: block;
      font-family: var(--mono);
      font-size: 10px;
      letter-spacing: 0.12em;
      text-transform: uppercase;
      color: var(--muted);
      margin-bottom: 6px;
    }
    .signin-input {
      width: 100%;
      box-sizing: border-box;
      padding: 11px 14px;
      font-family: var(--sans);
      font-size: 14px;
      color: var(--text);
      background: rgba(255, 255, 255, 0.04);
      border: 1px solid rgba(255, 255, 255, 0.12);
      border-radius: 10px;
      outline: none;
      transition: border-color 0.15s, background 0.15s;
    }
    .signin-input::placeholder { color: rgba(255, 255, 255, 0.28); }
    .signin-input:focus {
      border-color: var(--accent, #f5c842);
      background: rgba(255, 255, 255, 0.06);
    }

    .signin-hint {
      font-family: var(--sans);
      font-size: 12px;
      line-height: 1.45;
      color: var(--muted);
      margin: 10px 0 16px;
      min-height: 18px;
    }
    .signin-hint.is-success { color: var(--accent, #f5c842); }
    .signin-hint.is-error   { color: #ff7a7a; }

    .signin-send-btn {
      width: 100%;
      padding: 12px 16px;
      font-family: var(--sans);
      font-size: 14px;
      font-weight: 600;
      color: #2a1d00;
      background: linear-gradient(135deg, #ffe27a, #f5c842);
      border: 0;
      border-radius: 10px;
      cursor: pointer;
      transition: transform 0.12s, box-shadow 0.15s, opacity 0.15s;
      box-shadow: 0 8px 24px -8px rgba(245, 200, 66, 0.55);
    }
    .signin-send-btn:hover { transform: translateY(-1px); }
    .signin-send-btn:active { transform: translateY(0); }
    .signin-send-btn:disabled {
      opacity: 0.6;
      cursor: progress;
      transform: none;
    }

    /* "—— or ——" divider between the magic-link CTA and the Google
       OAuth button. Single line via flex with the "or" text sandwiched
       between two pseudo-element rules. */
    .signin-divider {
      display: flex;
      align-items: center;
      gap: 12px;
      margin: 18px 0 14px;
      font-family: var(--mono);
      font-size: 9.5px;
      letter-spacing: 0.14em;
      text-transform: uppercase;
      color: var(--muted);
    }
    .signin-divider::before,
    .signin-divider::after {
      content: '';
      flex: 1;
      height: 1px;
      background: rgba(255, 255, 255, 0.10);
    }

    /* "Continue with Google" button — neutral white-on-dark chip with
       the multicolor G logo. Distinct from the yellow magic-link CTA
       so the two options read as parallel choices, not a primary +
       secondary hierarchy. */
    .signin-google-btn {
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 10px;
      width: 100%;
      padding: 11px 16px;
      font-family: var(--sans);
      font-size: 13.5px;
      font-weight: 600;
      color: var(--text);
      background: rgba(255, 255, 255, 0.04);
      border: 1px solid rgba(255, 255, 255, 0.14);
      border-radius: 10px;
      cursor: pointer;
      transition: background 0.15s ease, border-color 0.15s ease,
                  transform 0.12s ease;
    }
    .signin-google-btn:hover {
      background: rgba(255, 255, 255, 0.08);
      border-color: rgba(255, 255, 255, 0.22);
      transform: translateY(-1px);
    }
    .signin-google-btn:active { transform: translateY(0); }
    .signin-google-btn:disabled {
      opacity: 0.65;
      cursor: progress;
      transform: none;
    }
    .signin-google-icon {
      flex-shrink: 0;
      display: block;
    }

    .signin-footer {
      font-family: var(--sans);
      font-size: 11px;
      line-height: 1.5;
      color: var(--muted);
      text-align: center;
      margin: 18px 0 0;
    }

    /* Hide-everything-else until auth confirms. The CSS gate is
       inverted from "hide when signed-out" to "hide UNLESS signed-in"
       so the editor never paints on first load — the body's
       data-auth-state attribute starts as either "signed-in",
       "signed-out", or "signing-in" (set by the pre-paint script at
       the top of <body>). Only the explicit signed-in state reveals
       the editor; anything else keeps it hidden. This eliminates the
       editor flash on a quick refresh.

       The wall is shown by default (no auth state required) and
       hidden ONLY when signed-in. The hidden=hidden attribute on the
       wall element is also lifted via the CSS rule below — JS no
       longer needs to manage wall.hidden, the CSS handles it. */
    body:not([data-auth-state="signed-in"]) > header,
    body:not([data-auth-state="signed-in"]) > main,
    body:not([data-auth-state="signed-in"]) > .intro-screen,
    body:not([data-auth-state="signed-in"]) > footer {
      display: none !important;
    }
    /* Wall visibility — default visible, hidden only when signed-in.
       Beats the inline hidden attribute via !important since the
       attribute is now decorative; the CSS state machine is the
       source of truth. */
    body:not([data-auth-state="signed-in"]) .signin-wall {
      display: flex !important;
    }
    body[data-auth-state="signed-in"] .signin-wall {
      display: none !important;
    }

    /* Full-screen browser block — covers the entire viewport when
       the capability check finds critical missing APIs. NOT
       dismissable: the user has to switch browsers to proceed. The
       app DOM stays mounted underneath (so refresh-with-state isn't
       lost) but is visually + interactively covered. Centered card
       with clear hierarchy: icon → title → lede → missing list →
       recommended browsers → context paragraph. */
    .browser-block {
      position: fixed;
      inset: 0;
      z-index: 100000;
      background: rgba(10, 10, 12, 0.96);
      backdrop-filter: blur(8px);
      -webkit-backdrop-filter: blur(8px);
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 32px 24px;
      overflow-y: auto;
      animation: browser-block-fade 0.25s ease-out;
    }
    .browser-block[hidden] { display: none; }
    @keyframes browser-block-fade {
      from { opacity: 0; }
      to   { opacity: 1; }
    }
    .browser-block-card {
      width: clamp(320px, 90vw, 560px);
      padding: 36px 40px 32px;
      background: rgba(24, 22, 28, 0.95);
      border: 1px solid rgba(229, 75, 75, 0.35);
      border-radius: 16px;
      box-shadow:
        0 30px 80px -20px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.04);
      color: var(--text);
      font-family: var(--sans);
      animation: browser-block-pop 0.28s cubic-bezier(0.16, 1, 0.3, 1);
    }
    @keyframes browser-block-pop {
      from { opacity: 0; transform: translateY(8px) scale(0.98); }
      to   { opacity: 1; transform: translateY(0)   scale(1);    }
    }
    .browser-block-icon {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 48px;
      height: 48px;
      border-radius: 50%;
      background: rgba(229, 75, 75, 0.18);
      color: #ff8a8a;
      font-size: 22px;
      margin-bottom: 18px;
    }
    .browser-block-title {
      font-family: var(--sans);
      font-size: 22px;
      font-weight: 600;
      line-height: 1.2;
      letter-spacing: -0.01em;
      margin: 0 0 8px;
      color: var(--text);
    }
    .browser-block-lede {
      font-size: 14px;
      line-height: 1.5;
      color: var(--muted);
      margin: 0 0 20px;
    }
    .browser-block-missing {
      padding: 14px 16px;
      background: rgba(229, 75, 75, 0.08);
      border: 1px solid rgba(229, 75, 75, 0.2);
      border-radius: 8px;
      margin-bottom: 20px;
    }
    .browser-block-missing-label {
      font-family: var(--mono);
      font-size: 11px;
      font-weight: 600;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      color: #ff8a8a;
      margin-bottom: 8px;
    }
    .browser-block-missing ul {
      margin: 0;
      padding: 0;
      list-style: none;
    }
    .browser-block-missing li {
      font-family: var(--mono);
      font-size: 12px;
      line-height: 1.5;
      color: var(--text);
      padding: 3px 0 3px 16px;
      position: relative;
    }
    .browser-block-missing li::before {
      content: '×';
      position: absolute;
      left: 0;
      color: #ff8a8a;
      font-weight: 600;
    }
    .browser-block-rec {
      padding: 16px;
      background: rgba(245, 200, 66, 0.06);
      border: 1px solid rgba(245, 200, 66, 0.25);
      border-radius: 8px;
      margin-bottom: 20px;
    }
    .browser-block-rec-title {
      font-family: var(--mono);
      font-size: 11px;
      font-weight: 600;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      color: var(--accent, #f5c842);
      margin-bottom: 10px;
    }
    .browser-block-rec-list {
      margin: 0;
      padding: 0;
      list-style: none;
      display: flex;
      flex-direction: column;
      gap: 6px;
    }
    .browser-block-rec-list li {
      font-size: 13px;
      color: var(--text);
      line-height: 1.5;
      padding-left: 18px;
      position: relative;
    }
    .browser-block-rec-list li::before {
      content: '✓';
      position: absolute;
      left: 0;
      color: var(--accent, #f5c842);
      font-weight: 600;
    }
    .browser-block-rec-list strong {
      color: var(--text);
      font-weight: 600;
    }
    .browser-block-foot {
      font-size: 12px;
      line-height: 1.55;
      color: var(--muted);
      font-style: italic;
      padding-top: 16px;
      border-top: 1px solid rgba(255, 255, 255, 0.08);
    }

    /* Browser-support banner — sits at the very top of the document
       (above <header>) when the editor detects missing browser
       capabilities. Two visual states:
         .is-critical → red-ish accent: editor probably can't work right
         .is-warning  → amber accent: specific feature degraded, mostly OK
       Dismissed per issue-signature; reappears if the issue list
       changes (browser upgrade brings new gaps, etc.). */
    .browser-support-banner {
      display: grid;
      grid-template-columns: 24px 1fr auto;
      gap: 12px;
      align-items: center;
      padding: 10px 18px;
      background: rgba(245, 200, 66, 0.10);
      border-bottom: 1px solid rgba(245, 200, 66, 0.4);
      color: var(--text);
      font-family: var(--mono);
    }
    .browser-support-banner[hidden] { display: none; }
    .browser-support-banner.is-critical {
      background: rgba(229, 75, 75, 0.12);
      border-bottom-color: rgba(229, 75, 75, 0.5);
    }
    .browser-support-icon {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 24px;
      height: 24px;
      border-radius: 50%;
      background: rgba(245, 200, 66, 0.25);
      color: var(--accent, #f5c842);
      font-weight: 600;
      font-size: 14px;
    }
    .browser-support-icon::before { content: '!'; }
    .browser-support-banner.is-critical .browser-support-icon {
      background: rgba(229, 75, 75, 0.25);
      color: #ff8a8a;
    }
    .browser-support-body { min-width: 0; }
    .browser-support-title {
      font-size: 12px;
      font-weight: 600;
      letter-spacing: 0.06em;
      color: var(--text);
      margin-bottom: 2px;
      text-transform: uppercase;
    }
    .browser-support-detail {
      font-size: 11px;
      color: var(--muted);
      line-height: 1.45;
      letter-spacing: 0.02em;
    }
    .browser-support-dismiss {
      background: transparent;
      border: 0;
      color: var(--muted);
      font-size: 20px;
      line-height: 1;
      cursor: pointer;
      padding: 4px 8px;
      border-radius: 4px;
      transition: color 0.12s, background 0.12s;
    }
    .browser-support-dismiss:hover {
      color: var(--text);
      background: rgba(255, 255, 255, 0.06);
    }

    /* Crash-recovery banner — sits atop the upload panel when a
       recent autosave exists. Yellow accent because it's an
       opportunity (not a warning) to recover work, and the dismiss
       × clears the autosave entirely. */
    .autosave-recover-banner {
      display: grid;
      grid-template-columns: 28px 1fr auto;
      gap: 10px;
      align-items: start;
      padding: 10px 12px;
      margin-bottom: 14px;
      background: rgba(245, 200, 66, 0.08);
      border: 1px solid rgba(245, 200, 66, 0.32);
      border-radius: 8px;
    }
    .autosave-recover-banner[hidden] { display: none; }
    .autosave-recover-icon {
      font-size: 18px;
      line-height: 1;
      color: var(--accent);
      margin-top: 2px;
    }
    .autosave-recover-title {
      font-family: var(--mono);
      font-size: 12px;
      color: var(--text);
      letter-spacing: 0.05em;
      font-weight: 600;
      margin-bottom: 2px;
    }
    .autosave-recover-meta {
      font-family: var(--mono);
      font-size: 11px;
      color: var(--accent);
      letter-spacing: 0.04em;
      margin-bottom: 4px;
    }
    .autosave-recover-hint {
      font-family: var(--mono);
      font-size: 11px;
      color: var(--muted);
      line-height: 1.4;
    }
    .autosave-recover-dismiss {
      background: transparent;
      border: 0;
      color: var(--muted);
      font-size: 18px;
      line-height: 1;
      cursor: pointer;
      padding: 2px 6px;
      border-radius: 4px;
    }
    .autosave-recover-dismiss:hover {
      color: var(--text);
      background: rgba(255, 255, 255, 0.06);
    }

    /* ── Upload section / load bundle button ── */
    .upload-divider {
      text-align: center;
      font-family: var(--mono);
      font-size: 11px;
      color: var(--muted);
      letter-spacing: 0.1em;
      text-transform: uppercase;
      margin: 16px 0 12px;
      position: relative;
    }

    .upload-divider::before,
    .upload-divider::after {
      content: '';
      position: absolute;
      top: 50%;
      width: 30%;
      height: 1px;
      background: var(--border);
    }
    .upload-divider::before { left: 0; }
    .upload-divider::after  { right: 0; }

    .upload-load-btn {
      /* Sized to match the header's .header-btn chip (Export ad et al.):
         32px tall, tight horizontal padding, 11.5px monospace caps with
         0.08em tracking. Sidebar still spans full width because the
         column is narrow — only the height/typography are mirrored. */
      width: 100%;
      height: 32px;
      padding: 0 10px;
      line-height: 1;
      background: var(--btn-bg);
      color: var(--text);
      border: 1px solid rgba(255, 255, 255, 0.08);
      border-radius: 8px;
      font-family: var(--mono);
      font-weight: 500;
      font-size: 11.5px;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      white-space: nowrap;
      cursor: pointer;
      transition: all 0.12s;
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
      backdrop-filter: blur(12px);
      -webkit-backdrop-filter: blur(12px);
    }

    .upload-load-btn:hover {
      background: rgba(255, 255, 255, 0.08);
      border-color: rgba(245, 200, 66, 0.4);
      color: var(--accent);
    }

    /* ── Library list (per-user saved jobs) ── */
    .library-count {
      font-family: var(--mono);
      font-size: 11px;
      color: var(--muted);
      margin-left: auto;
      padding-left: 8px;
    }
    .library-list {
      display: flex;
      flex-direction: column;
      gap: 6px;
      /* The outer list lets the toolbar (sort dropdown anchor) sit
         outside the scroll boundary so the dropdown can hang past
         the bottom edge without being clipped. The inner
         .library-scroll wrapper actually carries the height cap. */
      overflow: visible;
    }

    /* ─── Sidebar "recents" view ──────────────────────────────────
       Replaces the cramped folder tree that used to live in the
       sidebar. Shows the 5 most-recently-updated jobs only — full
       browse lives in the saved-jobs modal opened via "Browse all"
       or File > Open. */
    .library-recent-list {
      display: flex;
      flex-direction: column;
      gap: 4px;
      margin: 8px 0 0;
    }
    .library-recent-empty {
      font-family: var(--mono);
      font-size: 10.5px;
      letter-spacing: 0.04em;
      color: var(--muted);
      padding: 14px 6px;
      text-align: center;
    }
    .library-browse-all-btn {
      display: flex;
      align-items: center;
      justify-content: space-between;
      width: 100%;
      margin-top: 10px;
      padding: 9px 12px;
      font-family: var(--mono);
      font-size: 10.5px;
      font-weight: 600;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      color: var(--text);
      background: rgba(255, 255, 255, 0.03);
      border: 1px solid rgba(255, 255, 255, 0.10);
      border-radius: 8px;
      cursor: pointer;
      transition: all 0.12s ease;
    }
    .library-browse-all-btn:hover {
      color: var(--accent, #f5c842);
      background: rgba(245, 200, 66, 0.08);
      border-color: rgba(245, 200, 66, 0.35);
    }
    .library-browse-arrow {
      font-size: 14px;
      transition: transform 0.15s ease;
    }
    .library-browse-all-btn:hover .library-browse-arrow {
      transform: translateX(3px);
    }

    /* ─── Saved-jobs modal ───────────────────────────────────────
       Full library browse surface — folders, sort, search. Opens
       via the sidebar's "Browse all" button or File > Open (⌘O). */
    .saved-jobs-modal {
      position: fixed;
      inset: 0;
      z-index: 8500;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 24px;
      box-sizing: border-box;
    }
    .saved-jobs-modal[hidden] { display: none; }
    .saved-jobs-modal-backdrop {
      position: absolute;
      inset: 0;
      background: rgba(8, 6, 10, 0.65);
      backdrop-filter: blur(6px);
      -webkit-backdrop-filter: blur(6px);
    }
    .saved-jobs-modal-card {
      position: relative;
      width: 100%;
      max-width: 720px;
      max-height: 82vh;
      display: flex;
      flex-direction: column;
      background: rgba(20, 18, 22, 0.92);
      backdrop-filter: blur(14px);
      -webkit-backdrop-filter: blur(14px);
      border: 1px solid rgba(255, 255, 255, 0.10);
      border-radius: 16px;
      box-shadow:
        0 30px 70px -20px rgba(0, 0, 0, 0.7),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
      overflow: hidden;
    }
    .saved-jobs-modal-header {
      display: flex;
      align-items: flex-start;
      justify-content: space-between;
      gap: 16px;
      padding: 20px 22px 14px;
      border-bottom: 1px solid rgba(255, 255, 255, 0.06);
    }
    .saved-jobs-modal-title {
      font-family: var(--sans);
      font-size: 18px;
      font-weight: 600;
      letter-spacing: -0.01em;
      color: var(--text);
      margin: 0 0 4px;
    }
    .saved-jobs-modal-sub {
      font-family: var(--sans);
      font-size: 12.5px;
      color: var(--muted);
      margin: 0;
    }
    .saved-jobs-modal-close {
      width: 30px;
      height: 30px;
      padding: 0;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      font-size: 14px;
      color: var(--muted);
      background: transparent;
      border: 0;
      border-radius: 6px;
      cursor: pointer;
      flex-shrink: 0;
      transition: color 0.12s, background 0.12s;
    }
    .saved-jobs-modal-close:hover {
      color: var(--text);
      background: rgba(255, 255, 255, 0.06);
    }
    .saved-jobs-modal-toolbar {
      padding: 14px 22px 8px;
    }
    /* When the library-list is reparented into the modal, give it
       room to scroll inside the card's max-height. The toolbar above
       (filter) stays sticky-ish via the card's flex layout. */
    .saved-jobs-modal-card > .library-list {
      flex: 1;
      min-height: 0;
      padding: 6px 22px 22px;
      overflow: visible;
    }
    .saved-jobs-modal-card .library-scroll {
      max-height: none;
    }
    .intro-col .library-list { padding-right: 0; }

    /* Inner scroll wrapper — wraps everything after the toolbar so the
       row tree gets a height cap + native scrollbar when content
       overflows. Used in both intro + sidebar contexts; sidebar
       capped tighter than intro to fit the narrower panel. */
    .library-scroll {
      display: flex;
      flex-direction: column;
      gap: 6px;
      max-height: 280px;
      overflow-y: auto;
      padding-right: 4px;
    }
    .intro-col .library-scroll {
      max-height: 55vh;
    }
    /* Subtle scrollbar styling (WebKit) so it doesn't fight the
       library card's dark glass aesthetic. */
    .library-scroll::-webkit-scrollbar {
      width: 8px;
    }
    .library-scroll::-webkit-scrollbar-track {
      background: transparent;
    }
    .library-scroll::-webkit-scrollbar-thumb {
      background: rgba(255, 255, 255, 0.10);
      border-radius: 999px;
    }
    .library-scroll::-webkit-scrollbar-thumb:hover {
      background: rgba(255, 255, 255, 0.18);
    }
    /* Filter input above the library list — only shown when the list
       has enough items to make scanning slow. Compact + understated
       so it doesn't compete with the row content visually. */
    .library-filter {
      width: 100%;
      box-sizing: border-box;
      margin: 6px 0 8px;
      padding: 6px 9px;
      font-family: var(--mono);
      font-size: 11px;
      color: var(--text);
      background: rgba(255, 255, 255, 0.04);
      border: 1px solid var(--border);
      border-radius: 6px;
      outline: none;
      letter-spacing: 0.05em;
    }
    .library-filter::placeholder { color: var(--muted); }
    .library-filter:focus {
      border-color: rgba(245, 200, 66, 0.45);
      background: rgba(245, 200, 66, 0.04);
    }
    .library-empty {
      font-family: var(--mono);
      font-size: 12px;
      color: var(--muted);
      letter-spacing: 0.1em;
      line-height: 1.5;
      padding: 12px 10px;
      border: 1px dashed var(--border);
      border-radius: 8px;
      background: rgba(255, 255, 255, 0.02);
    }
    /* (Earlier .library-row + .lib-meta / .lib-name / .lib-info /
       .lib-actions / .lib-thumb rules removed — they were fully
       shadowed by the newer modern-row block further down in this
       file (search for "Job rows (cloud, modern treatment)"). Two
       definitions of the same selector fragment the source-of-truth;
       this is the older, dead one. Deleted in the 2026-05-28 audit
       cleanup.) */
    .lib-action-btn {
      background: var(--btn-bg);
      border: 1px solid var(--border);
      border-radius: 5px;
      color: var(--muted);
      font-family: var(--mono);
      font-weight: 500;
      font-size: 10px;
      padding: 3px 7px;
      cursor: pointer;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      transition: all 0.1s;
    }
    .lib-action-btn:hover { color: var(--accent); border-color: rgba(245, 200, 66, 0.4); }
    .lib-action-btn.danger:hover { color: var(--danger); border-color: rgba(255, 107, 107, 0.5); }
    .lib-action-btn:disabled { opacity: 0.5; cursor: not-allowed; }

    /* ── Library toolbar + folder rows ─────────────────────────────
       Hosts the "+ New folder" button above the list. Folder rows
       render between job rows as expandable group headers; nested
       folders indent via --lib-depth so the tree reads at a glance. */
    .library-toolbar {
      display: flex;
      align-items: center;
      gap: 6px;
      margin-bottom: 10px;
    }
    .library-new-folder-btn {
      display: inline-flex;
      align-items: center;
      gap: 6px;
      padding: 7px 12px;
      font-family: var(--mono);
      font-size: 10.5px;
      font-weight: 600;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      color: var(--text);
      background: rgba(245, 200, 66, 0.08);
      border: 1px solid rgba(245, 200, 66, 0.25);
      border-radius: 7px;
      cursor: pointer;
      transition: all 0.12s ease;
    }
    .library-new-folder-btn:hover {
      color: #2a1d00;
      background: linear-gradient(135deg, #ffe27a, #f5c842);
      border-color: transparent;
      box-shadow: 0 4px 14px -4px rgba(245, 200, 66, 0.55);
      transform: translateY(-1px);
    }
    .lib-new-folder-icon {
      font-size: 13px;
      line-height: 1;
      font-weight: 700;
    }

    /* ─── Sort dropdown ────────────────────────────────────────── */
    .library-sort-wrap {
      position: relative;
      margin-left: auto;  /* push sort to the right side of the toolbar */
    }
    .library-sort-btn {
      display: inline-flex;
      align-items: center;
      gap: 6px;
      padding: 7px 10px;
      font-family: var(--mono);
      font-size: 10.5px;
      font-weight: 500;
      letter-spacing: 0.06em;
      color: var(--muted);
      background: rgba(255, 255, 255, 0.03);
      border: 1px solid var(--border, rgba(255, 255, 255, 0.08));
      border-radius: 7px;
      cursor: pointer;
      transition: all 0.12s ease;
    }
    .library-sort-btn:hover,
    .library-sort-btn[aria-expanded="true"] {
      color: var(--text);
      background: rgba(255, 255, 255, 0.06);
      border-color: rgba(255, 255, 255, 0.16);
    }
    .library-sort-icon {
      font-size: 12px;
      line-height: 1;
      opacity: 0.7;
    }
    .library-sort-label {
      max-width: 140px;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    .library-sort-caret {
      font-size: 8px;
      opacity: 0.55;
      margin-left: 1px;
    }
    .library-sort-panel {
      position: absolute;
      top: calc(100% + 4px);
      right: 0;
      min-width: 180px;
      padding: 4px;
      background: rgba(20, 18, 22, 0.96);
      backdrop-filter: var(--glass-blur, blur(8px));
      -webkit-backdrop-filter: var(--glass-blur, blur(8px));
      border: 1px solid var(--border, rgba(255, 255, 255, 0.08));
      border-radius: 9px;
      box-shadow:
        0 12px 30px -10px rgba(0, 0, 0, 0.6),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
      z-index: 50;
      display: flex;
      flex-direction: column;
      gap: 1px;
    }
    .library-sort-panel[hidden] { display: none; }
    .library-sort-option {
      display: flex;
      align-items: center;
      gap: 8px;
      padding: 7px 10px;
      background: transparent;
      border: 0;
      border-radius: 6px;
      cursor: pointer;
      text-align: left;
      color: var(--text);
      font-family: var(--sans);
      font-size: 12.5px;
      transition: background 0.1s;
    }
    .library-sort-option:hover {
      background: rgba(245, 200, 66, 0.08);
    }
    .library-sort-option.is-current {
      color: var(--accent, #f5c842);
    }
    .library-sort-check {
      width: 12px;
      font-family: var(--mono);
      font-size: 11px;
      color: var(--accent, #f5c842);
      text-align: center;
    }

    /* ─── Tree indentation + connector lines ─────────────────────
       Children groups get a left padding + a subtle vertical guide
       line so the parent/child relationship reads at a glance. The
       guide aligns with the chevron column of the parent folder. */
    .lib-folder-children {
      position: relative;
      padding-left: 18px;
      margin-left: 0;
    }
    .lib-folder-children::before {
      content: '';
      position: absolute;
      left: 8px;
      top: 0;
      bottom: 0;
      width: 1px;
      background: linear-gradient(
        to bottom,
        rgba(255, 255, 255, 0.06) 0%,
        rgba(255, 255, 255, 0.10) 30%,
        rgba(255, 255, 255, 0.10) 70%,
        rgba(255, 255, 255, 0.06) 100%
      );
    }

    /* Reset the depth-based margin (was applied uniformly before).
       Indentation now comes from the .lib-folder-children wrapper —
       each level adds 18px via the padding-left above. */
    .library-row,
    .library-folder-row {
      margin-left: 0;
    }

    /* ─── Folder rows ─────────────────────────────────────────────
       Subtle "navigation row" treatment: no border by default, gentle
       hover background, accent-colored chevron + folder icon for
       scannability. Reads as part of the list, not a competing card. */
    .library-folder-row {
      display: grid;
      grid-template-columns: 14px 22px 18px 1fr auto auto;
      gap: 6px;
      align-items: center;
      padding: 8px 10px;
      background: transparent;
      border: 1px solid transparent;
      border-radius: 7px;
      cursor: pointer;
      user-select: none;
      transition: background 0.12s ease, border-color 0.12s ease;
      margin-bottom: 2px;
    }
    .library-folder-row:hover {
      background: rgba(255, 255, 255, 0.04);
    }
    .lib-folder-chevron {
      color: rgba(255, 255, 255, 0.55);
      font-size: 11px;
      width: 14px;
      text-align: center;
      transition: transform 0.18s ease, color 0.12s;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      line-height: 1;
    }
    /* Use rotation instead of glyph swap for a smoother feel. The JS
       still writes ▾ when expanded; we rotate from -90deg when
       collapsed to 0deg when expanded for visual continuity. */
    .library-folder-row.is-collapsed .lib-folder-chevron {
      transform: rotate(-90deg);
    }
    .library-folder-row:hover .lib-folder-chevron { color: var(--accent); }
    .lib-folder-icon {
      font-size: 13px;
      line-height: 1;
      filter: saturate(0.85);
    }
    .lib-folder-name {
      font-family: var(--sans);
      font-size: 13px;
      font-weight: 600;
      color: var(--text);
      letter-spacing: -0.005em;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    .lib-folder-count {
      font-family: var(--mono);
      font-size: 10px;
      color: var(--muted);
      letter-spacing: 0.04em;
      padding: 2px 7px;
      background: rgba(255, 255, 255, 0.04);
      border-radius: 999px;
      min-width: 18px;
      text-align: center;
    }
    .lib-folder-count:empty { display: none; }
    .lib-folder-actions {
      display: flex;
      gap: 2px;
      opacity: 0;
      transition: opacity 0.15s ease;
    }
    .library-folder-row:hover .lib-folder-actions { opacity: 1; }
    /* Folder action buttons — square icon-only chips */
    .lib-folder-actions .lib-action-btn {
      padding: 0;
      width: 26px;
      height: 26px;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      font-size: 12px;
      letter-spacing: 0;
      text-transform: none;
    }

    /* ─── Job rows (cloud, modern treatment) ─────────────────────
       Compact card with thumbnail, name, meta. Action chips appear
       only on hover as a horizontal icon row — the row itself stays
       quiet by default. Hover reveals the actions and lifts the
       border subtly so the user feels invited to interact. */
    .library-row {
      display: grid;
      grid-template-columns: 22px 36px 1fr auto;
      gap: 10px;
      align-items: center;
      padding: 8px 10px;
      background: rgba(255, 255, 255, 0.02);
      border: 1px solid rgba(255, 255, 255, 0.06);
      border-radius: 8px;
      cursor: pointer;
      transition: all 0.15s ease;
      user-select: none;
      margin-bottom: 4px;
      position: relative;
    }
    .library-row:hover {
      border-color: rgba(245, 200, 66, 0.35);
      background: rgba(245, 200, 66, 0.04);
    }
    /* Selected state — soft yellow tint, no edge stripe, no shift. */
    .library-row.is-selected {
      background: rgba(245, 200, 66, 0.10) !important;
      border-color: rgba(245, 200, 66, 0.45);
    }
    .library-row.is-compact {
      grid-template-columns: 22px 28px 1fr;
      padding: 5px 8px;
      margin-bottom: 2px;
    }
    .library-row.is-compact .lib-thumb { width: 28px; height: 28px; }
    .library-row.is-compact .lib-actions,
    .library-row.is-compact .lib-info { display: none; }
    .library-row.is-compact .lib-name { font-size: 12px; }
    .library-row .lib-thumb {
      width: 36px;
      height: 36px;
      object-fit: contain;
      border-radius: 6px;
      background: repeating-conic-gradient(rgba(255,255,255,0.04) 0% 25%, rgba(255,255,255,0.08) 0% 50%) 0 0 / 6px 6px;
      flex-shrink: 0;
    }
    .library-row .lib-meta { min-width: 0; }
    .library-row .lib-name {
      font-family: var(--sans);
      font-weight: 600;
      font-size: 12.5px;
      color: var(--text);
      letter-spacing: -0.005em;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    .library-row .lib-info {
      font-family: var(--mono);
      font-size: 9.5px;
      color: var(--muted);
      letter-spacing: 0.05em;
      margin-top: 2px;
      text-transform: uppercase;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    .library-row .lib-actions {
      display: flex;
      flex-direction: row;
      gap: 2px;
      align-items: center;
      opacity: 0;
      transition: opacity 0.15s ease;
    }
    .library-row:hover .lib-actions { opacity: 1; }
    /* Job action chips — icon-only squares, no text labels.
       Tooltips (via title="") describe the action on hover. */
    .library-row .lib-action-btn {
      padding: 0;
      width: 26px;
      height: 26px;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      font-size: 12px;
      letter-spacing: 0;
      text-transform: none;
      font-family: var(--sans);
    }
    /* The "Open" action — styled to match the "+ New folder" chip
       in the library toolbar. Yellow-tinted at rest, full yellow
       gradient on hover. Arrow icon + label, no vertical bump. */
    .library-row .lib-action-btn.lib-action-open {
      width: auto;
      height: 26px;
      gap: 6px;
      padding: 0 11px;
      font-family: var(--mono);
      font-size: 10px;
      font-weight: 600;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      color: var(--text);
      background: rgba(245, 200, 66, 0.08);
      border: 1px solid rgba(245, 200, 66, 0.25);
      border-radius: 7px;
      box-shadow: none;
      transition: color 0.12s ease, background 0.12s ease, border-color 0.12s ease;
    }
    .library-row .lib-action-btn.lib-action-open:hover {
      color: #2a1d00;
      background: linear-gradient(135deg, #ffe27a, #f5c842);
      border-color: transparent;
    }
    .lib-open-icon {
      width: 12px;
      height: 12px;
      display: block;
    }

    /* Drag + drop visual states */
    .library-row.is-dragging,
    .library-folder-row.is-dragging {
      opacity: 0.35;
      transform: none;
    }
    .library-folder-row.is-drop-target {
      background: rgba(245, 200, 66, 0.12) !important;
      border-color: rgba(245, 200, 66, 0.55) !important;
      box-shadow: 0 0 0 1px rgba(245, 200, 66, 0.4), 0 4px 14px -4px rgba(245, 200, 66, 0.35);
    }
    /* List-level drop = move to root. Subtle outline so the user
       knows the list as a whole is a valid target without dimming
       individual folder rows that are themselves better targets. */
    .library-list.is-root-drop-target {
      outline: 1px dashed rgba(245, 200, 66, 0.5);
      outline-offset: -4px;
      border-radius: 10px;
    }

    /* ─── Star (pin) button ──────────────────────────────────────
       Rounded-corner 5-point star. Outlined gray when unpinned;
       solid brand-gold when pinned. The chip itself is transparent
       — the star icon carries the brand. */
    .lib-row-pin {
      width: 24px;
      height: 24px;
      padding: 0;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      background: transparent;
      border: 0;
      border-radius: 999px;
      cursor: pointer;
      color: rgba(255, 255, 255, 0.38);
      transition: color 0.15s ease, background 0.15s ease,
                  transform 0.15s ease;
      flex-shrink: 0;
    }
    .lib-row-pin svg {
      width: 17px;
      height: 17px;
      display: block;
    }
    .lib-row-pin svg path {
      fill: none;
      stroke: currentColor;
      stroke-width: 2.1;
    }
    .lib-row-pin:hover {
      color: var(--accent, #f5c842);
      background: rgba(245, 200, 66, 0.10);
      transform: scale(1.1);
    }
    /* Pinned: filled brand-gold star. Stroke matches so the rounded
       linejoin softens the outer corners too. Soft golden glow under
       the star for the "active" affordance. */
    .lib-row-pin.is-pinned {
      color: var(--accent, #f5c842);
    }
    .lib-row-pin.is-pinned svg {
      filter: drop-shadow(0 0 6px rgba(245, 200, 66, 0.4));
    }
    .lib-row-pin.is-pinned svg path {
      fill: currentColor;
      stroke: currentColor;
      stroke-width: 2.1;
    }
    .lib-row-pin.is-pinned:hover {
      background: rgba(245, 200, 66, 0.12);
    }
    .lib-row-pin:active { transform: scale(0.94); }

    /* (Library section styles — .library-section / -header / -icon /
       -title / -count / -body — removed: they were emitted for the
       Recent files section that got deleted per user request. The
       sidebar's compact recent list uses .library-recent-list +
       .library-recent-empty instead. Deleted in 2026-05-28 audit
       cleanup. Note: #library-section ID on the sidebar panel
       container is still in use.) */

    /* ─── Bulk-action bar ────────────────────────────────────────
       Slides up from the bottom of the library list when any rows
       are selected. Sticky so it stays visible while scrolling the
       list. Floats with a soft yellow border to read as "action
       surface", not "another row". */
    .library-bulk-bar {
      position: sticky;
      bottom: 6px;
      z-index: 30;
      display: flex;
      align-items: center;
      gap: 4px;
      margin-top: 12px;
      padding: 8px 10px;
      background: linear-gradient(180deg, rgba(28, 24, 32, 0.95), rgba(20, 18, 22, 0.98));
      backdrop-filter: blur(10px);
      -webkit-backdrop-filter: blur(10px);
      border: 1px solid rgba(245, 200, 66, 0.35);
      border-radius: 10px;
      box-shadow: 0 8px 24px -8px rgba(0, 0, 0, 0.6);
      animation: bulkBarSlideUp 0.18s ease-out;
    }
    @keyframes bulkBarSlideUp {
      from { transform: translateY(8px); opacity: 0; }
      to   { transform: translateY(0); opacity: 1; }
    }
    .library-bulk-count {
      flex: 1;
      font-family: var(--mono);
      font-size: 10.5px;
      font-weight: 600;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      color: var(--accent, #f5c842);
    }
    .library-bulk-btn {
      padding: 5px 8px;
      font-family: var(--mono);
      font-size: 10px;
      font-weight: 600;
      letter-spacing: 0.06em;
      text-transform: uppercase;
      color: var(--text);
      background: rgba(255, 255, 255, 0.04);
      border: 1px solid rgba(255, 255, 255, 0.08);
      border-radius: 6px;
      cursor: pointer;
      transition: all 0.1s;
    }
    .library-bulk-btn:hover {
      background: rgba(255, 255, 255, 0.08);
      border-color: rgba(255, 255, 255, 0.18);
    }
    .library-bulk-btn.danger:hover {
      color: #ff7a7a;
      border-color: rgba(255, 122, 122, 0.55);
      background: rgba(255, 122, 122, 0.08);
    }
    .library-bulk-clear {
      padding: 0;
      width: 24px;
      height: 24px;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      font-size: 13px;
      background: transparent;
      border: 0;
      border-radius: 5px;
      color: var(--muted);
      cursor: pointer;
      transition: color 0.1s, background 0.1s;
      margin-left: 4px;
    }
    .library-bulk-clear:hover {
      color: var(--text);
      background: rgba(255, 255, 255, 0.06);
    }

    /* ── Folder-picker modal ──────────────────────────────────────
       Tree-style destination chooser opened by the "📂 move" affordances
       on job rows + folder rows. Reuses .app-modal layout; the list
       fills the card body with one button per destination. */
    .folder-picker-list {
      max-height: 320px;
      overflow-y: auto;
      margin: 8px 0 16px;
      padding: 4px;
      background: rgba(255, 255, 255, 0.02);
      border: 1px solid var(--border, rgba(255, 255, 255, 0.08));
      border-radius: 8px;
    }
    .folder-picker-option {
      width: 100%;
      display: flex;
      align-items: center;
      gap: 10px;
      padding: 8px 10px;
      padding-left: calc(10px + var(--picker-depth, 0) * 14px);
      background: transparent;
      border: 0;
      border-radius: 6px;
      cursor: pointer;
      color: var(--text);
      font-family: var(--sans);
      font-size: 13px;
      text-align: left;
      transition: background 0.1s;
    }
    .folder-picker-option:hover:not(:disabled) {
      background: rgba(245, 200, 66, 0.08);
    }
    .folder-picker-option:disabled {
      opacity: 0.35;
      cursor: not-allowed;
    }
    .folder-picker-option.is-current {
      background: rgba(255, 255, 255, 0.04);
    }
    .folder-picker-icon { font-size: 14px; }
    .folder-picker-label { flex: 1; }
    .folder-picker-tag {
      font-family: var(--mono);
      font-size: 9.5px;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      color: var(--muted);
      padding: 2px 6px;
      border: 1px solid var(--border);
      border-radius: 999px;
    }

    /* (Workspace ID styles removed — the workspace-info / -row /
       -label / -copy-btn / -id / -info-help / -restore / -restore-row
       / -restore-input classes were emitted by the legacy anonymous
       Render-backend sidebar and have no markup since the auth wall
       went live. Deleted in the 2026-05-28 audit cleanup.) */

    /* "Saved!" hint next to the save-to-library button */
    .library-save-hint {
      display: inline-block;
      margin-left: 8px;
      font-family: var(--mono);
      font-weight: 500;
      font-size: 11px;
      color: var(--accent);
      letter-spacing: 0.1em;
    }

    /* Save-to-library button at the top of the saved-jobs panel. Same
       chip dimensions as the header's .header-btn (32px tall, tight
       padding, 11.5px monospace caps) but in the neutral white-on-glass
       palette — keeps it visually paired with the sibling Load saved
       bundle button instead of competing with the Export ad CTA. */
    .library-save-btn-top {
      width: 100%;
      height: 32px;
      margin-bottom: 12px;
      padding: 0 10px;
      line-height: 1;
      background: var(--btn-bg);
      color: var(--text);
      border: 1px solid rgba(255, 255, 255, 0.08);
      border-radius: 8px;
      font-family: var(--mono);
      font-size: 11.5px;
      font-weight: 500;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      text-align: center;
      white-space: nowrap;
      cursor: pointer;
      transition: all 0.12s;
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
      backdrop-filter: blur(12px);
      -webkit-backdrop-filter: blur(12px);
    }
    .library-save-btn-top:hover,
    .library-save-btn-top.preset-io-btn:hover {
      background: rgba(255, 255, 255, 0.08);
      color: var(--accent);
      border-color: rgba(245, 200, 66, 0.4);
    }

    /* ── Pre-upload retina prompt ── */
    #retina-prompt {
      display: none;
      border: 1px solid var(--border);
      border-radius: 12px;
      padding: 16px;
      background: rgba(20, 18, 22, 0.45);
      backdrop-filter: var(--glass-blur);
      -webkit-backdrop-filter: var(--glass-blur);
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .retina-filename {
      font-family: var(--mono);
      font-size: 0.7rem;
      color: var(--accent);
      letter-spacing: 0.1em;
      margin-bottom: 4px;
      word-break: break-all;
    }

    .retina-question {
      font-family: var(--sans);
      font-size: 0.78rem;
      letter-spacing: 0;
      text-transform: none;
      color: var(--muted);
      margin-bottom: 12px;
      line-height: 1.5;
    }

    .retina-options {
      display: flex;
      flex-direction: column;
      gap: 6px;
      margin-bottom: 14px;
    }

    .retina-option {
      display: flex;
      align-items: flex-start;
      gap: 10px;
      padding: 10px 12px;
      border: 1px solid var(--border);
      border-radius: 8px;
      cursor: pointer;
      transition: all 0.12s;
    }

    .retina-option:hover { border-color: var(--accent); background: rgba(245, 200, 66,0.03); }
    .retina-option.selected { border-color: var(--accent); background: rgba(245, 200, 66,0.06); }

    .retina-option input[type="radio"] {
      accent-color: var(--accent);
      margin-top: 2px;
      cursor: pointer;
    }

    .retina-option-body { flex: 1; min-width: 0; }

    .retina-option-title {
      font-family: var(--sans);
      font-size: 0.85rem;
      font-weight: 500;
      color: var(--text);
      letter-spacing: 0;
    }

    .retina-option-desc {
      font-family: var(--sans);
      font-size: 0.72rem;
      color: var(--muted);
      margin-top: 3px;
      line-height: 1.5;
    }

    .retina-detected {
      display: inline-block;
      font-family: var(--mono);
      font-size: 0.5rem;
      color: var(--accent);
      letter-spacing: 0.1em;
      text-transform: uppercase;
      border: 1px solid var(--accent);
      padding: 2px 6px;
      border-radius: 6px;
      margin-left: 8px;
      vertical-align: middle;
    }

    .retina-actions { display: flex; gap: 8px; }

    .retina-btn {
      flex: 1;
      padding: 10px;
      font-family: var(--mono);
      font-size: 0.7rem;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      border-radius: 8px;
      cursor: pointer;
      transition: all 0.15s;
    }

    .retina-btn-cancel {
      background: var(--btn-bg);
      color: var(--muted);
      border: 1px solid var(--border);
      backdrop-filter: blur(12px);
      -webkit-backdrop-filter: blur(12px);
    }
    .retina-btn-cancel:hover { background: rgba(255, 255, 255, 0.08); color: var(--text); border-color: rgba(255, 255, 255, 0.16); }

    .retina-btn-go {
      background: linear-gradient(180deg, #ffd87a, #f5c842);
      color: #2a1d00;
      border: 1px solid rgba(245, 200, 66, 0.6);
      font-weight: 600;
      box-shadow: 0 4px 14px rgba(245, 200, 66, 0.25), inset 0 1px 0 rgba(255,255,255,0.3);
    }
    .retina-btn-go:hover { background: linear-gradient(180deg, #ffe190, #ffd05a); color: #2a1d00; }

    /* ── Track editor (timeline visualization) ── */
    #track-editor {
      display: none;
      padding: 10px 16px 12px;
      user-select: none;
      position: relative;
      /* glass treatment from shared floating-panel block (specific
         border-radius + top border applied in the .has-manifest rule
         above to fuse with #transport above it) */
      /* Ditto — flex parent (panel-right) defaults to letting content grow
         the item past available width. Cap it explicitly so the inner
         scroll-host's overflow-x is the only thing that handles excess width. */
      min-width: 0;
      box-sizing: border-box;
    }

    /* Playhead — vertical line + handle + time label */
    .track-playhead {
      position: absolute;
      top: 0; bottom: 0;
      width: 2px;
      background: var(--accent);
      /* Above the sticky ruler-wrap (z-index: 10) so the time-label
         pill at the top of the playhead doesn't get hidden behind the
         ruler's opaque background. The line itself crossing through the
         ruler tick marks reads fine — matches every video-editing UI. */
      z-index: 11;
      pointer-events: none;
      transform: translateX(-1px);
      display: none;
      box-shadow: 0 0 8px rgba(245, 200, 66,0.4);
    }

    .track-playhead.show { display: block; }

    .track-playhead-handle {
      position: absolute;
      top: 0; bottom: 0;
      left: -5px; right: -5px;
      cursor: ew-resize;
      pointer-events: auto;
    }

    .track-playhead-label {
      position: absolute;
      /* Sits inside the scroll-host, at the top of the ruler band — the
         host now has overflow-y:auto for the 20-row cap, so a negative
         top would get clipped against the host's content box. 2px gives
         the pill enough room to render its shadow without bleeding into
         the row area below. */
      top: 2px;
      left: 50%;
      transform: translateX(-50%);
      background: linear-gradient(180deg, #ffd87a, #f5c842);
      color: #2a1d00;
      padding: 2px 7px;
      border-radius: 6px;
      font-family: var(--mono);
      font-weight: 600;
      font-size: 11px;
      letter-spacing: 0.1em;
      white-space: nowrap;
      pointer-events: none;
      z-index: 6;
      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.3);
    }
    /* When the playhead is right against the right edge of the timeline its
       centered label would otherwise extend past the ruler into the HOVER
       column. Snap the label to be left-aligned with the playhead instead. */
    .track-playhead.at-right-edge .track-playhead-label {
      left: auto;
      right: 0;
      transform: none;
    }
    .track-playhead.at-left-edge .track-playhead-label {
      left: 0;
      transform: none;
    }

    /* Ruler is also clickable for fast jump-to-time */
    .track-ruler { cursor: pointer; }

    .track-editor-header {
      display: flex;
      align-items: center;
      gap: 14px;
      margin-bottom: 6px;
      flex-wrap: nowrap;
    }
    /* When the header is ALSO acting as the preset row (merged into a
       single line — current layout), apply the original preset-row's
       bottom border + spacing so the divider sits below the unified
       row instead of between two stacked rows. */
    .track-editor-header.track-global-preset-row {
      margin-bottom: 8px;
      padding-bottom: 6px;
      border-bottom: 1px dashed var(--border);
    }

    .track-editor-title {
      font-family: var(--mono);
      font-weight: 600;
      font-size: 12px;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      color: var(--text);
      opacity: 0.85;
    }
    /* Diamond color legend — small swatch + label pairs that map the
       four kf colors users see on the timeline rows. Sits in the
       track-editor header alongside the title + canvas-info readout.
       Each swatch is the actual diamond styling (rotated square +
       border) at a smaller size so it reads as "this exact shape." */
    .track-kf-legend {
      display: inline-flex;
      align-items: center;
      gap: 5px;
      font-family: var(--mono);
      font-size: 9.5px;
      letter-spacing: 0.06em;
      color: var(--muted);
      user-select: none;
    }
    /* (Was: viewport-narrow hide rule. The legend now lives in the `?`
       shortcuts modal — overlay-centered with its own scroll, so it
       doesn't compete with timeline header chrome for width.) */
    .track-kf-legend-label {
      margin-right: 3px;
    }
    .track-kf-legend-label:last-child { margin-right: 0; }
    /* Swatches mirror the actual diamond visual: layer-color fill +
       a kf-type ring. The ring colors read from the SAME --kf-*-ring
       variables that drive .track-keyframe-{type} above, so editing
       a kf color at :root updates both the legend swatch AND every
       diamond on the timeline in one step. */
    .track-kf-legend-swatch {
      display: inline-block;
      width: 7px;
      height: 7px;
      margin: 2px;
      transform: rotate(45deg);
      background: rgba(220, 230, 225, 0.55);
      border: 1px solid #0a0a0a;
      flex-shrink: 0;
    }
    .track-kf-legend-swatch.lg-entry { box-shadow: 0 0 0 1.5px var(--kf-entry-ring); }
    .track-kf-legend-swatch.lg-exit  { box-shadow: 0 0 0 1.5px var(--kf-exit-ring); }
    .track-kf-legend-swatch.lg-hover { box-shadow: 0 0 0 1.5px var(--kf-hover-ring); }
    .track-kf-legend-swatch.lg-mask  { background: var(--kf-mask-fill); box-shadow: 0 0 0 1.5px var(--kf-mask-ring); }

    /* ── Layer / group / mask color legend ─────────────────────────
       Shown inside the `?` shortcuts modal next to the keyframe-
       diamond legend. Same source-of-truth color vars as the
       layers panel + timeline rows so the legend stays in sync. */
    /* Vertical stack — each row is one swatch + its label, glued
       together so the wrap never splits a pair across lines. */
    .layer-color-legend {
      display: flex;
      flex-direction: column;
      align-items: flex-start;
      gap: 6px;
      font-family: var(--mono);
      font-size: 10.5px;
      letter-spacing: 0.04em;
      color: var(--muted);
      user-select: none;
    }
    .layer-color-legend-item {
      display: inline-flex;
      align-items: center;
      gap: 8px;
      white-space: nowrap;
    }
    .layer-color-legend-swatch {
      display: inline-block;
      width: 10px;
      height: 10px;
      border-radius: 2px;
      flex-shrink: 0;
    }
    .layer-color-legend-swatch.lc-layer { background: var(--accent); box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.45); }
    .layer-color-legend-swatch.lc-group { background: var(--color-group); box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.45); }
    .layer-color-legend-swatch.lc-mask  { background: var(--color-mask);  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.45); }

    .track-editor-hint {
      font-family: var(--mono);
      font-size: 11px;
      color: var(--muted);
      letter-spacing: 0.1em;
    }

    /* Standalone preset-row layout — only applied when the row is
       NOT also acting as the editor header (legacy / future use).
       The merged layout reads its border + spacing from
       .track-editor-header.track-global-preset-row above. */
    .track-global-preset-row:not(.track-editor-header) {
      display: flex;
      gap: 14px;
      margin-bottom: 8px;
      padding-bottom: 6px;
      border-bottom: 1px dashed var(--border);
    }

    .track-global-preset-bar {
      display: flex;
      align-items: center;
      gap: 8px;
      flex: 0 1 auto;
      min-width: 0;
    }
    /* Stagger group gets its own min-width since it has 3 icon
       buttons + a number input; keep it from collapsing too small
       when the row is tight. */
    .track-global-preset-bar.track-global-stagger { flex-shrink: 0; }
    /* Trailing group at the right edge of the merged header — kf
       legend + pause-hover. margin-left:auto pushes it right; the
       internal layout stays a tight flex row. */
    .track-global-preset-bar.track-global-trailing {
      margin-left: auto;
      flex-shrink: 0;
    }

    .track-global-preset-label {
      font-family: var(--mono);
      font-size: 0.55rem;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      color: var(--muted);
      flex-shrink: 0;
    }
    /* Entry / Exit preset labels carry a `data-suffix=" preset"` on
       the wide-viewport variant — appended via ::after so the suffix
       can drop out when the row gets tight. The base text ("Entry"
       or "Exit") stays in the HTML, so the dropdown is always
       labeled even when the suffix is hidden. */
    .preset-label-suffix::after {
      content: attr(data-suffix);
    }
    @media (max-width: 1300px) {
      .preset-label-suffix::after { content: none; }
    }

    /* Entry / exit preset dropdowns in the timeline header. Shorter
       than they used to be so the row fits on one line alongside
       Stagger + Pause hover (and the now-hidden Keyframes guide).
       Both selects share the same max-width — keeps the row visually
       balanced. */
    #global-preset-select,
    #global-exit-preset-select {
      flex: 0 1 150px;
      min-width: 0;
      max-width: 150px;
    }
    /* Global Keyframes toggle is hidden for now — designers control
       kf mode per-layer via the diamond on each track row. The
       button stays in the DOM so existing kf-state code (the All
       Off / Some / All On label switch) doesn't break. */
    #track-kf-global-btn { display: none; }

    /* Scroll host wraps the ruler, all rows, and the playhead. Horizontal
       scroll kicks in when the inner content (ruler+rows) is wider than the
       host — i.e. when zoom > 1. Vertical scroll is suppressed so the panel
       height stays fixed. */
    .track-scroll-host {
      position: relative;
      overflow-x: auto;
      /* Vertical scroll kicks in past max-height — banners with many
         layers (a masked group expanded into per-child layers can easily
         produce 15-25+ rows) used to push the timeline off-screen. Cap
         is sized to ~20 rows of visible space (each row 20px tall + 3px
         gap = 23px, plus the ruler ~26px on top). The horizontal-scroll
         affordance from overflow-x: auto is preserved. */
      overflow-y: auto;
      max-height: 486px;
      /* Hard cap so the host never grows past its parent regardless of how
         wide its inner content gets. Without this, the wide ruler/rows can
         push the host wider, which propagates back up through the flex chain
         and shifts the rest of the layout. */
      width: 100%;
      max-width: 100%;
      min-width: 0;
      /* Reserve a small bit of bottom padding so the horizontal scrollbar
         doesn't visually clip the last track-row. */
      padding-bottom: 4px;
    }
    /* Custom thin scrollbar so the timeline area doesn't feel chunky.
       Both width (vertical scroll for tall layer lists) and height
       (horizontal scroll for zoomed-in time ranges) match. */
    .track-scroll-host::-webkit-scrollbar { width: 8px; height: 8px; }
    .track-scroll-host::-webkit-scrollbar-track { background: transparent; }
    .track-scroll-host::-webkit-scrollbar-thumb {
      background: rgba(255,255,255,0.12);
      border-radius: 6px;
    }
    .track-scroll-host::-webkit-scrollbar-thumb:hover {
      background: rgba(255,255,255,0.22);
    }

    .track-ruler-wrap {
      display: flex;
      gap: 8px;
      margin-bottom: 4px;
      /* Stick to the top of the scroll-host while the rows scroll
         underneath. Without this, scrolling 20+ layers leaves the user
         flying blind — the time ticks (1s, 2s, …) would scroll off the
         top of the panel. The opaque background covers any playhead /
         row content that scrolls behind it. */
      position: sticky;
      top: 0;
      z-index: 10;
      background: var(--bg);
    }

    /* Layer-name and HOVER columns are sticky so they remain visible while
       the inner ruler/track-tracks scroll horizontally during zoom. */
    .track-name-spacer {
      width: 100px;
      flex-shrink: 0;
      position: sticky;
      left: 0;
      z-index: 5;
      background: rgba(10, 10, 14, 0.85);
      backdrop-filter: blur(8px);
      -webkit-backdrop-filter: blur(8px);
      display: flex;
      align-items: center;
      padding-bottom: 4px;
    }

    /* Zoom controls live inside the name-spacer area at the start of the ruler */
    .track-zoom-controls {
      display: flex;
      align-items: center;
      gap: 0;
      width: 100%;
      padding-right: 6px;
    }
    .track-zoom-btn,
    .track-zoom-level {
      flex: 1;
      height: 18px;
      background: var(--btn-bg);
      border: 1px solid var(--border);
      color: var(--muted);
      font-family: var(--mono);
      font-weight: 500;
      font-size: 10px;
      letter-spacing: 0.1em;
      cursor: pointer;
      padding: 0;
      transition: color 0.1s, background 0.1s, border-color 0.1s;
    }
    .track-zoom-btn { font-size: 12px; line-height: 1; }
    .track-zoom-btn:first-child  { border-radius: 6px 0 0 6px; }
    .track-zoom-btn:last-child   { border-radius: 0 6px 6px 0; }
    .track-zoom-level            { border-left: 0; border-right: 0; min-width: 28px; }
    .track-zoom-btn:hover,
    .track-zoom-level:hover      { color: var(--accent); border-color: rgba(245, 200, 66, 0.4); background: rgba(245, 200, 66, 0.08); }
    .track-zoom-btn:disabled     { opacity: 0.4; cursor: not-allowed; }
    .track-zoom-btn:disabled:hover { color: var(--muted); border-color: var(--border); background: var(--btn-bg); }


    .track-hover-spacer {
      width: 50px;
      flex-shrink: 0;
      font-family: var(--mono);
      font-weight: 600;
      font-size: 10px;
      letter-spacing: 0.04em;
      color: var(--muted);
      text-transform: uppercase;
      text-align: center;
      padding-bottom: 4px;
      border-bottom: 1px solid rgba(255, 255, 255, 0.06);
      /* Sticky on the RIGHT, lined up with .track-hover-cell below so
         the header label aligns with the cell column. The 3px inset
         from the row's right edge matches the cell's right inset so
         the visual column sits inside the active group outline.

         At zoom > 1 the timeline content scrolls underneath this
         sticky column, so the LEFT edge needs an explicit visual
         break (border + left-cast shadow) — otherwise the column
         reads as "part of the ruler's time labels" instead of a
         distinct affordance, and the bars sliding behind its
         translucent background look broken. */
      position: sticky;
      right: 3px;
      z-index: 5;
      background: rgba(10, 10, 14, 0.95);
      backdrop-filter: blur(10px);
      -webkit-backdrop-filter: blur(10px);
      box-shadow: -10px 0 14px -8px rgba(0, 0, 0, 0.55);
    }

    .track-hover-cell {
      width: 50px;
      flex-shrink: 0;
      height: 18px;
      box-sizing: border-box;
      display: flex;
      align-items: center;
      justify-content: center;
      font-family: var(--mono);
      font-weight: 500;
      font-size: 10px;
      /* Cast a soft shadow leftward — same purpose as the
         hover-spacer's box-shadow above. At zoom > 1, timeline bars
         slide UNDER this sticky cell; the shadow gives the column a
         clear left edge so the scrolling bars don't look truncated
         or visually merged with the hover-pill. */
      box-shadow: -10px 0 14px -8px rgba(0, 0, 0, 0.55);
      /* Tightened from 0.1em to fit "⟳ 0.25s" on a single line inside
         the 50px cell. The wider tracking pushed the label past the
         cell's right edge and forced a 2-line wrap in rollover mode. */
      letter-spacing: 0;
      white-space: nowrap;
      border-radius: 5px;
      /* Sticky on the RIGHT, after the timeline. The 3px right inset
         tucks the cell inside the group row's active outline (which
         extends to right:0 of the row) so the outline visibly wraps
         the cell instead of clipping past it. */
      position: sticky;
      right: 3px;
      z-index: 4;
      background: rgba(10, 10, 14, 0.95);
      backdrop-filter: blur(10px);
      -webkit-backdrop-filter: blur(10px);
    }

    .track-hover-cell.active {
      color: var(--accent);
      border: 1px solid rgba(245, 200, 66, 0.4);
      background: rgba(245, 200, 66, 0.10);
      cursor: pointer;
      transition: all 0.12s;
    }

    .track-hover-cell.active:hover { background: rgba(245, 200, 66, 0.18); border-color: rgba(245, 200, 66, 0.6); }
    .track-hover-cell.active.playing {
      color: #2a1d00;
      background: linear-gradient(180deg, #ffd87a, #f5c842);
      border-color: rgba(245, 200, 66, 0.6);
      box-shadow: 0 0 10px rgba(245, 200, 66, 0.4);
    }

    .track-hover-cell.empty {
      border: 1px dashed transparent;   /* invisible, but preserves height */
    }

    /* Group rows use "⟳ HOVER" (one chip per group, regardless of how
       many children carry a rollover). Hair smaller font so the longer
       label fits inside the 50px sticky cell width comfortably. */
    .track-hover-cell.track-hover-cell-group {
      font-size: 9px;
      letter-spacing: 0.04em;
    }

    .track-ruler {
      position: relative;
      height: 22px;
      flex: 1;
      border-bottom: 1px solid var(--border);
    }

    .track-tick {
      position: absolute;
      bottom: 0;
      width: 1px;
      height: 6px;
      background: var(--muted);
    }

    /* Stop-at marker — drawn over the ruler at manifest.timeline.stopAt.
       The vertical line extends down through every track row. Drag the
       tag (or the line) to retime the rest frame. Hit-area is widened
       beyond the visible 1px line via a transparent pseudo so the line
       is grabbable without pixel-perfect aim. */
    /* Stop-at marker — solid warm-red vertical line with a glow,
       paired with a pill tag at the top. Liquid styling matches the
       playhead label's pill aesthetic but uses the danger color so it's
       still distinguishable. */
    .track-stop-marker {
      position: absolute;
      top: 0;
      /* Default to a small extension; renderTrackEditor sets `bottom`
         dynamically to match the actual last-row's bottom edge. The
         legacy `-10000px` value forced the scroll-host to grow its
         scrollable content area by 10kpx, which triggered a vertical
         scrollbar — and the vertical scrollbar's ~8px width shaved the
         visible inner width and triggered a horizontal scrollbar too
         (the bug the user was seeing on enable). */
      bottom: 0;
      width: 0;
      border-left: 1px solid var(--danger);
      box-shadow: 0 0 6px rgba(255, 107, 107, 0.4);
      pointer-events: auto;
      z-index: 3;
      cursor: ew-resize;
    }
    .track-stop-marker::before {
      content: '';
      position: absolute;
      top: 0;
      bottom: 0;
      left: -5px;
      width: 11px;            /* widens the grabbable hit zone */
      pointer-events: auto;
    }
    .track-stop-marker-tag {
      position: absolute;
      top: -2px;
      left: 4px;
      padding: 2px 7px;
      background: linear-gradient(180deg, #ff8a8a, #ff6b6b);
      color: #2a0606;
      font-family: var(--mono);
      font-weight: 600;
      font-size: 11px;
      letter-spacing: 0.1em;
      border: 1px solid rgba(255, 107, 107, 0.6);
      border-radius: 6px;
      white-space: nowrap;
      cursor: grab;
      user-select: none;
      box-shadow:
        0 1px 3px rgba(0, 0, 0, 0.5),
        inset 0 1px 0 rgba(255, 255, 255, 0.3);
    }
    .track-stop-marker-tag:active {
      cursor: grabbing;
    }
    /* When the stop marker is near the right edge of the ruler, anchor
       the tag to the LEFT of the line (inside the ruler) instead of
       letting it overflow off the right side. Mirrors the master
       playhead's .at-right-edge tag convention. */
    .track-stop-marker.at-right-edge .track-stop-marker-tag {
      left: auto;
      right: 4px;
    }
    /* Symmetric anchor for the left edge — keeps the tag inside the
       ruler when the marker is dragged to t=0. */
    .track-stop-marker.at-left-edge .track-stop-marker-tag {
      left: 4px;
      right: auto;
    }

    .track-tick-label {
      position: absolute;
      bottom: 9px;
      font-family: var(--mono);
      font-weight: 500;
      font-size: 11px;
      color: var(--text);
      opacity: 0.5;
      letter-spacing: 0.1em;
      transform: translateX(-50%);
      white-space: nowrap;
    }

    .track-rows {
      display: flex;
      flex-direction: column;
      gap: 3px;
    }

    .track-row {
      display: flex;
      align-items: center;
      gap: 8px;
      height: 24px;
    }

    /* Live highlight on rows that the rubber-band is currently covering. */
    .track-row.rubber-hover {
      background: rgba(245, 200, 66, 0.06);
    }

    /* Floating rubber-band rectangle drawn over the timeline area while
       the user shift-drags to multi-select layers. position:fixed because
       the band is appended to <body> and tracks raw clientX/Y. */
    .track-rubber-band {
      position: fixed;
      pointer-events: none;
      border: 1px solid var(--accent);
      background: rgba(245, 200, 66, 0.08);
      z-index: 100;
    }

    .track-name {
      width: 100px;
      flex-shrink: 0;
      position: sticky;
      left: 0;
      z-index: 4;
      background: rgba(10, 10, 14, 0.85);
      backdrop-filter: blur(8px);
      -webkit-backdrop-filter: blur(8px);
      padding-left: 8px;
      box-sizing: border-box;
      font-family: var(--mono);
      font-weight: 500;
      font-size: 12px;
      color: var(--text);
      letter-spacing: 0.1em;
      display: flex;
      align-items: center;
      gap: 6px;
      flex-shrink: 0;
      cursor: pointer;
      transition: color 0.12s;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    .track-name:hover { color: var(--text); }
    .track-name.active { color: var(--accent); }

    /* Per-layer keyframes toggle, lives at the right edge of the sticky
       track-name column. Single-click flips the layer's useKeyframes state. */
    /* Per-layer keyframes toggle. The button is a transparent hit-zone;
       only the diamond glyph (◇ off, ◆ on) carries the visual state.
       Off: hollow muted diamond. On: solid yellow diamond with a faint
       glow — no surrounding square (which previously read as an
       outlined-diamond-inside-yellow-box). */
    /* Solo + kf-diamond pair sit at the right edge of the name
       column. .track-name-toggles is the flex wrapper that pushes
       them right (margin-left: auto) and keeps them tight together
       with a small gap. Solo first, kf second — same left-to-right
       reading order the user authored. */
    .track-name-toggles {
      margin-left: auto;
      display: flex;
      align-items: center;
      gap: 4px;
      flex-shrink: 0;
    }
    .track-kf-toggle {
      width: 16px;
      height: 16px;
      flex-shrink: 0;
      display: flex;
      align-items: center;
      justify-content: center;
      background: transparent;
      border: none;
      border-radius: 0;
      cursor: pointer;
      color: var(--muted);
      font-family: var(--mono);
      font-size: 14px;
      line-height: 1;
      padding: 0;
      transition: color 0.12s, text-shadow 0.12s, transform 0.12s;
    }
    .track-kf-toggle:hover {
      color: var(--accent);
      transform: scale(1.1);
    }
    .track-kf-toggle.on {
      color: var(--accent);
      text-shadow: 0 0 8px rgba(245, 200, 66, 0.55);
    }

    /* Solo toggle — small filled-circle button to the LEFT of the
       kf diamond. Inactive: dim hollow circle; active: cyan filled. */
    .track-solo-toggle {
      width: 12px;
      height: 12px;
      flex-shrink: 0;
      background: transparent;
      border: 1.5px solid rgba(255, 255, 255, 0.30);
      border-radius: 50%;
      cursor: pointer;
      padding: 0;
      transition: background 0.12s, border-color 0.12s, box-shadow 0.12s;
    }
    .track-solo-toggle:hover {
      border-color: var(--color-hover);
    }
    .track-solo-toggle.on {
      background: var(--color-hover);
      border-color: var(--color-hover);
      box-shadow: 0 0 6px rgba(95, 184, 200, 0.65);
    }

    /* Global "KF mode · all" toggle in the preset header row */
    .track-global-preset-keyframes { flex: 0 0 auto; }

    /* Stagger toolbar — two icon buttons flanking a numeric offset input */
    .track-global-stagger {
      flex: 0 0 auto;
      display: flex;
      align-items: center;
      gap: 4px;
    }
    .track-global-stagger .track-global-preset-label {
      margin-right: 4px;
    }
    .stagger-btn {
      width: 32px;
      height: 26px;
      padding: 0;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      background: transparent;
      border: 1px solid var(--border);
      border-radius: 6px;
      color: var(--muted);
      cursor: pointer;
      transition: color 0.1s, border-color 0.1s, background 0.1s;
    }
    .stagger-btn:hover {
      color: var(--accent);
      border-color: var(--accent);
      background: rgba(245, 200, 66,0.06);
    }
    /* SVG bars inherit the button's text color so hover/disabled states tint
       the icon together with the border. */
    .stagger-svg {
      width: 18px;
      height: 14px;
      display: block;
    }
    .stagger-svg rect { fill: currentColor; }
    .stagger-input {
      width: 50px;
      height: 26px;
      padding: 0 6px;
      text-align: center;
      font-size: 0.6rem;
    }
    .track-kf-global-btn {
      padding: 6px 10px;
      background: transparent;
      border: 1px solid var(--border);
      color: var(--muted);
      font-family: var(--mono);
      font-size: 0.55rem;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      cursor: pointer;
      border-radius: 6px;
      transition: color 0.1s, background 0.1s, border-color 0.1s;
      white-space: nowrap;
    }
    .track-kf-global-btn:hover { color: var(--accent); border-color: var(--accent); }
    .track-kf-global-btn.all-on {
      color: #0a0a0a;
      background: var(--accent);
      border-color: var(--accent);
    }

    .track-name-dot {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      flex-shrink: 0;
    }

    .track-track {
      position: relative;
      flex: 1;
      height: 20px;
      background: rgba(0, 0, 0, 0.30);
      border: 1px solid rgba(255, 255, 255, 0.05);
      border-radius: 5px;
      overflow: hidden;
      transition: background 0.12s ease, border-color 0.12s ease;
    }
    /* Whole-row selection — runs the full inner-timeline width so the
       user keeps a visual anchor on the active lane even when the bar
       is mostly off-screen at zoom > 1. Both the layer row's
       .track-track and the group row's .track-track-group have an
       explicit width = getInnerTimelineWidth() set in JS, so these
       highlights stretch the full scrolled content (not the viewport).
       Group rows reuse the existing --grp-hl-bg / --grp-hl-shadow
       palette variables so the variant colors (yellow singleton,
       green mask, pink video) flow through unchanged — just moved
       from the row's ::before overlay to the inner track-track-group
       so they cover the actual scrolled width. */
    .track-row.is-selected .track-track {
      background: rgba(245, 200, 66, 0.09);
      box-shadow: inset 0 0 0 1.5px rgba(245, 200, 66, 0.55);
      border-color: rgba(245, 200, 66, 0.45);
    }
    /* Group-row highlight — both :hover and .active flow through the
       same selector pair so the user gets identical full-width
       feedback in either state (instead of a narrow hover tint and a
       wider active tint at different sizes, which was visually
       inconsistent). Variant-specific palette colors arrive via the
       existing --grp-hl-bg / --grp-hl-shadow CSS variables set by the
       singleton / mask / video :hover + .active rules. */
    .track-row.track-row-group:hover .track-track-group,
    .track-row.track-row-group.active .track-track-group {
      background: var(--grp-hl-bg, transparent);
      box-shadow: var(--grp-hl-shadow, none);
      transition: background 0.12s, box-shadow 0.12s;
    }

    /* Visibility window — hatched off-stage region + endpoint guides.
       Authored via dragging the guides on each layer's row; the
       hatched area shows the segment where the layer is NOT visible.
       The existing #VISIBLE checkbox in the panel is the master
       kill switch — when off, the layer is hidden entirely (these
       markers stop rendering). */
    .track-track .visibility-offstage {
      position: absolute;
      top: 0; bottom: 0;
      background:
        repeating-linear-gradient(
          135deg,
          rgba(255, 255, 255, 0.14) 0,
          rgba(255, 255, 255, 0.14) 1.5px,
          transparent 1.5px,
          transparent 6px
        ),
        rgba(0, 0, 0, 0.55);
      z-index: 1;
      pointer-events: none;
    }
    /* The marker is now a wide-hit-zone wrapper containing a thin
       visible line. 14px hit zone makes the marker easy to grab
       even when it's tucked against the timeline edge; the 2px
       line still reads as a precise tick mark visually. */
    .track-track .visibility-endpoint {
      position: absolute;
      top: 0; bottom: 0;
      width: 14px;
      margin-left: -7px;     /* center the hit zone on the time-position */
      cursor: ew-resize;
      z-index: 4;
      background: transparent;
    }
    .track-track .visibility-endpoint .visibility-endpoint-line {
      position: absolute;
      left: 50%;
      top: 0; bottom: 0;
      width: 2px;
      margin-left: -1px;
      background: rgba(245, 200, 66, 0.85);
      box-shadow: 0 0 4px rgba(245, 200, 66, 0.4);
      pointer-events: none;
    }
    .track-track .visibility-endpoint .visibility-endpoint-line::before,
    .track-track .visibility-endpoint .visibility-endpoint-line::after {
      content: '';
      position: absolute;
      left: 50%;
      width: 7px;
      height: 4px;
      background: rgba(245, 200, 66, 0.85);
      transform: translateX(-50%);
    }
    .track-track .visibility-endpoint .visibility-endpoint-line::before { top: 0; }
    .track-track .visibility-endpoint .visibility-endpoint-line::after  { bottom: 0; }
    .track-track .visibility-endpoint:hover .visibility-endpoint-line {
      background: rgba(245, 200, 66, 1);
    }
    .track-track .visibility-endpoint:hover .visibility-endpoint-line::before,
    .track-track .visibility-endpoint:hover .visibility-endpoint-line::after {
      background: rgba(245, 200, 66, 1);
    }
    /* Default-position markers (still at row edge, no window set
       yet) render extra-faintly so they don't fight every row's
       readability. Drag inward to "set" them — they brighten via
       the .is-set modifier. Right-click resets back to default. */
    .track-track .visibility-endpoint:not(.is-set) .visibility-endpoint-line {
      background: rgba(245, 200, 66, 0.20);
      box-shadow: none;
    }
    .track-track .visibility-endpoint:not(.is-set) .visibility-endpoint-line::before,
    .track-track .visibility-endpoint:not(.is-set) .visibility-endpoint-line::after {
      background: rgba(245, 200, 66, 0.20);
    }
    .track-track .visibility-endpoint:not(.is-set):hover .visibility-endpoint-line {
      background: rgba(245, 200, 66, 0.55);
    }
    .track-track .visibility-endpoint:not(.is-set):hover .visibility-endpoint-line::before,
    .track-track .visibility-endpoint:not(.is-set):hover .visibility-endpoint-line::after {
      background: rgba(245, 200, 66, 0.55);
    }

    /* Plain bar — no inset highlight or drop shadow; either tends to
       extend the visual edge into the resize-grab zone and makes the
       left/right edges harder to grab when stretching the bar. */
    .track-bar {
      position: absolute;
      top: 0;
      bottom: 0;
      border-radius: 6px;
      cursor: grab;
      display: flex;
      align-items: center;
      padding: 0 6px;
      font-family: var(--mono);
      font-weight: 600;
      font-size: 10px;
      color: #fff;
      white-space: nowrap;
      overflow: hidden;
      transition: filter 0.12s ease, box-shadow 0.12s ease;
      letter-spacing: 0.1em;
      /* High-quality pill polish: hairline top highlight (light catching
         the top edge), hairline bottom inner shadow (subtle depth), and
         a soft 1px outer drop shadow to lift the pill off the dark
         track. No visible stroke in any state — selection / hover are
         signaled by brightness + saturation only. */
      box-shadow:
        inset 0  1px 0 rgba(255, 255, 255, 0.18),
        inset 0 -1px 0 rgba(0, 0, 0, 0.20),
        0 1px 2px rgba(0, 0, 0, 0.40);
    }

    .track-bar:hover    { filter: brightness(1.10) saturate(1.05); }
    .track-bar.dragging { cursor: grabbing; filter: brightness(1.18) saturate(1.10); }

    /* Resize-handle hint shown on hover — two thin white vertical bars
       near each end of the pill, signaling the bar is drag-stretchable.
       Implemented as a single ::before pseudo inset from the bar's
       padded edges, with white left + right borders so both handles
       render from one element (leaves ::after free for the exit-bar
       stripe overlay). Fades in on hover; stays visible while dragging
       so the user keeps seeing what edge they grabbed. */
    .track-bar::before {
      content: '';
      position: absolute;
      top: 3px;
      bottom: 3px;
      left: 5px;
      right: 5px;
      pointer-events: none;
      border-left: 2px solid #ffffff;
      border-right: 2px solid #ffffff;
      opacity: 0;
      transition: opacity 0.1s;
    }
    .track-bar:hover::before,
    .track-bar.dragging::before {
      opacity: 0.85;
    }
    .track-bar.selected {
      /* Selection = brighter + a bit more saturated + a stronger top
         highlight + slightly heavier lift. No stroke, no ring. The
         bar reads as "lit up" rather than "outlined." */
      box-shadow:
        inset 0  1px 0 rgba(255, 255, 255, 0.35),
        inset 0 -1px 0 rgba(0, 0, 0, 0.25),
        0 1px 4px rgba(0, 0, 0, 0.55);
      filter: brightness(1.22) saturate(1.15);
    }
    .track-bar.focused {
      filter: brightness(1.16) saturate(1.10);
    }
    .track-bar.focused.selected {
      box-shadow:
        inset 0  1px 0 rgba(255, 255, 255, 0.35),
        inset 0 -1px 0 rgba(0, 0, 0, 0.25),
        0 1px 4px rgba(0, 0, 0, 0.55);
      filter: brightness(1.24) saturate(1.18);
    }
    /* When a bar is selected, dim the rest a tiny bit so the eye latches
       onto the active layer without the unselected bars going invisible.
       Uses :has() at the #track-rows level so the dim applies to ANY
       sibling bar (keyframe-mode bars + rollover bars + classic entry/
       exit bars) regardless of which row holds the selected one. */
    #track-rows:has(.track-bar.selected) .track-bar:not(.selected),
    #track-rows:has(.track-bar.selected) .track-keyframe:not(.selected) {
      opacity: 0.78;
      transition: opacity 0.12s ease;
    }

    /* ── Bar context menu (right-click on track bars) ──
       z-index 9999 so it floats above timeline bars, layer panels,
       header etc. — without the bump, transparent areas of the menu
       let track-row content bleed through visibly. Solid dark
       background (no var-driven translucency) ensures items are
       readable regardless of what's behind. */
    .bar-context-menu {
      position: fixed;
      display: none;
      background: #14121a;
      border: 1px solid rgba(255, 255, 255, 0.16);
      border-radius: 8px;
      padding: 4px 0;
      font-family: var(--mono);
      font-size: 0.62rem;
      letter-spacing: 0.1em;
      min-width: 180px;
      z-index: 9999;
      box-shadow: 0 8px 28px rgba(0, 0, 0, 0.75);
    }

    .bar-context-menu.show { display: block; }

    /* Bar-hover popover. Long-hover over a track bar pops this card
       with the bar's phase + ease curve glyph + a tidy info column
       (duration, start, end, repeat / yoyo). One unified view —
       previously the native browser tooltip stacked underneath; now
       _showBarHoverPopover strips the bar's `title` while showing so
       no second card appears. */
    .bar-hover-popover {
      position: fixed;
      pointer-events: none;
      background: #14121a;
      border: 1px solid rgba(255, 255, 255, 0.16);
      border-radius: 8px;
      padding: 9px 11px 7px;
      box-shadow: 0 8px 28px rgba(0, 0, 0, 0.75);
      font-family: var(--mono);
      font-size: 10px;
      color: var(--text);
      letter-spacing: 0.04em;
      z-index: 9998;
      opacity: 0;
      transform: translateY(2px);
      transition: opacity 0.12s, transform 0.12s;
      max-width: 260px;
    }
    .bar-hover-popover.show {
      opacity: 1;
      transform: translateY(0);
    }
    .bar-hover-popover .bhp-header {
      font-size: 9px;
      text-transform: uppercase;
      letter-spacing: 0.12em;
      color: var(--muted);
      margin-bottom: 6px;
    }
    .bar-hover-popover .bhp-body {
      display: flex;
      flex-direction: row;
      gap: 10px;
      align-items: center;
    }
    .bar-hover-popover .bhp-curve {
      display: block;
      width: 90px;
      height: 50px;
      flex-shrink: 0;
      background: rgba(0, 0, 0, 0.30);
      border-radius: 4px;
    }
    .bar-hover-popover .bhp-axis {
      stroke: rgba(255, 255, 255, 0.10);
      stroke-width: 1;
      fill: none;
    }
    .bar-hover-popover .bhp-path {
      stroke: var(--accent);
      stroke-width: 1.6;
      fill: none;
      stroke-linecap: round;
      stroke-linejoin: round;
    }
    .bar-hover-popover .bhp-rows {
      display: flex;
      flex-direction: column;
      gap: 3px;
      flex: 1;
      min-width: 0;
    }
    .bar-hover-popover .bhp-row {
      display: flex;
      justify-content: space-between;
      gap: 10px;
      font-size: 9.5px;
    }
    .bar-hover-popover .bhp-key {
      color: var(--muted);
      text-transform: uppercase;
      letter-spacing: 0.10em;
    }
    .bar-hover-popover .bhp-val {
      color: var(--text);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .bar-hover-popover .bhp-hint {
      font-size: 8.5px;
      color: var(--muted);
      letter-spacing: 0.06em;
      margin-top: 6px;
      padding-top: 5px;
      border-top: 1px solid rgba(255, 255, 255, 0.06);
      opacity: 0.75;
    }

    .bar-context-item {
      padding: 7px 14px;
      cursor: pointer;
      color: var(--text);
      transition: background 0.1s;
    }

    .bar-context-sep {
      height: 1px;
      background: var(--border);
      margin: 4px 0;
    }

    .bar-context-item:hover {
      background: rgba(245, 200, 66,0.1);
      color: var(--accent);
    }

    .bar-context-item.disabled {
      color: var(--muted);
      cursor: not-allowed;
      opacity: 0.5;
    }

    .bar-context-item.disabled:hover {
      background: transparent;
      color: var(--muted);
    }

    /* ── Vars panel tabs ── */
    .vars-tabs {
      display: flex;
      gap: 0;
      border-bottom: 1px solid var(--border);
    }

    .vars-tab {
      display: inline-flex;
      align-items: center;
      gap: 5px;
      font-family: var(--mono);
      font-weight: 500;
      font-size: 0.65rem;
      letter-spacing: 0.12em;
      text-transform: uppercase;
      color: var(--muted);
      background: transparent;
      border: none;
      padding: 8px 10px;
      cursor: pointer;
      /* underline lives on .toggle-active-indicator now */
      transition: color 0.2s ease-out;
      white-space: nowrap;
      /* Stack above the absolute sliding indicator. */
      position: relative;
      z-index: 1;
    }

    .vars-tab .vt-icon {
      flex-shrink: 0;
      width: 11px;
      height: 11px;
      opacity: 0.75;
      transition: opacity 0.15s;
    }
    .vars-tab:hover { color: var(--text); }
    .vars-tab:hover .vt-icon { opacity: 1; }

    .vars-tab.active {
      color: var(--accent);
      /* underline now drawn by .toggle-active-indicator--underline */
    }
    .vars-tab.active .vt-icon { opacity: 1; }

    /* ── Animation phase subtabs (Entry / Exit) ──
       Segmented-control feel: a single shared track holding the two pills
       side-by-side, separated by a 1px divider. Active pill fills with a
       muted accent wash, inactive stays glass. */
    /* Persistent under the main vars-tabs row. Compact — sits just below
       the Static/Animation/Rollover row, so it shouldn't add noticeable
       vertical bulk. Hidden by default so the initial Static-tab view
       doesn't flash the pills before activateTab runs; activateTab flips
       to display: flex when the Animation tab is selected. */
    .anim-phase-tabs {
      display: none;
      gap: 0;
      margin-top: 6px;
      margin-bottom: 8px;
      padding: 2px;
      background: rgba(0, 0, 0, 0.25);
      border: 1px solid var(--border);
      border-radius: 6px;
      transition: opacity 0.15s;
    }

    .anim-phase-tab {
      flex: 1;
      background: transparent;
      border: 1px solid transparent;
      color: var(--muted);
      padding: 3px 10px;
      font-family: var(--mono);
      font-weight: 600;
      font-size: 10.5px;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      cursor: pointer;
      border-radius: 4px;
      /* color-only transition — fill is owned by the GSAP indicator */
      transition: color 0.2s ease-out;
      /* Stack above the absolute sliding indicator. */
      position: relative;
      z-index: 1;
    }

    .anim-phase-tab:hover { color: var(--text); }

    .anim-phase-tab.active {
      color: var(--accent);
      /* fill + border + glow now drawn by .toggle-active-indicator--phase */
    }

    /* Exit phase track bars — diagonal stripe pattern to differentiate from entry */
    .track-bar.track-bar-exit::after {
      content: '';
      position: absolute;
      inset: 0;
      background-image: repeating-linear-gradient(
        135deg,
        transparent 0,
        transparent 4px,
        rgba(0,0,0,0.18) 4px,
        rgba(0,0,0,0.18) 8px
      );
      pointer-events: none;
      border-radius: inherit;
    }

    /* ── Rollover-mode track view ──
       When the user clicks the Rollover tab, #track-editor gets the
       .mode-rollover class and the bottom timeline swaps to a per-layer
       rollover view (local time, 0 → r.duration). The master playhead
       is REUSED as the rollover scrub bar so the drag UX is identical
       (drag handle, time label, edge snapping); CSS just retints it
       cyan to signal we're scrubbing rollover, not master time. */
    #track-editor.mode-rollover .track-playhead {
      background: #5fb8c8;
      box-shadow: 0 0 8px rgba(95, 184, 200, 0.4);
    }
    #track-editor.mode-rollover .track-playhead-label {
      background: linear-gradient(180deg, #8fdde7, #5fb8c8);
      color: #0b1d22;
    }
    /* Hide the legacy rollover-scrub marker — superseded by the master
       playhead reuse above. Kept in markup for backward DOM-ref safety. */
    .track-rollover-scrub { display: none !important; }

    /* Hide the HOVER column (header + per-row hover cells) when the
       bottom track editor is in master-timeline mode. The Animation
       tab focuses on entry/exit; rollover-related affordances live
       behind the Rollover tab where this column has its own dedicated
       UI. The HOVER cells were reading visually as "rollover bars" in
       the animation panel even though they're click-to-preview chips,
       which confused users reviewing entry/exit timing. */
    /* HOVER column stays visible in animation mode too — surfaces a
       click-to-preview "⟳ Xs" chip on every layer that has a meaningful
       rollover, so the user can audition the hover state without
       switching tabs. Empty rows (layers with no rollover) keep the
       column reserved so the layer-list / timeline edges stay aligned
       across rows. */

    /* Rollover bar — cyan-tinted edge to match the canvas hover-end overlay,
       so the "this is rollover authoring" affordance is consistent across
       both surfaces. Stripe overlay differs from entry/exit so the user
       never confuses them at a glance. */
    .track-bar.track-bar-rollover {
      box-shadow: inset 0 0 0 1.5px rgba(95, 184, 200, 0.85);
    }
    /* Group-summary variant of the rollover bar — same hover-blue
       inset border as per-layer rollover bars, plus a violet-tinted
       fill so the row reads as "this is a group rollover summary"
       (parallel to .track-bar-group-summary for entry/exit). Mask /
       video / singleton variants override the fill below to match
       the row palette. The base group color uses the same RGB
       triple driving the rest of the group chrome. */
    .track-bar.track-bar-rollover.track-bar-rollover-group {
      background: linear-gradient(180deg, rgba(var(--color-group-rgb), 0.40), rgba(140, 110, 220, 0.50));
      color: rgb(240, 230, 255);
      font-family: var(--mono);
      font-size: 10px;
      letter-spacing: 0.04em;
      cursor: pointer;
    }
    .track-bar.track-bar-rollover.track-bar-rollover-group:hover {
      background: linear-gradient(180deg, rgba(170, 140, 245, 0.55), rgba(150, 120, 230, 0.65));
    }
    .track-bar.track-bar-rollover.track-bar-rollover-group.selected {
      box-shadow:
        inset 0 0 0 1.5px rgba(95, 184, 200, 0.85),
        0 0 0 1px rgba(200, 170, 255, 0.9),
        0 0 8px rgba(var(--color-group-rgb), 0.35);
    }
    .track-bar.track-bar-rollover.track-bar-rollover-group-mask {
      background: linear-gradient(180deg, rgba(94, 198, 132, 0.40), rgba(72, 170, 110, 0.50));
      color: rgb(220, 250, 230);
    }
    .track-bar.track-bar-rollover.track-bar-rollover-group-video {
      background: linear-gradient(180deg, rgba(255, 123, 227, 0.45), rgba(230, 90, 200, 0.55));
      color: rgb(255, 235, 250);
    }
    .track-bar.track-bar-rollover.track-bar-rollover-group-singleton {
      background: linear-gradient(180deg, rgba(245, 200, 66, 0.40), rgba(215, 175, 55, 0.50));
      color: rgb(50, 40, 10);
    }
    .track-bar.track-bar-rollover::after {
      content: '';
      position: absolute;
      inset: 0;
      background-image: repeating-linear-gradient(
        45deg,
        transparent 0,
        transparent 5px,
        rgba(95, 184, 200, 0.22) 5px,
        rgba(95, 184, 200, 0.22) 9px
      );
      pointer-events: none;
      border-radius: inherit;
    }
    /* Per-kf-type ring colors — driven by CSS variables so a future
       palette change is a one-line edit at :root. Each type sets two
       custom properties on itself:
         • --kf-ring        → resting diamond color
         • --kf-ring-bright → .active / .selected (brighter, thicker)
       The base .track-keyframe rule reads --kf-ring for the resting
       shadow, and .active / .selected read --kf-ring-bright. This
       way the per-type ring color flows through .active/.selected
       state changes WITHOUT any specificity wrestling — both states
       inherit the same variable scoped to the kf-type class. */
    .track-keyframe.track-keyframe-entry {
      --kf-ring: var(--kf-entry-ring);
      --kf-ring-bright: var(--kf-entry-ring-bright);
      box-shadow: 0 0 0 1.5px var(--kf-ring);
    }
    .track-keyframe.track-keyframe-exit {
      --kf-ring: var(--kf-exit-ring);
      --kf-ring-bright: var(--kf-exit-ring-bright);
      box-shadow: 0 0 0 1.5px var(--kf-ring);
    }
    .track-keyframe.track-keyframe-rollover {
      --kf-ring: var(--kf-hover-ring);
      --kf-ring-bright: var(--kf-hover-ring-bright);
      box-shadow: 0 0 0 1.5px var(--kf-ring);
    }
    /* Mask path keyframes — green theme to match the MASK badge +
       vector-edit overlay so everywhere a "mask thing" appears, it
       reads with the same color identity. Mask kfs OVERRIDE the
       per-layer fill (masks don't have a meaningful layer color). */
    .track-keyframe.track-keyframe-mask {
      --kf-ring: var(--kf-mask-ring);
      --kf-ring-bright: var(--kf-mask-ring-bright);
      background: var(--kf-mask-fill) !important;
      box-shadow: 0 0 0 1.5px var(--kf-ring);
    }
    .track-keyframe.track-keyframe-mask.active,
    .track-keyframe.track-keyframe-mask.selected {
      background: #fff !important;
      box-shadow: 0 0 0 2px var(--kf-ring-bright), 0 0 8px var(--kf-ring-bright);
    }
    .track-bar.track-bar-mask {
      background: rgba(94, 198, 132, 0.18) !important;
      border: 1px solid var(--color-mask-border);
      color: rgba(180, 240, 200, 0.9);
    }
    /* Highlight the mask track row that's currently being point-edited.
       Subtle outer glow so the user can spot at a glance which row
       their vector-edit session is bound to. */
    .track-row.vector-editing-row {
      box-shadow: inset 0 0 0 1px rgba(94, 198, 132, 0.55),
                  0 0 12px -4px rgba(94, 198, 132, 0.6);
      background: rgba(94, 198, 132, 0.05);
    }

    /* ── Mask animation panel (right-pane takeover during vector edit) ── */
    /* Hide the normal vars-panel tab content + tabs when vector-edit mode
       is active. The mask-animation-panel below sits in the same DOM
       slot and renders its own content. Editor-mode / Static/Animation/
       Rollover tabs are hidden too — they're irrelevant for a mask. */
    body.vector-edit-active #vars-panel .vars-tabs,
    body.vector-edit-active #vars-panel .anim-phase-tabs,
    body.vector-edit-active #vars-panel #vars-static,
    body.vector-edit-active #vars-panel #vars-anim,
    body.vector-edit-active #vars-panel #vars-rollover {
      display: none !important;
    }
    /* Header Simple/Advanced toggle stays visible during vector-edit
       — the user can still meaningfully flip authoring mode (kf data
       across the rest of the banner) while focused on a single
       vector path. Previously hidden alongside the vars-panel tabs,
       which made the header feel like it was "missing" controls. */
    body.vector-edit-active #vars-panel #mask-animation-panel {
      display: block;
    }
    #mask-animation-panel {
      padding: 12px 14px 18px;
      color: var(--text);
    }
    .mask-anim-header { margin-bottom: 14px; }
    .mask-anim-title-row {
      display: flex;
      justify-content: space-between;
      align-items: center;
      gap: 8px;
      margin-bottom: 4px;
    }
    .mask-anim-title {
      font-family: var(--mono);
      font-weight: 700;
      font-size: 12px;
      letter-spacing: 0.12em;
      color: rgba(94, 198, 132, 0.95);
      flex: 1 1 auto;
      text-align: center;
      min-width: 0;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    /* Back-crumb chip — sibling of the title + Done button. Shows the
       layer name being edited and exits vector-edit mode on click.
       Visually paired with the Done button (same height, same glass
       treatment) so the row reads as bracketing exits on both sides
       with the title in the middle. */
    .mask-anim-back-btn {
      display: inline-flex;
      align-items: center;
      gap: 4px;
      font-family: var(--mono);
      font-weight: 500;
      font-size: 10.5px;
      color: rgba(220, 230, 225, 0.7);
      background: rgba(255, 255, 255, 0.04);
      border: 1px solid rgba(255, 255, 255, 0.1);
      border-radius: 5px;
      padding: 3px 8px 3px 6px;
      max-width: 130px;
      min-width: 0;
      cursor: pointer;
      transition: background 0.15s, color 0.15s, border-color 0.15s;
    }
    .mask-anim-back-btn:hover {
      background: rgba(94, 198, 132, 0.1);
      color: rgb(170, 240, 200);
      border-color: rgba(94, 198, 132, 0.4);
    }
    .mask-anim-back-arrow {
      font-family: var(--sans);
      font-size: 12px;
      opacity: 0.85;
    }
    .mask-anim-back-name {
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      min-width: 0;
    }
    .mask-anim-done-btn {
      font-family: var(--sans);
      font-weight: 600;
      font-size: 10.5px;
      color: rgba(220, 230, 225, 0.9);
      background: rgba(255, 255, 255, 0.06);
      border: 1px solid rgba(255, 255, 255, 0.12);
      border-radius: 5px;
      padding: 3px 9px;
      cursor: pointer;
    }
    .mask-anim-done-btn:hover { background: rgba(255, 255, 255, 0.1); }
    .mask-anim-subtitle {
      font-family: var(--mono);
      font-size: 10.5px;
      color: rgba(220, 230, 225, 0.6);
    }
    .mask-anim-section {
      margin-bottom: 14px;
    }
    .mask-anim-section .anim-section-label {
      font-family: var(--mono);
      font-weight: 600;
      font-size: 10px;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      color: rgba(220, 230, 225, 0.55);
      margin-bottom: 6px;
    }
    /* Keyframe-mode pill — gold when on, hollow when off. Mirrors the
       entry/exit kf-mode banner so the affordance is familiar. */
    .mask-anim-kf-toggle {
      display: flex;
      align-items: center;
      gap: 8px;
      width: 100%;
      font-family: var(--mono);
      font-weight: 600;
      font-size: 11px;
      letter-spacing: 0.08em;
      text-align: left;
      padding: 8px 12px;
      border-radius: 8px;
      cursor: pointer;
      transition: all 0.15s;
      background: rgba(94, 198, 132, 0.06);
      border: 1px solid rgba(94, 198, 132, 0.25);
      color: rgba(220, 230, 225, 0.85);
    }
    .mask-anim-kf-toggle.on {
      background: linear-gradient(180deg, rgba(120, 220, 160, 0.18), rgba(94, 198, 132, 0.22));
      border-color: rgba(94, 198, 132, 0.8);
      color: rgb(170, 240, 200);
    }
    .mask-anim-kf-toggle-dot {
      width: 10px; height: 10px;
      border-radius: 50%;
      background: rgba(220, 230, 225, 0.25);
      border: 1px solid rgba(220, 230, 225, 0.4);
      flex-shrink: 0;
    }
    .mask-anim-kf-toggle.on .mask-anim-kf-toggle-dot {
      background: rgb(94, 198, 132);
      border-color: rgb(140, 230, 175);
      box-shadow: 0 0 6px rgba(94, 198, 132, 0.7);
    }
    /* "+ Add mask keyframe at X.XXs" button — large green capture
       affordance. Mirrors the entry/exit kf-add buttons. */
    .mask-anim-add-kf {
      display: block;
      width: 100%;
      font-family: var(--mono);
      font-weight: 600;
      font-size: 11px;
      letter-spacing: 0.06em;
      text-align: center;
      padding: 8px 12px;
      border-radius: 8px;
      cursor: pointer;
      background: rgba(94, 198, 132, 0.08);
      border: 1px dashed rgba(94, 198, 132, 0.4);
      color: rgba(170, 240, 200, 0.95);
      transition: all 0.15s;
      margin-bottom: 8px;
    }
    .mask-anim-add-kf:hover {
      background: rgba(94, 198, 132, 0.14);
      border-color: rgba(94, 198, 132, 0.6);
    }
    #mask-anim-add-kf-time {
      color: rgb(170, 240, 200);
      font-weight: 700;
    }
    /* Per-kf cards in the mask-animation panel. Smaller-footprint than
       the entry/exit kf cards because each card carries only time +
       summary + delete (no per-prop value grid — the per-anchor inputs
       live in their own section below). */
    #mask-anim-kf-list .mask-anim-kf-card {
      display: flex;
      align-items: center;
      gap: 8px;
      padding: 6px 10px;
      background: rgba(94, 198, 132, 0.04);
      border: 1px solid rgba(94, 198, 132, 0.18);
      border-radius: 8px;
      margin-bottom: 4px;
      transition: all 0.12s;
    }
    #mask-anim-kf-list .mask-anim-kf-card:hover {
      border-color: rgba(94, 198, 132, 0.4);
      background: rgba(94, 198, 132, 0.08);
    }
    #mask-anim-kf-list .mask-anim-kf-card.active {
      border-color: rgb(94, 198, 132);
      background: rgba(94, 198, 132, 0.15);
      box-shadow: 0 0 0 1px rgba(94, 198, 132, 0.6), 0 0 12px rgba(94, 198, 132, 0.25);
    }
    .mask-anim-kf-diamond {
      width: 10px; height: 10px;
      transform: rotate(45deg);
      background: rgba(94, 198, 132, 0.95);
      border: 1px solid rgba(45, 130, 80, 0.85);
      flex-shrink: 0;
    }
    .mask-anim-kf-info {
      flex: 1; min-width: 0;
      font-family: var(--mono);
      font-size: 11px;
      line-height: 1.3;
    }
    .mask-anim-kf-time {
      color: rgb(170, 240, 200);
      font-weight: 600;
    }
    .mask-anim-kf-summary {
      color: rgba(220, 230, 225, 0.55);
      font-size: 10.5px;
    }
    .mask-anim-kf-delete {
      width: 22px; height: 22px;
      font-family: var(--sans);
      font-weight: 500;
      font-size: 13px;
      color: rgba(220, 230, 225, 0.5);
      background: transparent;
      border: 1px solid rgba(255, 255, 255, 0.08);
      border-radius: 5px;
      cursor: pointer;
      flex-shrink: 0;
      transition: all 0.12s;
    }
    .mask-anim-kf-delete:hover {
      color: #ff8e8e;
      border-color: rgba(255, 142, 142, 0.4);
      background: rgba(255, 142, 142, 0.08);
    }
    /* Per-anchor inputs — 3 rows of 2 number inputs (anchor / handle-in /
       handle-out × x,y). Layout mirrors the entry/exit kf-values grid. */
    .mask-anim-anchor-grid {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 6px 10px;
    }
    .mask-anim-anchor-grid label {
      grid-column: span 2;
      font-family: var(--mono);
      font-size: 10px;
      letter-spacing: 0.06em;
      text-transform: uppercase;
      color: rgba(220, 230, 225, 0.55);
      margin-top: 4px;
    }
    .mask-anim-anchor-grid input {
      font-family: var(--mono);
      font-size: 11.5px;
      padding: 4px 8px;
      background: rgba(0, 0, 0, 0.3);
      border: 1px solid rgba(255, 255, 255, 0.08);
      border-radius: 5px;
      color: var(--text);
      width: 100%;
      min-width: 0;
    }
    .mask-anim-anchor-grid input:focus {
      outline: none;
      border-color: rgba(94, 198, 132, 0.6);
      background: rgba(0, 0, 0, 0.5);
    }
    /* Axis-prefix wrapper — flex row with a one-letter X / Y label
       on the left and the number input filling the rest. Makes each
       field self-describing instead of relying on column position. */
    .mask-anim-anchor-input-wrap {
      display: flex;
      align-items: center;
      gap: 6px;
      min-width: 0;
    }
    .mask-anim-anchor-axis {
      font-family: var(--mono);
      font-size: 10px;
      font-weight: 600;
      letter-spacing: 0.08em;
      color: rgba(220, 230, 225, 0.5);
      flex-shrink: 0;
      width: 10px;
      text-align: right;
    }
    /* Mask preset grid — mirrors the Simple-mode entry/exit preset grid
       (.preset-thumb) but in green via --color-mask. Each card has an
       animated SVG preview that loops while the grid is hovered, plus
       a two-step click (first click → .selected with green "APPLY"
       pill label, second click → commits). 3-col grid like the other
       preset grids in the editor. */
    .mask-anim-preset-grid {
      display: grid;
      grid-template-columns: repeat(3, minmax(0, 1fr));
      gap: 8px;
      margin-top: 8px;
      margin-bottom: 10px;
    }
    /* The card itself reuses .preset-thumb's structure (set on the
       element alongside .preset-thumb-mask). The .preset-thumb-mask
       modifier swaps every gold accent for green. Selectors below
       qualify with BOTH classes (specificity 0,3,0 minimum) so they
       beat the global .preset-thumb:hover / .preset-thumb.selected
       rules further down the stylesheet — those would otherwise win
       on equal-specificity-source-order and the cards would render
       yellow. */
    .preset-thumb.preset-thumb-mask:hover {
      background: rgba(94, 198, 132, 0.08);
      border-color: rgba(94, 198, 132, 0.40);
    }
    .preset-thumb.preset-thumb-mask.selected,
    .preset-thumb.preset-thumb-mask.applied {
      border-color: var(--color-mask);
      background: rgba(94, 198, 132, 0.12);
      box-shadow: 0 0 0 1px rgba(94, 198, 132, 0.55),
                  0 0 14px -2px rgba(94, 198, 132, 0.45);
    }
    /* "APPLY" pill label flips to a green gradient for the mask variant. */
    .preset-thumb.preset-thumb-mask.selected .preset-thumb-label {
      font-family: var(--mono);
      font-weight: 600;
      font-size: 9.5px;
      letter-spacing: 0.12em;
      text-transform: uppercase;
      color: #06200f;
      background: linear-gradient(180deg, #9ce7b7, #5ec684);
      border: 1px solid rgba(94, 198, 132, 0.65);
      border-radius: 6px;
      padding: 4px 6px;
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.35),
                  0 4px 12px rgba(94, 198, 132, 0.25);
    }
    .preset-thumb.preset-thumb-mask.selected:hover .preset-thumb-label {
      background: linear-gradient(180deg, #aef0c7, #6dd193);
    }
    /* Preview shape tint — drives stroke + fill of the animated SVG
       element inside .preset-thumb-stage for mask cards. */
    .preset-thumb-mask .mask-preview-shape {
      fill: var(--color-mask-fill, rgba(94, 198, 132, 0.95));
      stroke: rgba(140, 220, 170, 0.9);
      stroke-width: 1.2;
      filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.4))
              drop-shadow(0 0 4px rgba(94, 198, 132, 0.4));
    }
    .preset-thumb-mask .mask-preview-shape.is-stroke {
      fill: none;
      stroke-width: 2.2;
      stroke-linecap: round;
      stroke-linejoin: round;
    }
    /* Hint area below the grid — unchanged from the prior design. */
    .mask-anim-preset-hint {
      font-family: var(--sans);
      font-size: 10px;
      color: rgba(220, 230, 225, 0.45);
      line-height: 1.4;
      margin-top: 6px;
    }
    /* Stroke draw range row — dual-handle range slider. Two stacked
       <input type=range> overlay a shared track + a fill bar that
       paints between the two thumb positions. The inputs themselves
       have transparent tracks (pointer-events: none) so the thumbs
       are the only interactive bit and the underlying track shows
       through unimpeded. The fill bar paints the [tail, head] window
       in green so the user sees the visible-stroke range at a glance.
       % readout on the right shows "tail–head%". */
    .mask-anim-draw-row {
      display: flex;
      align-items: center;
      gap: 10px;
    }
    .mask-anim-draw-dual {
      position: relative;
      flex: 1;
      height: 24px;
    }
    .mask-anim-draw-track {
      position: absolute;
      top: 50%;
      left: 0; right: 0;
      transform: translateY(-50%);
      height: 4px;
      background: rgba(255, 255, 255, 0.08);
      border-radius: 2px;
      pointer-events: none;
    }
    .mask-anim-draw-fill {
      position: absolute;
      top: 50%;
      transform: translateY(-50%);
      left: 0;
      width: 100%;
      height: 4px;
      background: rgba(94, 198, 132, 0.55);
      border-radius: 2px;
      pointer-events: none;
    }
    .mask-anim-draw-input {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 24px;
      margin: 0;
      background: transparent;
      pointer-events: none;
      -webkit-appearance: none;
      appearance: none;
    }
    .mask-anim-draw-input:focus { outline: none; }
    /* Handlebar-style thumbs — vertical bars that face inward toward
       the fill, like a video-trim selector or the iOS dual-range
       pattern. Tail is rounded on the LEFT side (so the inward-
       facing right edge is a flat "grab" surface); head mirrors with
       the rounded side on the RIGHT. Slightly taller than the track
       so they read as separate "handles" you can grip. Pointer-events
       on the thumbs only so the track passes clicks through to
       whichever range is below in z-order. */
    .mask-anim-draw-input::-webkit-slider-runnable-track,
    .mask-anim-draw-input::-moz-range-track {
      background: transparent;
      height: 24px;
    }
    .mask-anim-draw-input::-webkit-slider-thumb {
      -webkit-appearance: none;
      appearance: none;
      width: 8px;
      height: 18px;
      background: rgb(170, 240, 200);
      border: 1.5px solid rgb(58, 130, 88);
      box-shadow: 0 0 5px rgba(94, 198, 132, 0.55);
      cursor: ew-resize;
      pointer-events: auto;
    }
    .mask-anim-draw-input::-moz-range-thumb {
      width: 8px;
      height: 18px;
      background: rgb(170, 240, 200);
      border: 1.5px solid rgb(58, 130, 88);
      box-shadow: 0 0 5px rgba(94, 198, 132, 0.55);
      cursor: ew-resize;
      pointer-events: auto;
    }
    /* Tail (left) handle — rounded on the LEFT, flat right edge
       faces the fill. */
    .mask-anim-draw-tail::-webkit-slider-thumb {
      border-radius: 4px 1px 1px 4px;
    }
    .mask-anim-draw-tail::-moz-range-thumb {
      border-radius: 4px 1px 1px 4px;
    }
    /* Head (right) handle — mirror image. */
    .mask-anim-draw-head::-webkit-slider-thumb {
      border-radius: 1px 4px 4px 1px;
    }
    .mask-anim-draw-head::-moz-range-thumb {
      border-radius: 1px 4px 4px 1px;
    }
    /* Subtle hover lift so users see the thumbs are draggable
       without needing the tooltip. */
    .mask-anim-draw-input:hover::-webkit-slider-thumb {
      background: rgb(200, 250, 220);
      box-shadow: 0 0 7px rgba(94, 198, 132, 0.75);
    }
    .mask-anim-draw-input:hover::-moz-range-thumb {
      background: rgb(200, 250, 220);
      box-shadow: 0 0 7px rgba(94, 198, 132, 0.75);
    }
    /* Head thumb sits slightly higher in z-order — when the two
       overlap, the head wins clicks; if the user wants to grab the
       tail they can drag the head away first. (Simpler than a smart
       "nearest thumb" handoff and matches how every other dual-range
       implementation in the wild behaves.) */
    .mask-anim-draw-head { z-index: 2; }
    .mask-anim-draw-tail { z-index: 1; }
    .mask-anim-draw-val {
      font-family: var(--mono);
      font-size: 11px;
      font-weight: 600;
      color: rgb(170, 240, 200);
      min-width: 58px;
      text-align: right;
    }
    /* Per-kf ease dropdown inside each card. Mirrors the entry/exit
       kf cards' inline ease select — same width, same monospace
       font, same subtle dark-on-dark styling so it doesn't fight the
       diamond + time + summary for attention. */
    .mask-anim-kf-ease {
      flex-shrink: 0;
      width: 96px;
      font-family: var(--mono);
      font-size: 10.5px;
      padding: 3px 6px;
      background: rgba(0, 0, 0, 0.3);
      border: 1px solid rgba(255, 255, 255, 0.08);
      border-radius: 5px;
      color: rgba(220, 230, 225, 0.85);
      cursor: pointer;
    }
    .mask-anim-kf-ease:focus {
      outline: none;
      border-color: rgba(94, 198, 132, 0.6);
    }
    /* Highlight the ease select when it's set to anything other than
       'none' — visual confirmation that the value is applied. Linear
       ease shows in the muted neutral; any non-linear curve gets the
       green-tinted active state. */
    .mask-anim-kf-ease.set {
      color: rgb(170, 240, 200);
      border-color: rgba(94, 198, 132, 0.5);
      background: rgba(94, 198, 132, 0.08);
      font-weight: 600;
    }
    /* Mask track-bar extension — visualizes the repeat / yoyo cycles
       past the actual kf chain, mirroring the entry/exit pattern. The
       core bar stays editable; the extension is informational only
       (pointer-events:none so drags target the editable region). */
    .track-bar.track-bar-mask.track-bar-extend {
      opacity: 0.55;
      background: rgba(94, 198, 132, 0.12) !important;
      border-style: dashed;
      pointer-events: none;
    }
    .track-bar.track-bar-mask.track-bar-extend.track-bar-extend-infinite {
      background: repeating-linear-gradient(
        45deg,
        rgba(94, 198, 132, 0.16),
        rgba(94, 198, 132, 0.16) 6px,
        rgba(94, 198, 132, 0.05) 6px,
        rgba(94, 198, 132, 0.05) 12px
      ) !important;
    }
    .track-bar.track-bar-mask.track-bar-extend.track-bar-extend-yoyo {
      background: repeating-linear-gradient(
        -45deg,
        rgba(94, 198, 132, 0.16),
        rgba(94, 198, 132, 0.16) 6px,
        rgba(94, 198, 132, 0.05) 6px,
        rgba(94, 198, 132, 0.05) 12px
      ) !important;
    }
    /* Repeat + Yoyo controls row — 2-column grid that hosts the same
       var-field / var-label / var-input + stepper / var-check-wrap +
       yoyo-hint markup as the entry/exit anim-meta grid. All visual
       styling comes from those shared classes; this row only owns
       the layout (two columns side-by-side). */
    .mask-anim-meta-row {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 14px;
      align-items: start;
    }
    /* Visually dim the yoyo hint when repeat ≥ 1 (toggle is no longer
       a no-op) so the hint only reads as active when relevant. */
    .mask-anim-meta-row.repeat-ok .yoyo-hint {
      opacity: 0.25;
    }
    .track-bar-rollover-empty {
      height: 18px;
      line-height: 18px;
      padding: 0 8px;
      font-size: 11px;
      color: rgba(95, 184, 200, 0.7);
      border: 1px dashed rgba(95, 184, 200, 0.4);
      border-radius: 4px;
      cursor: pointer;
      width: fit-content;
      max-width: 160px;
      display: inline-block;
      transition: all 0.15s;
    }
    .track-bar-rollover-empty:hover {
      color: rgba(95, 184, 200, 1);
      border-color: rgba(95, 184, 200, 0.8);
      background: rgba(95, 184, 200, 0.08);
    }

    /* ── Animation sub-sections ── */
    .anim-section {
      margin-top: 10px;
    }

    .anim-section-label {
      display: flex;
      align-items: center;
      gap: 6px;
      font-family: var(--mono);
      font-weight: 600;
      font-size: 11px;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      color: var(--muted);
      margin-bottom: 5px;
    }

    /* Section-prelude title — sits ABOVE an anim-section-label and
       names the active selection ("GROUP 45 TIMELINE PROPERTIES").
       Slightly larger + brighter than the section label so the
       reading order is: name → section → fields. Spans the full
       panel width, separated from anything above by a divider so
       the section reads as its own concern. Uppercased to match
       the section-label visual family. */
    .timeline-props-title {
      display: flex;
      align-items: center;
      gap: 6px;
      font-family: var(--mono);
      font-weight: 600;
      font-size: 12px;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      color: var(--text);
      margin-bottom: 10px;
      padding-top: 12px;
      border-top: 1px solid var(--border);
    }

    /* Inline icon shared between anim-section-label and
       timeline-props-title. currentColor lets the parent's color
       (muted for section labels, text for titles) drive the stroke
       so the icon visually belongs to whichever heading it's in. */
    .anim-section-icon {
      flex-shrink: 0;
      opacity: 0.85;
    }

    .anim-section-hint {
      font-family: var(--mono);
      font-size: 11px;
      color: var(--muted);
      letter-spacing: 0.1em;
      line-height: 1.5;
      margin-bottom: 8px;
    }

    .anim-section-hint code {
      background: var(--input-bg);
      border: 1px solid var(--border);
      border-radius: 6px;
      padding: 1px 5px;
      font-size: 0.95em;
      color: var(--accent);
      font-family: var(--mono);
    }

    .var-input.anim-position {
      font-family: var(--mono);
    }

    /* ── On-canvas origin overlay ── */
    #origin-overlay {
      display: none;
      position: absolute;
      inset: 0;
      pointer-events: none;  /* let canvas events through; child opts in */
    }

    #origin-overlay.show { display: block; }

    /* ── Vector-edit overlay (mask path editing) ──
       Sits over the canvas like #origin-overlay; dots are positioned
       via percentage of the container so they track zoom + pan
       automatically. While active, the layer's bbox + scale/rotate
       handles fade out so the user's focus is the anchor points
       (matches Figma's "enter vector mode collapses the bbox"
       affordance). */
    #vector-edit-overlay {
      position: absolute;
      inset: 0;
      pointer-events: none;
    }
    #canvas-container.vector-editing #origin-overlay #origin-bounds,
    #canvas-container.vector-editing #origin-overlay #from-bounds,
    #canvas-container.vector-editing #origin-overlay #exit-bounds,
    #canvas-container.vector-editing #origin-overlay #hover-bounds,
    #canvas-container.vector-editing #origin-overlay #rest-label,
    #canvas-container.vector-editing #origin-overlay #from-label,
    #canvas-container.vector-editing #origin-overlay #exit-label,
    #canvas-container.vector-editing #origin-overlay #hover-label,
    #canvas-container.vector-editing #origin-overlay #origin-dot,
    #canvas-container.vector-editing #origin-overlay .xform-handle,
    #canvas-container.vector-editing #origin-overlay .rotate-handle {
      display: none !important;
    }

    /* Group-selection mode: a single union bbox stands in for all
       members. The per-layer animation affordances (FROM / EXIT
       alt-pose bboxes, their labels, connectors) don't have a
       coherent group-level meaning in v1, so hide them. The xform
       scale handles + rotate handle + origin-dot DO show, wired
       to operate on group.variables so the user can scale + rotate
       the group as a single unit (Phase 7).

       Specificity dance: the mod-alt-held / mod-ctrl-held rules
       (#origin-overlay.mod-alt-held #from-bounds.mod-elevated,
       liquid.css:1671) reveal the FROM / EXIT bboxes when alt or
       cmd is held — and outweigh a plain `body.group-selected
       #from-bounds` selector via the extra `.mod-elevated` class.
       The mod-aware selectors below explicitly out-specify so the
       group's "no per-member animation chrome" rule wins even when
       the user is holding a modifier key. */
    body.group-selected #origin-overlay #from-bounds,
    body.group-selected #origin-overlay #exit-bounds,
    body.group-selected #origin-overlay #hover-bounds,
    body.group-selected #origin-overlay #from-label,
    body.group-selected #origin-overlay #exit-label,
    body.group-selected #origin-overlay #hover-label,
    body.group-selected #origin-overlay #from-connector,
    body.group-selected #origin-overlay #from-connector-arrow,
    body.group-selected #origin-overlay #exit-connector,
    body.group-selected #origin-overlay #exit-connector-arrow,
    body.group-selected #origin-overlay.mod-alt-held #from-bounds.mod-elevated,
    body.group-selected #origin-overlay.mod-alt-held #from-label,
    body.group-selected #origin-overlay.mod-ctrl-held #exit-bounds.mod-elevated,
    body.group-selected #origin-overlay.mod-ctrl-held #exit-label,
    /* Multi-select inherits the same "hide per-layer animation chrome"
       rule — only the cyan union bbox + MULTI pill should be visible. */
    body.multi-selected #origin-overlay #from-bounds,
    body.multi-selected #origin-overlay #exit-bounds,
    body.multi-selected #origin-overlay #hover-bounds,
    body.multi-selected #origin-overlay #from-label,
    body.multi-selected #origin-overlay #exit-label,
    body.multi-selected #origin-overlay #hover-label,
    body.multi-selected #origin-overlay #from-connector,
    body.multi-selected #origin-overlay #from-connector-arrow,
    body.multi-selected #origin-overlay #exit-connector,
    body.multi-selected #origin-overlay #exit-connector-arrow,
    body.multi-selected #origin-overlay.mod-alt-held #from-bounds.mod-elevated,
    body.multi-selected #origin-overlay.mod-alt-held #from-label,
    body.multi-selected #origin-overlay.mod-ctrl-held #exit-bounds.mod-elevated,
    body.multi-selected #origin-overlay.mod-ctrl-held #exit-label {
      display: none !important;
    }
    /* Violet bbox outline + label to match the layers panel's group
       row styling. The default REST bbox is yellow (accent); the
       override colors here keep the visual language consistent so
       the user sees "this is a group" at a glance. */
    body.group-selected #origin-overlay #origin-bounds,
    #origin-overlay #origin-bounds.is-group {
      border-color: rgba(var(--color-group-rgb), 0.75) !important;
      background: rgba(var(--color-group-rgb), 0.04) !important;
      /* Replace the base yellow outer glow with a violet one — the
         base box-shadow at #origin-bounds bleeds past the dashed
         border and reads as a yellow outline if not overridden. */
      box-shadow:
        0 0 0 1px rgba(0,0,0,0.55) inset,
        0 0 0 1px rgba(0,0,0,0.55),
        0 0 10px rgba(var(--color-group-rgb), 0.45) !important;
    }
    body.group-selected #origin-overlay #rest-label,
    #origin-overlay #rest-label.is-group {
      /* Solid dark background like the default yellow pill — the
         translucent violet background was washing out against the
         canvas (especially over bright artwork), making the name
         unreadable. Tint stays in the border + text color. */
      background: #2a2a2a !important;
      border-color: var(--color-group) !important;
      color: var(--color-group-bright) !important;
    }
    body.group-selected #origin-overlay #rest-label::before,
    #origin-overlay #rest-label.is-group::before {
      background: var(--color-group) !important;
    }
    /* Mask-containing groups use the green mask palette instead of the
       violet group palette — mirrors the green track dot + MASK badge
       so the canvas bbox reads as "this is a mask group" at a glance. */
    body.group-selected.group-mask-selected #origin-overlay #origin-bounds,
    #origin-overlay #origin-bounds.is-mask-group {
      border-color: var(--color-mask-ring) !important;
      background: var(--color-mask-soft) !important;
      /* Same fix as the group bbox — swap the base yellow outer glow
         to green so the mask bbox reads as fully green. */
      box-shadow:
        0 0 0 1px rgba(0,0,0,0.55) inset,
        0 0 0 1px rgba(0,0,0,0.55),
        0 0 10px rgba(94, 198, 132, 0.45) !important;
    }
    body.group-selected.group-mask-selected #origin-overlay #rest-label,
    #origin-overlay #rest-label.is-mask-group {
      background: #2a2a2a !important;
      border-color: var(--color-mask) !important;
      color: var(--color-mask) !important;
    }
    body.group-selected.group-mask-selected #origin-overlay #rest-label::before,
    #origin-overlay #rest-label.is-mask-group::before {
      background: var(--color-mask) !important;
    }
    /* Video group canvas chrome — hot-pink palette wins over the mask
       / regular group palette when group.isVideo is set. Bbox border
       + chip stroke + chip text + chip dot + transform handles all
       switch to #ff7be3. Matches the layer-panel + timeline row. */
    body.group-selected.group-video-selected #origin-overlay #origin-bounds,
    #origin-overlay #origin-bounds.is-video-group {
      border-color: rgba(255, 123, 227, 0.75) !important;
      background: rgba(255, 123, 227, 0.04) !important;
      box-shadow:
        0 0 0 1px rgba(0, 0, 0, 0.55) inset,
        0 0 0 1px rgba(0, 0, 0, 0.55),
        0 0 10px rgba(255, 123, 227, 0.45) !important;
    }
    body.group-selected.group-video-selected #origin-overlay #rest-label,
    #origin-overlay #rest-label.is-video-group {
      background: #2a2a2a !important;
      border-color: #ff7be3 !important;
      color: rgb(255, 195, 240) !important;
    }
    body.group-selected.group-video-selected #origin-overlay #rest-label::before,
    #origin-overlay #rest-label.is-video-group::before {
      background: #ff7be3 !important;
    }
    /* Multi-select chrome — cyan palette for a wholesale-move-target
       union bbox. Distinct from yellow (single layer), violet (group),
       green (mask group), and pink (video group) so designers can read
       at a glance that an arbitrary multi-select is active. The .is-
       multi-select class lives on #origin-bounds + #rest-label; body.
       multi-selected gates the bbox-level overrides so non-multi-select
       passes don't get pulled in by class fallthrough.
       Also hides per-layer chrome (xform / rotate handles, origin dot)
       since the union bbox doesn't expose per-layer gesture surfaces.  */
    body.multi-selected #origin-overlay #origin-bounds,
    #origin-overlay #origin-bounds.is-multi-select {
      border-color: rgba(80, 200, 220, 0.78) !important;
      background: rgba(80, 200, 220, 0.04) !important;
      box-shadow:
        0 0 0 1px rgba(0, 0, 0, 0.55) inset,
        0 0 0 1px rgba(0, 0, 0, 0.55),
        0 0 10px rgba(80, 200, 220, 0.45) !important;
    }
    body.multi-selected #origin-overlay #rest-label,
    #origin-overlay #rest-label.is-multi-select {
      background: #2a2a2a !important;
      border-color: rgb(80, 200, 220) !important;
      color: rgb(170, 235, 245) !important;
    }
    body.multi-selected #origin-overlay #rest-label::before,
    #origin-overlay #rest-label.is-multi-select::before {
      background: rgb(80, 200, 220) !important;
    }
    /* Multi-select chrome — keep the xform + rotate handles visible
       (they drive the new union-pivot scale / rotate gestures) but hide
       the per-layer origin dot, which would point at the primary's
       anchor and read as misleading next to a multi-select pivot. The
       handles get the cyan palette to match the bbox + pill. */
    body.multi-selected #origin-overlay #origin-dot {
      display: none !important;
    }
    body.multi-selected #origin-overlay #origin-bounds .xform-handle {
      background: rgb(80, 200, 220) !important;
    }
    body.multi-selected #origin-overlay #origin-bounds .rotate-handle {
      border-color: rgb(80, 200, 220) !important;
      color: rgb(80, 200, 220) !important;
      box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6),
                  0 0 8px rgba(80, 200, 220, 0.3) !important;
    }
    body.multi-selected #origin-overlay #origin-bounds .rotate-handle:hover {
      background: rgba(80, 200, 220, 0.14) !important;
    }
    body.multi-selected #origin-overlay #origin-bounds .rotate-handle.dragging {
      background: rgba(80, 200, 220, 0.22) !important;
    }
    /* Hide the FROM / EXIT xform-handle sets in multi-select — those
       belong to the primary layer's animation poses and are surfaced
       only when the user is on the Animation tab with a single layer.
       Without this rule both the REST set and the FROM/EXIT sets would
       paint on top of each other inside the cyan bbox. */
    body.multi-selected #origin-overlay #from-bounds .xform-handle,
    body.multi-selected #origin-overlay #exit-bounds .xform-handle,
    body.multi-selected #origin-overlay #from-bounds .rotate-handle,
    body.multi-selected #origin-overlay #exit-bounds .rotate-handle {
      display: none !important;
    }
    /* Static panel during multi-select: grey out + disable every section
       EXCEPT the anchor picker. Anchor is the one Static control that
       has well-defined wholesale semantics ("left-anchor all four
       selected layers" reads cleanly), so the click handler applies it
       to every selected layer. Everything else (X / Y / W / H, Opacity,
       Rotation, Scale X / Y, Skew, Blur, Brightness, Active toggle,
       Blend, Visibility window) is per-layer — typing a value into
       any of those during multi-select would silently mutate only the
       primary, which the user reasonably wouldn't expect.

       Animation + Rollover panels stay live — designers may want to
       author tween / hover state for the primary while everything else
       comes along via the timeline track row selection. If that turns
       out to be confusing too, easy to extend the rule below.    */
    body.multi-selected #vars-static > *:not(.anchor-section) {
      opacity: 0.35;
      pointer-events: none;
      filter: grayscale(0.4);
    }
    /* Group + mask transform handles — match the bbox palette so the
       scale + rotate affordances read as the same gesture surface as
       the dashed bbox + chip. Yellow xform handles on a violet bbox
       broke the read of "this is the group's gesture target." */
    body.group-selected #origin-overlay #origin-bounds .xform-handle,
    #origin-overlay #origin-bounds.is-group .xform-handle {
      background: var(--color-group) !important;
    }
    body.group-selected #origin-overlay #origin-bounds .rotate-handle,
    #origin-overlay #origin-bounds.is-group .rotate-handle {
      border-color: var(--color-group) !important;
      color: var(--color-group) !important;
      box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6),
                  0 0 8px rgba(var(--color-group-rgb), 0.25) !important;
    }
    body.group-selected #origin-overlay #origin-bounds .rotate-handle:hover,
    #origin-overlay #origin-bounds.is-group .rotate-handle:hover {
      background: rgba(var(--color-group-rgb), 0.12) !important;
    }
    body.group-selected #origin-overlay #origin-bounds .rotate-handle.dragging,
    #origin-overlay #origin-bounds.is-group .rotate-handle.dragging {
      background: rgba(var(--color-group-rgb), 0.2) !important;
    }
    body.group-selected #origin-overlay #origin-bounds .rotate-handle::before,
    #origin-overlay #origin-bounds.is-group .rotate-handle::before {
      background: rgba(var(--color-group-rgb), 0.5) !important;
    }
    /* Mask groups override the group palette to green. */
    body.group-selected.group-mask-selected #origin-overlay #origin-bounds .xform-handle,
    #origin-overlay #origin-bounds.is-mask-group .xform-handle {
      background: var(--color-mask) !important;
    }
    body.group-selected.group-mask-selected #origin-overlay #origin-bounds .rotate-handle,
    #origin-overlay #origin-bounds.is-mask-group .rotate-handle {
      border-color: var(--color-mask) !important;
      color: var(--color-mask) !important;
      box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6),
                  0 0 8px rgba(94, 198, 132, 0.25) !important;
    }
    body.group-selected.group-mask-selected #origin-overlay #origin-bounds .rotate-handle:hover,
    #origin-overlay #origin-bounds.is-mask-group .rotate-handle:hover {
      background: rgba(94, 198, 132, 0.12) !important;
    }
    body.group-selected.group-mask-selected #origin-overlay #origin-bounds .rotate-handle.dragging,
    #origin-overlay #origin-bounds.is-mask-group .rotate-handle.dragging {
      background: rgba(94, 198, 132, 0.2) !important;
    }
    body.group-selected.group-mask-selected #origin-overlay #origin-bounds .rotate-handle::before,
    #origin-overlay #origin-bounds.is-mask-group .rotate-handle::before {
      background: rgba(94, 198, 132, 0.5) !important;
    }
    /* Video group transform handles + rotate handle — pink. Placed
       AFTER the group + mask rule blocks so source order wins the
       cascade against the equally-specific .is-group selector. A
       video group carries is-group + is-video-group on #origin-bounds
       (the editor toggles is-mask-group OFF when isVideo is true at
       index.html:~4102, so mask + video don't combine on the same
       element); without this ordering the .is-group rule below
       inherited the violet palette instead of pink. */
    body.group-selected.group-video-selected #origin-overlay #origin-bounds .xform-handle,
    #origin-overlay #origin-bounds.is-video-group .xform-handle {
      background: #ff7be3 !important;
    }
    body.group-selected.group-video-selected #origin-overlay #origin-bounds .rotate-handle,
    #origin-overlay #origin-bounds.is-video-group .rotate-handle {
      border-color: #ff7be3 !important;
      color: #ff7be3 !important;
      box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6),
                  0 0 8px rgba(255, 123, 227, 0.25) !important;
    }
    body.group-selected.group-video-selected #origin-overlay #origin-bounds .rotate-handle:hover,
    #origin-overlay #origin-bounds.is-video-group .rotate-handle:hover {
      background: rgba(255, 123, 227, 0.12) !important;
    }
    body.group-selected.group-video-selected #origin-overlay #origin-bounds .rotate-handle.dragging,
    #origin-overlay #origin-bounds.is-video-group .rotate-handle.dragging {
      background: rgba(255, 123, 227, 0.2) !important;
    }
    body.group-selected.group-video-selected #origin-overlay #origin-bounds .rotate-handle::before,
    #origin-overlay #origin-bounds.is-video-group .rotate-handle::before {
      background: rgba(255, 123, 227, 0.5) !important;
    }
    /* Anchor point — solid square (corner) or circle (smooth). Sits
       centered on its position via translate(-50%, -50%). Green theme
       matches the MASK badge so the read is "this is a mask path". */
    .vec-anchor {
      position: absolute;
      width: 9px;
      height: 9px;
      margin-left: -4.5px;
      margin-top: -4.5px;
      background: rgba(94, 198, 132, 0.95);
      border: 1.5px solid rgba(20, 80, 50, 0.85);
      box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.85), 0 1px 3px rgba(0, 0, 0, 0.5);
      pointer-events: auto;
      cursor: pointer;
      z-index: 2;
    }
    .vec-anchor.smooth { border-radius: 50%; }
    .vec-anchor.selected {
      background: #fff;
      border-color: rgba(94, 198, 132, 1);
      box-shadow: 0 0 0 2px rgba(94, 198, 132, 0.9), 0 0 8px rgba(94, 198, 132, 0.6);
    }
    /* Handle dot — smaller diamond, only shown for the selected anchor's
       in/out handles so the overlay doesn't clutter every anchor with
       four extra dots. */
    .vec-handle {
      position: absolute;
      width: 7px;
      height: 7px;
      margin-left: -3.5px;
      margin-top: -3.5px;
      background: rgba(94, 198, 132, 0.7);
      border: 1px solid rgba(20, 80, 50, 0.7);
      transform: rotate(45deg);
      box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.7);
      pointer-events: auto;
      cursor: pointer;
      z-index: 2;
    }
    /* Thin line connecting an anchor to its handle. Drawn as an
       absolutely-positioned 1px div rotated/scaled to span the
       (anchor → handle) segment. Cheaper than SVG for v1. */
    .vec-handle-arm {
      position: absolute;
      height: 1px;
      background: rgba(94, 198, 132, 0.45);
      transform-origin: 0 50%;
      pointer-events: none;
      z-index: 1;
    }


    /* While the master timeline is actively playing, hide ALL bounding-box
       overlays, FROM markers, transform handles, snap guides, etc. so the
       canvas reads as the final viewer experience. They reappear the moment
       the timeline pauses (manually, scrub, or settles at the end).
       Exception: when a transform-handle drag is in flight
       (.xform-dragging) the bbox + chips MUST stay visible regardless of
       any stale .is-playing class — otherwise the user's REST bbox
       vanishes the moment they grab a scale or rotate handle and only
       reappears on mouseup. */
    #origin-overlay.is-playing:not(.xform-dragging) #origin-bounds,
    #origin-overlay.is-playing:not(.xform-dragging) #from-bounds,
    #origin-overlay.is-playing:not(.xform-dragging) #exit-bounds,
    #origin-overlay.is-playing:not(.xform-dragging) #hover-bounds,
    #origin-overlay.is-playing:not(.xform-dragging) #origin-dot,
    #origin-overlay.is-playing:not(.xform-dragging) #rest-label,
    #origin-overlay.is-playing:not(.xform-dragging) #from-label,
    #origin-overlay.is-playing:not(.xform-dragging) #exit-label,
    #origin-overlay.is-playing:not(.xform-dragging) #hover-label,
    #origin-overlay.is-playing:not(.xform-dragging) #from-connector,
    #origin-overlay.is-playing:not(.xform-dragging) #from-connector-arrow,
    #origin-overlay.is-playing:not(.xform-dragging) #exit-connector,
    #origin-overlay.is-playing:not(.xform-dragging) #exit-connector-arrow,
    #origin-overlay.is-playing:not(.xform-dragging) .xform-handle,
    #origin-overlay.is-playing:not(.xform-dragging) .rotate-handle,
    #origin-overlay.is-playing:not(.xform-dragging) .snap-guide {
      display: none !important;
    }

    /* Click-to-activate dimming. Default selects REST (.kb-rest); clicking
       the FROM box switches to .kb-from. Whichever is *not* active fades
       enough to signal which marker the arrow keys will nudge, but stays
       readable so the FROM outline doesn't disappear against busy layers. */
    /* Dim the inactive bounds + connector — but NOT the label chips.
       The chips need full opacity so they stay legible when one chip
       lands on top of another (the START chip stacking over the END
       chip during alt-preview was the original break). Keeping the
       bounds dimmed still communicates "this is the inactive pose"
       without making the text show-through.

       Previously this used `opacity: 0.75` on #from-bounds, but CSS
       opacity inherits compositionally to descendants — which made
       the #from-label chip read as semi-transparent until you alt-
       clicked into kb-from (where this rule stops applying). The
       fix: dim only the bbox's own visual chrome (border + outer
       glow + inset rings) by reducing each rgba alpha to 75% of the
       base, so the chip child is left at full opacity. */
    #origin-overlay.kb-rest #from-bounds {
      border-color: rgba(245, 200, 66, 0.71);
      box-shadow:
        0 0 0 1px rgba(0, 0, 0, 0.41) inset,
        0 0 0 1px rgba(0, 0, 0, 0.41),
        0 0 10px rgba(245, 200, 66, 0.24);
    }
    #origin-overlay.kb-rest #from-connector,
    #origin-overlay.kb-rest #from-connector-arrow {
      opacity: 0.75;
    }
    #origin-overlay.kb-from #origin-bounds,
    #origin-overlay.kb-from #origin-dot,
    #origin-overlay.kb-from .xform-handle:not(.xform-from),
    #origin-overlay.kb-from .rotate-handle:not(.rotate-handle-from) {
      opacity: 0.75;
    }
    /* Active-label accent removed: the yellow outer ring was leaking
       through on group / mask selections (the chip switches to violet
       or green, but the kb-rest accent ring kept the yellow halo). The
       chip's own border-color already signals which pose is active —
       this extra ring was redundant once the per-phase chip palette
       landed. */

    /* ── Rollover edit mode (.kb-rollover) ─────────────────────────────────
       Active when the user is on the Rollover tab. The entire entry
       FROM/REST affordance set is HIDDEN — the user can only edit the
       rollover poses from this tab. No accidental rest drags, no FROM
       marker, no kb arrow nudges aimed at the wrong target. The cyan
       HOVER START + HOVER END bboxes are the only visible pair (in
       Simple mode), or just the HOVER END live bbox (in kf mode — same
       single-bbox affordance entry kf mode uses for REST). */
    #origin-overlay.kb-rollover #from-bounds,
    #origin-overlay.kb-rollover #from-label,
    #origin-overlay.kb-rollover #from-connector,
    #origin-overlay.kb-rollover #from-connector-arrow,
    #origin-overlay.kb-rollover #origin-bounds,
    #origin-overlay.kb-rollover #origin-dot,
    #origin-overlay.kb-rollover #rest-label,
    #origin-overlay.kb-rollover #exit-bounds,
    #origin-overlay.kb-rollover #exit-label,
    #origin-overlay.kb-rollover #exit-connector,
    #origin-overlay.kb-rollover #exit-connector-arrow,
    #origin-overlay.kb-rollover .xform-handle:not(.xform-hover):not(.xform-hover-start),
    #origin-overlay.kb-rollover .rotate-handle:not(.rotate-handle-hover):not(.rotate-handle-hover-start) {
      display: none !important;
    }

    /* ── Animation > Exit pill, simple mode (.kb-exit-simple) ──────────────
       Mirror of .kb-rollover for the exit phase: lock every green REST-side
       affordance so the user can ONLY interact with the orange EXIT bbox +
       chip. The green dashed bbox + REST chip remain visible (dimmed) as
       a positional reference — the user needs to see where the exit
       starts FROM. The interactive children (scale handles, rotate
       handle, origin dot) are hidden outright since there's no useful
       reason to show locked UI. The FROM bbox is also irrelevant on the
       Exit pill, so hide it too. */
    #origin-overlay.kb-exit-simple #from-bounds,
    #origin-overlay.kb-exit-simple #from-label,
    #origin-overlay.kb-exit-simple #from-connector,
    #origin-overlay.kb-exit-simple #from-connector-arrow,
    #origin-overlay.kb-exit-simple #origin-dot,
    #origin-overlay.kb-exit-simple .xform-handle[data-target="rest"],
    #origin-overlay.kb-exit-simple .rotate-handle[data-target="rest"] {
      display: none !important;
    }
    #origin-overlay.kb-exit-simple #origin-bounds,
    #origin-overlay.kb-exit-simple #rest-label {
      pointer-events: none;
      opacity: 0.4;
    }
    /* HOVER END + HOVER START bboxes are hidden by default; only kb-rollover
       shows them. HOVER START gets a stripe pattern so it visually mirrors
       the entry FROM bbox (yellow stripe → cyan stripe), making the START vs
       END pair read at a glance. */
    #hover-bounds, #hover-start-bounds { display: none; }
    #origin-overlay.kb-rollover #hover-bounds {
      display: block;
    }
    #origin-overlay.kb-rollover #hover-start-bounds {
      display: block;
    }
    #origin-overlay.kb-rollover #hover-label {
      box-shadow: 0 0 0 1px #5fb8c8, 0 0 0 3px rgba(95, 184, 200, 0.18);
    }
    #origin-overlay.kb-rollover #hover-start-label {
      box-shadow: 0 0 0 1px #5fb8c8, 0 0 0 3px rgba(95, 184, 200, 0.18);
    }
    /* Rollover-kf mode hides the HOVER START bbox/handles entirely — the
       canvas shows the live interpolated state through the kf chain (via
       _liveHover applied to the renderer), and the HOVER END bbox tracks
       that pose so the user sees ONE bbox at the active kf, mirroring how
       entry kf mode shows just the REST bbox at live state. */
    #origin-overlay.kb-rollover-kf #hover-start-bounds,
    #origin-overlay.kb-rollover-kf #hover-start-label,
    #origin-overlay.kb-rollover-kf .xform-handle.xform-hover-start,
    #origin-overlay.kb-rollover-kf .rotate-handle-hover-start {
      display: none !important;
    }
    /* Hide hover-bounds + label during playback (consistent with from/rest). */
    #origin-overlay.is-playing #hover-bounds,
    #origin-overlay.is-playing #hover-label,
    #origin-overlay.is-playing #hover-start-bounds,
    #origin-overlay.is-playing #hover-start-label {
      display: none !important;
    }

    #origin-bounds {
      position: absolute;
      /* Border + glow matched to #from-bounds / #exit-bounds (1.5px
         dashed, 0.95-alpha, outer 10px glow) so END reads as visually
         dominant when all three bboxes overlap on Static tab. The
         previous styling (1px dashed at 0.55 alpha, no outer glow)
         was visually MUCH weaker than FROM (yellow glow) and EXIT
         (orange glow) — z-index 10 made END technically on top, but
         the orange glow bleeding past the dashes still read as "EXIT
         is on top." Matching the visual weight resolves the
         perception gap. */
      border: 1.5px dashed rgba(245, 200, 66, 0.95);
      box-shadow:
        0 0 0 1px rgba(0,0,0,0.55) inset,
        0 0 0 1px rgba(0,0,0,0.55),
        0 0 10px rgba(245, 200, 66, 0.45);
      transition: border-color 0.12s, box-shadow 0.12s;
      pointer-events: none;
      /* Sits clearly above #from-bounds (z-index 2) and #exit-bounds
         (z-index 2) so the green END bbox + chip + handles are on top
         in the default layered view (Static tab + Animation tab when
         the user isn't actively previewing FROM/EXIT). The
         .mod-elevated class on a sibling bumps its z to 99 to break
         past this when alt/cmd is held. */
      z-index: 10;
    }

    /* Static-tab kf-edit cue. When the user is on Static and the
       playhead falls inside the EXIT range of a layer that has
       useExitKeyframes on, dragging the layer body will write into
       the exit kf chain (the surprising case the new routing fixes).
       Tint the REST bbox border + glow orange so the user can SEE
       which phase the next edit affects before committing.
       Entry-kf range keeps the default yellow (matches existing REST
       color — already correct). REST gap is unmarked. */
    #origin-overlay.static-kf-target-exit #origin-bounds {
      border-color: rgba(255, 170, 60, 0.95);
      box-shadow:
        0 0 0 1px rgba(0,0,0,0.55) inset,
        0 0 0 1px rgba(0,0,0,0.55),
        0 0 10px rgba(255, 170, 60, 0.55);
    }

    /* Transform handles — 4 corners + 4 edge midpoints. Drag to scale. */
    .xform-handle {
      position: absolute;
      width: 10px;
      height: 10px;
      margin-left: -5px;
      margin-top:  -5px;
      background: var(--accent);
      border: 1px solid rgba(0, 0, 0, 0.6);
      box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.15);
      pointer-events: auto;
      z-index: 5;
      transition: transform 0.08s;
    }
    .xform-handle:hover    { transform: scale(1.25); }
    .xform-handle.dragging { transform: scale(1.4); }
    /* FROM-marker handles — slightly smaller hollow squares to read as
       "secondary" affordance vs. the resting-bounds handles. */
    .xform-handle.xform-from {
      width: 8px;
      height: 8px;
      margin-left: -4px;
      margin-top:  -4px;
      background: #0a0a0a;
      border: 1px solid var(--accent);
      box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6);
    }
    .xform-handle.h-nw { left:   0%; top:   0%; cursor: nwse-resize; }
    .xform-handle.h-n  { left:  50%; top:   0%; cursor: ns-resize; }
    .xform-handle.h-ne { left: 100%; top:   0%; cursor: nesw-resize; }
    .xform-handle.h-e  { left: 100%; top:  50%; cursor: ew-resize; }
    .xform-handle.h-se { left: 100%; top: 100%; cursor: nwse-resize; }
    .xform-handle.h-s  { left:  50%; top: 100%; cursor: ns-resize; }
    .xform-handle.h-sw { left:   0%; top: 100%; cursor: nesw-resize; }
    .xform-handle.h-w  { left:   0%; top:  50%; cursor: ew-resize; }

    /* Rotation handle — sits above the top-center of the bounding box. A
       circular pill with ↻ icon. Drag it in a circle to rotate the layer. */
    .rotate-handle {
      position: absolute;
      /* Top-right corner, outside the bounding box. Keeps it clear of the
         REST/KF label (which is anchored top-left) regardless of layer width. */
      top: -22px;
      right: -22px;
      width: 20px;
      height: 20px;
      border-radius: 50%;
      background: #0a0a0a;
      border: 1px solid var(--accent);
      box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6),
                  0 0 8px rgba(245, 200, 66, 0.25);
      color: var(--accent);
      font-size: 11px;
      line-height: 18px;
      text-align: center;
      cursor: grab;
      pointer-events: auto;
      z-index: 6;
      user-select: none;
      transition: transform 0.08s, background 0.1s;
    }
    .rotate-handle:hover    { background: rgba(245, 200, 66, 0.12); transform: scale(1.12); }
    .rotate-handle.dragging { cursor: grabbing; transform: scale(1.18); background: rgba(245, 200, 66, 0.2); }
    /* Diagonal "stem" from the handle pointing toward the box's top-right
       corner so the handle reads as attached, not floating. */
    .rotate-handle::before {
      content: '';
      position: absolute;
      top: 14px;
      left: 14px;
      width: 8px;
      height: 1px;
      background: rgba(245, 200, 66, 0.5);
      transform: rotate(45deg);
      transform-origin: 0 0;
    }
    /* FROM-marker variant — same size as the REST rotate handle so
       FROM / REST / EXIT all read as equally-grabbable affordances.
       The color (currentColor → accent yellow) keeps the phase
       identity without shrinking the click target. */
    .rotate-handle-from {
      /* width/height/top/right/font-size inherited from .rotate-handle
         base — only stem-position carve-out below in case anything
         downstream depends on it. */
    }

    /* EXIT scale handles — orange variant matching the EXIT bbox color. */
    .xform-handle.xform-exit {
      width: 8px;
      height: 8px;
      margin-left: -4px;
      margin-top:  -4px;
      background: #0a0a0a;
      border: 1px solid #ffaa3c;
      box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6);
    }
    /* EXIT rotate handle — same SIZE as REST rotate, only the color
       palette swaps to orange to keep the phase identity. */
    .rotate-handle-exit {
      border-color: #ffaa3c;
      color: #ffaa3c;
      box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6),
                  0 0 8px rgba(255, 170, 60, 0.25);
    }
    .rotate-handle-exit:hover    { background: rgba(255, 170, 60, 0.12); }
    .rotate-handle-exit.dragging { background: rgba(255, 170, 60, 0.2); }
    .rotate-handle-exit::before { background: rgba(255, 170, 60, 0.5); }

    /* All four canvas pose labels (REST / FROM / EXIT / HOVER / HOVER-START)
       share the same shape, position, padding, font, border-radius, dot
       size, and shadow. Each variant overrides only its colors. This
       block defines the shared baseline; per-label rules below set the
       background/text/dot tint. Tag text stays fully opaque (was 0.9
       which read as transparent against the solid chip bg). No border
       and no inset dot-shadow so the chip reads flat — the previous
       yellow border on #rest-label created the "beveled" look the user
       flagged. */
    #rest-label,
    #from-label,
    #exit-label,
    #hover-label,
    #hover-start-label {
      position: absolute;
      top: -24px;
      left: -1px;
      align-items: center;
      gap: 5px;
      padding: 3px 9px 3px 7px;
      font-size: 10px;
      font-weight: 600;
      line-height: 1;
      border-radius: 8px;
      /* Per-label rules set border-color; default is dark for the
         color-on-color phase chips (FROM yellow, EXIT orange, HOVER
         cyan, HOVER-START light cyan). REST overrides to yellow. */
      border: 1px solid #0a0a0a;
      white-space: nowrap;
      box-shadow: 0 1px 2px rgba(0,0,0,0.5);
      letter-spacing: 0.1em;
      transition: filter 0.1s;
    }
    #rest-label::before,
    #from-label::before,
    #exit-label::before,
    #hover-label::before,
    #hover-start-label::before {
      content: '';
      width: 7px;
      height: 7px;
      border-radius: 50%;
    }
    #rest-label .rest-label-text,
    #from-label .from-label-text,
    #exit-label .exit-label-text,
    #hover-label .hover-label-text,
    #hover-start-label .hover-start-label-text {
      font-size: 10px;
    }
    #rest-label .rest-label-tag,
    #from-label .from-label-tag,
    #exit-label .exit-label-tag,
    #hover-label .hover-label-tag,
    #hover-start-label .hover-start-label-tag {
      font-size: 9px;
      font-weight: 500;
      margin-left: 2px;
    }

    /* REST (end) — dark grey bg, white text, yellow stroke, green dot. */
    #rest-label {
      display: none;
      color: #ffffff;
      background: #2a2a2a;
      border-color: var(--accent);
      pointer-events: auto;
      cursor: grab;
    }
    #rest-label.show     { display: flex; }
    #rest-label:hover    { filter: brightness(1.15); }
    #rest-label.dragging { cursor: grabbing; filter: brightness(1.25); }
    #rest-label::before  { background: #5ec684; }


    /* FROM-position marker — second dashed box at the layer's animation start
       position, with a label and a connector line back to its resting box.
       The box itself is pointer-transparent so clicks pass through to layers
       underneath. The label chip is the drag handle.

       The dashed stroke + a 1px solid black inset + a stronger outer glow
       make the FROM box readable against busy backgrounds even when it's
       muted by the active-marker dimming below. */
    #from-bounds {
      position: absolute;
      display: none;
      border: 1.5px dashed rgba(245, 200, 66, 0.95);
      background: rgba(245, 200, 66, 0.05);
      box-shadow:
        0 0 0 1px rgba(0,0,0,0.55) inset,
        0 0 0 1px rgba(0,0,0,0.55),
        0 0 10px rgba(245, 200, 66,0.32);
      /* Visual-only by default — Static shows FROM as a reference for
         all three poses, so clicks should fall through to the layer
         body's REST drag. Restored to a draggable handle on the
         Animation tab where FROM is the primary START affordance.
         Mirrors the #exit-bounds gate. */
      pointer-events: none;
      cursor: default;
      transition: background 0.1s;
      z-index: 2;
    }
    #origin-overlay.tab-anim #from-bounds {
      pointer-events: auto;
      cursor: grab;
    }
    #from-bounds:hover    { background: rgba(245, 200, 66, 0.10); }
    #from-bounds:active   { cursor: grabbing; }
    #from-bounds.dragging { background: rgba(245, 200, 66, 0.14); cursor: grabbing; }

    /* FROM (start) — yellow bg, black text, black stroke, white dot.
       background is locked to fully opaque var(--accent) with !important
       so no downstream state (hover, dragging, kb-*, group-selected
       chrome, etc.) can dim or knock out the chip's fill. */
    #from-label {
      display: flex;
      color: #0a0a0a;
      background: var(--accent) !important;
      border-color: #0a0a0a;
      /* Visual-only by default. Clickable drag handle on Animation +
         Static tabs (so the user can recover an off-canvas FROM pose).
         Rollover keeps it visual-only. */
      pointer-events: none;
      cursor: default;
    }
    #origin-overlay.tab-anim #from-label,
    #origin-overlay.tab-static #from-label {
      pointer-events: auto;
      cursor: grab;
    }
    #from-label:hover    { filter: brightness(1.15); }
    #from-label.dragging { cursor: grabbing; filter: brightness(1.25); }
    #from-label::before  { background: #ffffff; }

    /* EXIT bbox + label — orange, visually distinct from FROM's yellow and
       HOVER's cyan. Same drag affordance as the FROM box but writes to
       obj.exitAnimation.to instead of obj.animation.from. Visible only on
       Animation tab + Exit pill in simple mode (kf mode shows kf bboxes
       through a different path). */
    #exit-bounds {
      position: absolute;
      display: none;
      border: 1.5px dashed rgba(255, 170, 60, 0.95);
      background: rgba(255, 170, 60, 0.05);
      box-shadow:
        0 0 0 1px rgba(0,0,0,0.55) inset,
        0 0 0 1px rgba(0,0,0,0.55),
        0 0 10px rgba(255, 170, 60, 0.32);
      /* Visual-only by default. The orange bbox is shown on Static
         (as a triple-bbox reference for all three poses) and on
         Animation > Entry pill while Cmd is held (preview); in those
         contexts a click here would hijack into an exit-edit and
         block the user's intent of "drag the layer body to move
         REST." Pointer-events restored to auto only on the Animation
         tab via .tab-anim — that's where this bbox IS the primary
         drag handle (Animation > Exit pill). */
      pointer-events: none;
      cursor: default;
      transition: background 0.1s;
      z-index: 2;
    }
    #origin-overlay.tab-anim #exit-bounds {
      pointer-events: auto;
      cursor: grab;
    }
    #exit-bounds:hover    { background: rgba(255, 170, 60, 0.10); }
    #exit-bounds:active   { cursor: grabbing; }
    #exit-bounds.dragging { background: rgba(255, 170, 60, 0.14); cursor: grabbing; }
    /* EXIT — orange bg, black text, black stroke, red dot. */
    #exit-label {
      display: flex;
      color: #0a0a0a;
      background: #ffaa3c;
      border-color: #0a0a0a;
      pointer-events: none;
      cursor: default;
    }
    #origin-overlay.tab-anim #exit-label,
    #origin-overlay.tab-static #exit-label {
      pointer-events: auto;
      cursor: grab;
    }
    #exit-label:hover    { filter: brightness(1.15); }
    #exit-label.dragging { cursor: grabbing; filter: brightness(1.25); }
    #exit-label::before  { background: #ef4444; }

    /* HOVER END bbox + label — cyan, visually distinct from entry's yellow.
       Same drag affordance as the FROM/REST boxes but writes to
       obj.rollover.to instead of obj.variables / obj.animation.from. */
    #hover-bounds {
      position: absolute;
      border: 1.5px dashed rgba(95, 184, 200, 0.95);
      background: rgba(95, 184, 200, 0.05);
      box-shadow:
        0 0 0 1px rgba(0,0,0,0.55) inset,
        0 0 0 1px rgba(0,0,0,0.55),
        0 0 10px rgba(95, 184, 200, 0.32);
      pointer-events: none;
      transition: background 0.1s;
      z-index: 3;
    }
    #hover-bounds.dragging { background: rgba(95, 184, 200, 0.14); }
    /* HOVER END — cyan bg, black text, black stroke, green dot. */
    #hover-label {
      display: flex;
      color: #0a0a0a;
      background: #5fb8c8;
      border-color: #0a0a0a;
      pointer-events: auto;
      cursor: grab;
    }
    #hover-label:hover    { filter: brightness(1.15); }
    #hover-label.dragging { cursor: grabbing; filter: brightness(1.25); }
    #hover-label::before  { background: #5ec684; }
    /* HOVER scale handles — cyan accent, hollow squares like .xform-from */
    .xform-handle.xform-hover {
      width: 8px;
      height: 8px;
      margin-left: -4px;
      margin-top:  -4px;
      background: #0a0a0a;
      border: 1px solid #5fb8c8;
      box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6);
    }
    .rotate-handle.rotate-handle-hover {
      border-color: #5fb8c8;
      color: #5fb8c8;
    }

    /* HOVER START bbox — mirrors HOVER END's cyan styling but with a
       diagonal stripe pattern in the background, the same way the entry
       FROM bbox is striped relative to the REST bbox. Lets users tell
       START vs END at a glance even when both poses overlap on the canvas. */
    #hover-start-bounds {
      position: absolute;
      border: 1.5px dashed rgba(95, 184, 200, 0.85);
      background-image: repeating-linear-gradient(
        135deg,
        transparent 0,
        transparent 5px,
        rgba(95, 184, 200, 0.18) 5px,
        rgba(95, 184, 200, 0.18) 9px
      );
      box-shadow:
        0 0 0 1px rgba(0,0,0,0.55) inset,
        0 0 0 1px rgba(0,0,0,0.55),
        0 0 10px rgba(95, 184, 200, 0.32);
      pointer-events: none;
      transition: filter 0.1s;
      z-index: 3;
    }
    #hover-start-bounds.dragging { filter: brightness(1.2); }
    /* HOVER START — light cyan bg, black text, black stroke, white dot. */
    #hover-start-label {
      display: flex;
      color: #0a0a0a;
      background: #a4d8e0;
      border-color: #0a0a0a;
      pointer-events: auto;
      cursor: grab;
    }
    #hover-start-label:hover    { filter: brightness(1.15); }
    #hover-start-label.dragging { cursor: grabbing; filter: brightness(1.25); }
    #hover-start-label::before  { background: #ffffff; }

    /* Click-toggle "selected" state — single click on a chip marks it
       selected; clicking elsewhere clears. The bump is stroke-only and
       per-chip so each color brightens against its own palette instead
       of a generic filter:brightness washing out the whole pill. */
    #rest-label.selected        { border-color: #ffd95c; }
    #from-label.selected,
    #exit-label.selected,
    #hover-label.selected,
    #hover-start-label.selected { border-color: #3a3a3a; }
    body.group-selected #origin-overlay #rest-label.selected,
    #origin-overlay #rest-label.is-group.selected {
      border-color: var(--color-group-bright) !important;
    }

    .xform-handle.xform-hover-start {
      width: 8px;
      height: 8px;
      margin-left: -4px;
      margin-top:  -4px;
      background: #0a0a0a;
      border: 1px solid #5fb8c8;
      box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6);
    }
    .rotate-handle.rotate-handle-hover-start {
      border-color: #5fb8c8;
      color: #5fb8c8;
    }

    /* SVG layer for the FROM→REST connector line */
    #origin-svg {
      position: absolute;
      inset: 0;
      width: 100%;
      height: 100%;
      pointer-events: none;
      overflow: visible;
    }
    #from-connector {
      stroke: rgba(245, 200, 66, 0.55);
      stroke-width: 1.25;
      stroke-dasharray: 3 3;
      fill: none;
      display: none;
    }
    #from-connector-arrow {
      fill: rgba(245, 200, 66, 0.7);
      display: none;
    }
    /* REST → EXIT connector. Orange-themed twin of #from-connector to
       match the #exit-bounds color cue. Drawn whenever the orange
       EXIT bbox is visible AND there's a meaningful gap between REST
       and EXIT. Cmd/Ctrl-drag previewing on Static / Animation > Entry
       reveals the bbox + connector together; releasing the modifier
       hides both. */
    #exit-connector {
      stroke: rgba(255, 170, 60, 0.6);
      stroke-width: 1.25;
      stroke-dasharray: 3 3;
      fill: none;
      display: none;
    }
    #exit-connector-arrow {
      fill: rgba(255, 170, 60, 0.75);
      display: none;
    }
    /* Snap-guide lines drawn while dragging a layer image. Magenta to read
       distinctly from the accent-yellow markers around them. */
    .snap-guide {
      stroke: #ff00aa;
      stroke-width: 1;
      stroke-dasharray: 4 3;
      fill: none;
      display: none;
      vector-effect: non-scaling-stroke;
    }

    #origin-dot {
      position: absolute;
      width: 14px;
      height: 14px;
      margin-left: -7px;
      margin-top:  -7px;
      border-radius: 50%;
      background: var(--accent);
      box-shadow: 0 0 0 2px rgba(0,0,0,0.55), 0 0 12px rgba(245, 200, 66,0.55);
      cursor: grab;
      pointer-events: auto;
      transition: transform 0.08s;
    }

    #origin-dot::before {
      content: '';
      position: absolute;
      inset: 4px;
      border-radius: 50%;
      background: #0a0a0a;
    }

    #origin-dot:hover { transform: scale(1.15); }
    #origin-dot.dragging { cursor: grabbing; transform: scale(1.2); }

    /* ── Animation preset section ── */
    .preset-section {
      padding-bottom: 12px;
      border-bottom: 1px solid var(--border);
      margin-bottom: 12px;
    }

    .preset-row {
      display: flex;
      gap: 8px;
      align-items: stretch;
    }

    .preset-select {
      flex: 1;
      min-width: 0;
      /* Caret + glass background inherited from `select.var-input`. */
    }
    /* Greyed when the selected preset is the (custom) sentinel — i.e. the
       layer was applied a real preset but its values have since drifted.
       Visual cue that the dropdown isn't reflecting a clean preset state. */
    .preset-select.is-custom {
      color: var(--muted);
      font-style: italic;
      opacity: 0.7;
    }

    /* Outlined style — matches the "⊙ Center on content" button so the
       header preset-Apply controls read as secondary actions instead of
       competing with the canvas/timeline as a primary CTA. Was a yellow
       gradient pill; the gradient still lives on the librarySaveBtn /
       retina-go variants which DO want primary-CTA emphasis. */
    .preset-apply-btn {
      background: var(--btn-bg);
      color: var(--accent);
      border: 1px solid rgba(245, 200, 66, 0.4);
      padding: 6px 16px;
      font-family: var(--mono);
      font-weight: 500;
      font-size: 11.5px;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      border-radius: 6px;
      cursor: pointer;
      transition: background 0.15s, border-color 0.15s;
      backdrop-filter: blur(12px);
      -webkit-backdrop-filter: blur(12px);
    }
    .preset-apply-btn:hover:not(:disabled) {
      background: rgba(245, 200, 66, 0.10);
      border-color: rgba(245, 200, 66, 0.6);
    }
    .preset-apply-btn:disabled { opacity: 0.35; cursor: not-allowed; }

    /* "Remove all animation" buttons — destructive action, neutral styling
       so they don't compete with primary preset Apply buttons but still read
       as a clear action. Per-layer button sits below the entry preset row. */
    /* Keyframe list rendered under the "Add keyframe at playhead" button */
    .keyframes-list {
      display: flex;
      flex-direction: column;
      gap: 4px;
      margin-top: 8px;
    }
    .keyframe-row {
      display: grid;
      grid-template-columns: 24px 1fr auto auto auto;
      gap: 8px;
      align-items: center;
      padding: 5px 8px;
      border: 1px solid var(--border);
      border-radius: 6px;
      font-family: var(--mono);
      font-size: 11.5px;
      background: rgba(0, 0, 0, 0.25);
      transition: border-color 0.1s;
    }
    /* Per-keyframe ease dropdown — stays slim so the row keeps the same
       horizontal rhythm as the time / GO / × cells. The first kf's ease
       is "decorative" since no segment ends at it; we soften it visually. */
    .keyframe-ease {
      background: var(--input-bg);
      border: 1px solid var(--border);
      border-radius: 6px;
      color: var(--text);
      font-family: var(--mono);
      font-size: 10.5px;
      letter-spacing: 0.1em;
      padding: 3px 5px;
      max-width: 110px;
    }
    .keyframe-row.is-first .keyframe-ease { opacity: 0.45; }
    .keyframe-row.is-first .keyframe-ease:hover,
    .keyframe-row.is-first .keyframe-ease:focus { opacity: 1; }
    .keyframe-row:hover { border-color: rgba(245, 200, 66, 0.35); }
    .keyframe-row.active { border-color: var(--accent); background: rgba(245, 200, 66, 0.10); }
    .keyframe-diamond-mini {
      width: 8px;
      height: 8px;
      transform: rotate(45deg);
      margin: 0 auto;
      background: var(--accent);
      border: 1px solid rgba(0, 0, 0, 0.6);
    }
    .keyframe-time { color: var(--text); font-family: var(--mono); }
    .keyframe-summary { color: var(--muted); font-size: 10.5px; letter-spacing: 0.1em; }
    .keyframe-jump-btn,
    .keyframe-del-btn {
      background: var(--btn-bg);
      border: 1px solid var(--border);
      border-radius: 5px;
      color: var(--muted);
      font-family: var(--mono);
      font-weight: 500;
      font-size: 10px;
      padding: 3px 7px;
      cursor: pointer;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      transition: all 0.1s;
    }
    .keyframe-jump-btn:hover { color: var(--accent); border-color: rgba(245, 200, 66, 0.4); }
    .keyframe-del-btn:hover  { color: var(--danger); border-color: rgba(255, 107, 107, 0.5); }

    /* Diamond markers in the track editor for keyframe-mode layers */
    .track-keyframe {
      position: absolute;
      top: 50%;
      width: 10px;
      height: 10px;
      margin-left: -5px;
      margin-top: -5px;
      transform: rotate(45deg);
      background: var(--accent);
      border: 1px solid #0a0a0a;
      pointer-events: auto;
      cursor: pointer;
      z-index: 3;
      transition: transform 0.08s;
    }
    .track-keyframe { cursor: grab; }
    .track-keyframe:active { cursor: grabbing; }
    .track-keyframe:hover { transform: rotate(45deg) scale(1.3); }
    /* Active + selected states read --kf-ring-bright, which is scoped
       by the per-type class above (track-keyframe-entry / -exit /
       -rollover / -mask). Each type's ring color flows through state
       changes automatically — no per-state-per-type combinatorial
       rules required. Falls back to the entry ring for any
       hypothetical bare .track-keyframe without a type class. */
    .track-keyframe.active { box-shadow: 0 0 0 2px var(--kf-ring-bright, var(--kf-entry-ring-bright)); }
    .track-keyframe.selected { box-shadow: 0 0 0 2px var(--kf-ring-bright, var(--kf-entry-ring-bright)); }
    /* Ghost diamonds — middle kfs surfaced in FROM/REST view as a hint that
       there's hidden state. Read-only (smaller, dimmed, no fill) so they're
       clearly distinct from real keyframes you can edit. Switch to keyframes
       mode to interact with them. */
    .track-keyframe.is-ghost {
      width: 7px; height: 7px;
      margin-left: -3.5px; margin-top: -3.5px;
      background: transparent;
      border: 1px dashed currentColor;
      color: var(--muted);
      opacity: 0.7;
      cursor: help;
      pointer-events: auto;
    }
    .track-keyframe.is-ghost:hover { opacity: 1; transform: rotate(45deg) scale(1.2); }
    /* Inline panel notice when middle kfs are hidden */
    .interim-kf-notice {
      margin-top: 6px;
      padding: 6px 8px 6px 28px;     /* extra left padding for the alert icon */
      position: relative;
      border: 1px dashed var(--accent);
      border-radius: 6px;
      background: rgba(245, 200, 66,0.04);
      color: var(--text);
      font-family: var(--mono);
      font-size: 0.55rem;
      letter-spacing: 0.1em;
      line-height: 1.5;
    }
    .interim-kf-notice .interim-kf-alert {
      position: absolute;
      left: 8px;
      top: 6px;
      color: var(--accent);
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 14px;
      height: 14px;
      flex-shrink: 0;
    }
    .interim-kf-notice .kf-link {
      color: var(--accent);
      text-decoration: underline;
      cursor: pointer;
    }
    /* Keyframe-mode bar spans first → last keyframe. The bar reads the layer
       color like a regular entry bar; we tone it down slightly so the diamonds
       layered on top stay legible. Drag to shift all keyframes together. */
    .track-bar.track-bar-keyframes {
      opacity: 0.55;
      cursor: grab;
    }
    .track-bar.track-bar-keyframes:hover    { opacity: 0.7; }
    .track-bar.track-bar-keyframes.dragging { cursor: grabbing; opacity: 0.85; }

    /* Repeat / yoyo extension bar — used in BOTH Advanced kf mode and
       Simple FROM/REST mode. Diagonal stripes + lower opacity so the
       region reads as "GSAP repeating, not editable here". The
       editable region (first cycle / FROM-REST bar) gets the full
       editing affordance; this extension is informational. */
    .track-bar.track-bar-extend {
      opacity: 0.32;
      box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
    }
    .track-bar.track-bar-extend::after {
      content: '';
      position: absolute;
      inset: 0;
      background-image: repeating-linear-gradient(
        135deg,
        transparent 0,
        transparent 5px,
        rgba(0, 0, 0, 0.30) 5px,
        rgba(0, 0, 0, 0.30) 10px
      );
      border-radius: inherit;
      pointer-events: none;
    }
    /* Yoyo overlays a counter-stripe to read as "back-and-forth". */
    .track-bar.track-bar-extend-yoyo::after {
      background-image:
        repeating-linear-gradient(135deg, transparent 0, transparent 5px, rgba(0,0,0,0.30) 5px, rgba(0,0,0,0.30) 10px),
        repeating-linear-gradient(45deg,  transparent 0, transparent 5px, rgba(0,0,0,0.18) 5px, rgba(0,0,0,0.18) 10px);
    }
    /* Infinite repeat — fade out at the right edge so the user reads
       "and so on" instead of "stops at this exact frame". */
    .track-bar.track-bar-extend-infinite {
      mask-image: linear-gradient(to right, #000 60%, rgba(0,0,0,0.3) 100%);
      -webkit-mask-image: linear-gradient(to right, #000 60%, rgba(0,0,0,0.3) 100%);
    }


    .clear-all-anim-btn {
      width: 100%;
      margin-top: 10px;
      padding: 7px 10px;
      background: transparent;
      color: var(--muted);
      border: 1px dashed var(--border);
      border-radius: 6px;
      font-family: var(--mono);
      font-size: 0.6rem;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      cursor: pointer;
      transition: all 0.12s;
    }
    .clear-all-anim-btn:hover {
      color: var(--danger);
      border-color: rgba(255, 68, 68, 0.5);
      background: rgba(255, 68, 68, 0.04);
    }
    .clear-all-anim-btn-global {
      margin-top: 0;        /* the row already provides spacing */
      white-space: nowrap;  /* keep "× Remove all animation" on one line */
    }
    /* Pause-hover label sits next to the global "× Remove all animation"
       button — keep "PAUSE HOVER" on a single line so the row reads
       cleanly instead of wrapping awkwardly. */
    .hover-preview-label { white-space: nowrap; }

    /* Compact KF toggle row: pill toggle on the left, "× Remove all" on the
       right. Sits at the top of the keyframes-section and replaces the old
       checkbox + bottom-of-panel destructive button. */
    .kf-toggle-row {
      display: flex;
      align-items: stretch;
      gap: 8px;
      margin-bottom: 6px;
    }
    .kf-toggle-pill {
      flex: 1;
      display: flex;
      align-items: center;
      gap: 8px;
      padding: 8px 12px;
      background: var(--btn-bg);
      border: 1px solid var(--border);
      border-radius: 6px;
      color: var(--muted);
      font-family: var(--mono);
      font-weight: 500;
      font-size: 11.5px;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      cursor: pointer;
      transition: all 0.12s;
    }
    .kf-toggle-pill:hover { color: var(--text); border-color: rgba(245, 200, 66, 0.4); }
    .kf-toggle-pill .kf-toggle-dot {
      width: 12px;
      height: 12px;
      border-radius: 50%;
      border: 1px solid var(--muted);
      background: transparent;
      flex-shrink: 0;
      transition: all 0.12s;
    }
    .kf-toggle-pill.on {
      color: #2a1d00;
      background: linear-gradient(180deg, #ffd87a, #f5c842);
      border-color: rgba(245, 200, 66, 0.6);
    }
    .kf-toggle-pill.on .kf-toggle-dot {
      background: #2a1d00;
      border-color: #2a1d00;
    }
    .clear-all-anim-btn-inline {
      flex-shrink: 0;
      padding: 8px 12px;
      background: rgba(255, 107, 107, 0.06);
      color: var(--danger);
      border: 1px dashed rgba(255, 107, 107, 0.45);
      border-radius: 6px;
      font-family: var(--mono);
      font-weight: 500;
      font-size: 11px;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      cursor: pointer;
      transition: all 0.12s;
    }
    .clear-all-anim-btn-inline:hover {
      background: rgba(255, 107, 107, 0.12);
      border-style: solid;
    }
    .track-global-preset-clear {
      display: flex;
      align-items: center;
      gap: 12px;
    }
    /* Trailing group on the timeline-tracks preset row — pulls the
       Keyframes + Pause-hover toggles to the far right and keeps them
       grouped so neither one floats alone on its line. */
    .track-global-trailing {
      display: flex;
      align-items: center;
      gap: 16px;
      margin-left: auto;
    }

    /* Label-on-left + pill-toggle-on-right grouping for the
       Pause-hover-preview switch in the tracks header. The actual
       pill styling lives in the shared toggle-checkbox block above
       (#pause-hover-preview joins #t-loop, #t-yoyo et al.). This
       wrapper is just spacing + the LOOP/REPEAT-style label. */
    .hover-preview-group {
      display: inline-flex;
      align-items: center;
      gap: 8px;
      user-select: none;
      cursor: pointer;
    }
    .hover-preview-label {
      font-family: var(--mono);
      font-size: 0.6rem;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      color: var(--muted);
    }
    .hover-preview-group:has(#pause-hover-preview:checked) .hover-preview-label {
      color: var(--accent);
    }
    /* Auto-paused state — when activeTab is Animation or Rollover the
       checkbox is force-checked + disabled and the toggle reads as
       "auto-suppressed by the current tab" rather than the user's
       explicit setting. The whole group dims and the cursor becomes
       not-allowed so it's clear the control is read-only here. */
    .hover-preview-group.auto-paused {
      cursor: not-allowed;
      opacity: 0.7;
    }
    .hover-preview-group.auto-paused .hover-preview-label {
      color: var(--accent);
    }
    .hover-preview-group.auto-paused #pause-hover-preview {
      cursor: not-allowed;
    }

    /* Entry & Exit preset descriptions are now surfaced exclusively as
       browser tooltips on each preset thumbnail (set via card.title) —
       the inline description block under the grid was visual noise once
       the tooltips were good enough. Rollover keeps its inline copy
       since hover tooltips are awkward on a single-thumbnail panel. */
    #preset-description,
    #exit-preset-description { display: none; }
    .preset-description {
      font-family: var(--mono);
      font-size: 11px;
      color: var(--muted);
      letter-spacing: 0.1em;
      line-height: 1.5;
      margin-top: 8px;
      min-height: 1em;
    }

    /* ── Anchor picker ── */
    .anchor-section { padding-bottom: 10px; border-bottom: 1px solid var(--border); margin-bottom: 10px; }

    .anchor-row {
      display: flex;
      align-items: stretch;
      gap: 16px;
    }

    .anchor-picker {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      grid-template-rows:    repeat(3, 1fr);
      width: 84px;
      height: 84px;
      border: 1px solid var(--border);
      border-radius: 8px;
      background: var(--input-bg);
      flex-shrink: 0;
      gap: 0;
    }

    .anchor-cell {
      background: transparent;
      border: none;
      cursor: pointer;
      position: relative;
      padding: 0;
      transition: background 0.12s;
    }

    .anchor-cell:hover { background: rgba(245, 200, 66,0.06); }

    .anchor-cell::after {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      width: 5px;
      height: 5px;
      border-radius: 50%;
      background: var(--muted);
      transition: all 0.12s;
    }

    .anchor-cell:hover::after { background: var(--accent); transform: translate(-50%, -50%) scale(1.2); }

    .anchor-cell.active::after {
      background: var(--accent);
      width: 8px;
      height: 8px;
      box-shadow: 0 0 0 3px rgba(245, 200, 66,0.18);
    }

    .anchor-info {
      flex: 1;
      display: flex;
      flex-direction: column;
      gap: 6px;
      min-width: 0;
    }

    .anchor-auto-btn {
      background: var(--btn-bg);
      color: var(--accent);
      border: 1px solid rgba(245, 200, 66, 0.4);
      padding: 6px 12px;
      font-family: var(--mono);
      font-weight: 500;
      font-size: 11.5px;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      border-radius: 6px;
      cursor: pointer;
      transition: background 0.15s;
      align-self: flex-start;
      backdrop-filter: blur(12px);
      -webkit-backdrop-filter: blur(12px);
    }

    .anchor-auto-btn:hover { background: rgba(245, 200, 66, 0.10); }

    .anchor-current {
      font-family: var(--mono);
      font-size: 10px;
      color: var(--muted);
      letter-spacing: 0.1em;
      line-height: 1.45;
    }

    .anchor-help {
      font-family: var(--mono);
      font-size: 10px;
      color: var(--muted);
      line-height: 1.45;
      letter-spacing: 0.1em;
    }
    /* Context-aware note shown only when the anchor picker is on a
       group or a grouped child. Documents the parent/child anchor
       math limitation (off-center anchors compound through the
       parent's rotation pivot in unexpected ways). */
    .anchor-group-hint {
      display: block;
      width: 100%;
      box-sizing: border-box;
      font-family: var(--mono);
      font-size: 10px;
      color: var(--muted);
      line-height: 1.45;
      letter-spacing: 0.04em;
      padding: 8px 10px;
      margin-top: 10px;
      border-radius: 6px;
      background: rgba(var(--color-group-rgb), 0.06);
      border: 1px solid rgba(var(--color-group-rgb), 0.18);
    }

    /* ── Rollover tab: group summary view ──
       Shown ONLY when the current selection is a group. Lists each
       eligible child with its applied rollover preset (or "none" /
       "custom") + Apply-to-all / Clear-all controls. The standard
       per-layer rollover form below is hidden in this mode. */
    .rollover-group-summary {
      padding: 14px 16px;
      background: rgba(var(--color-group-rgb), 0.06);
      border: 1px solid rgba(var(--color-group-rgb), 0.25);
      border-radius: 8px;
      margin-bottom: 12px;
    }
    .rollover-group-summary[hidden] { display: none; }
    .rollover-group-summary-header {
      display: grid;
      grid-template-columns: 22px 1fr;
      gap: 10px;
      align-items: start;
      margin-bottom: 12px;
    }
    .rollover-group-summary-icon {
      font-size: 16px;
      color: rgb(var(--color-group-rgb));
      line-height: 1;
      margin-top: 1px;
    }
    .rollover-group-summary-title {
      font-family: var(--mono);
      font-size: 12px;
      font-weight: 600;
      letter-spacing: 0.05em;
      text-transform: uppercase;
      color: var(--text);
    }
    .rollover-group-summary-sub {
      font-family: var(--mono);
      font-size: 11px;
      color: var(--muted);
      line-height: 1.4;
      letter-spacing: 0.02em;
      margin-top: 4px;
    }
    .rollover-group-summary-list {
      display: flex;
      flex-direction: column;
      gap: 4px;
      padding: 8px 10px;
      background: rgba(0, 0, 0, 0.22);
      border-radius: 6px;
      margin-bottom: 12px;
      max-height: 220px;
      overflow-y: auto;
    }
    .rollover-group-summary-empty {
      font-family: var(--mono);
      font-size: 11px;
      color: var(--muted);
      letter-spacing: 0.04em;
      padding: 6px 0;
    }
    .rollover-group-summary-row {
      display: grid;
      grid-template-columns: 10px 1fr auto;
      gap: 10px;
      align-items: center;
      padding: 6px 4px;
      font-family: var(--mono);
      font-size: 12px;
      border-bottom: 1px solid rgba(255, 255, 255, 0.04);
    }
    .rollover-group-summary-row:last-child { border-bottom: 0; }
    .rollover-group-summary-dot {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: rgba(255, 255, 255, 0.15);
      flex: 0 0 auto;
    }
    .rollover-group-summary-dot.is-on {
      background: var(--accent, #f5c842);
      box-shadow: 0 0 6px rgba(245, 200, 66, 0.5);
    }
    .rollover-group-summary-name {
      color: var(--text);
      letter-spacing: 0.04em;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    .rollover-group-summary-state {
      font-size: 11px;
      color: var(--muted);
      letter-spacing: 0.04em;
    }
    .rollover-group-summary-state.is-on {
      color: var(--accent, #f5c842);
    }
    .rollover-group-summary-actions {
      display: flex;
      gap: 8px;
      flex-wrap: wrap;
    }
    .rollover-group-action-btn {
      flex: 1 1 auto;
      min-width: 140px;
      padding: 9px 12px;
      font-family: var(--mono);
      font-size: 11px;
      font-weight: 600;
      letter-spacing: 0.06em;
      color: var(--text);
      background: var(--input-bg, rgba(255, 255, 255, 0.04));
      border: 1px solid var(--border, rgba(255, 255, 255, 0.12));
      border-radius: 6px;
      cursor: pointer;
      transition: background 0.12s, border-color 0.12s, color 0.12s;
    }
    .rollover-group-action-btn:hover:not(:disabled) {
      background: rgba(245, 200, 66, 0.08);
      border-color: rgba(245, 200, 66, 0.4);
      color: var(--accent, #f5c842);
    }
    .rollover-group-action-btn:disabled {
      opacity: 0.4;
      cursor: not-allowed;
    }
    .rollover-group-action-btn-danger:hover:not(:disabled) {
      background: rgba(229, 75, 75, 0.10);
      border-color: rgba(229, 75, 75, 0.45);
      color: #ff8a8a;
    }

    /* ── Rollover tab ── */
    .rollover-toggle-row {
      display: flex;
      align-items: center;
      justify-content: space-between;
      flex-wrap: wrap;
      gap: 12px;
      padding-bottom: 10px;
      border-bottom: 1px solid var(--border);
    }

    .rollover-enable-label {
      display: flex;
      align-items: center;
      gap: 8px;
      font-family: var(--mono);
      font-weight: 500;
      font-size: 12px;
      color: var(--text);
      letter-spacing: 0.1em;
      cursor: pointer;
    }

    .rollover-enable-label input { accent-color: var(--accent); cursor: pointer; }

    .rollover-preview-actions {
      display: flex;
      gap: 6px;
    }

    .rollover-preview-btn {
      background: var(--btn-bg);
      color: var(--accent);
      border: 1px solid rgba(245, 200, 66, 0.4);
      padding: 6px 12px;
      font-family: var(--mono);
      font-weight: 500;
      font-size: 11.5px;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      border-radius: 6px;
      cursor: pointer;
      transition: background 0.15s;
      backdrop-filter: blur(12px);
      -webkit-backdrop-filter: blur(12px);
    }

    .rollover-preview-btn:hover { background: rgba(245, 200, 66, 0.10); }

    .rollover-preview-btn:disabled {
      opacity: 0.4;
      cursor: not-allowed;
    }

    /* ── Timeline transport ── glass row, fused with timeline below */
    #transport {
      display: none;
      padding: 10px 16px;
      align-items: center;
      gap: 6px;
      /* glass treatment from shared floating-panel block;
         bottom corners + bottom border + negative margin to fuse with
         #track-editor are set in the .has-manifest rule above. */
      /* Stay on one line at all times. The whole editor has a min-width
         on <main> so the page horizontally scrolls before this row would
         ever need to wrap — wrapping looked broken (controls stacking on
         two rows, time readout splitting across lines). */
      flex-wrap: nowrap;
      white-space: nowrap;
    }

    .transport-btn {
      width: 30px;
      height: 30px;
      background: var(--btn-bg);
      border: 1px solid rgba(255, 255, 255, 0.08);
      border-radius: 8px;
      color: var(--text);
      font-family: var(--mono);
      font-size: 14px;
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: center;
      transition: all 0.12s;
      padding: 0;
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .transport-btn:hover {
      background: rgba(255, 255, 255, 0.08);
      color: var(--accent);
    }

    .transport-btn.active {
      border-color: rgba(245, 200, 66, 0.5);
      color: var(--accent);
      background: rgba(245, 200, 66, 0.10);
    }
    /* Pressed-state outline — accent-colored ring during the click
       (mousedown / Space-bar invocation). Replaces the gold-fill
       treatment that #t-restart used to carry by default; users now
       see a momentary "you clicked this" cue without the button
       living permanently in CTA-state styling. */
    .transport-btn:active {
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.05),
        0 0 0 2px rgba(245, 200, 66, 0.6);
      border-color: rgba(245, 200, 66, 0.8);
      color: var(--accent);
    }

    /* The play button is the visual focal point — warm yellow gradient
       with a centered halo to match the liquid mock. Targeted by id so
       it overrides the generic .transport-btn styling. (Restart used
       to share this fill but it didn't need to compete with Play for
       attention — it now reads as a neutral utility button.) */
    #t-play {
      background: linear-gradient(180deg, #ffd87a, #f5c842);
      color: #fff;
      border-color: rgba(245, 200, 66, 0.6);
      box-shadow:
        0 2px 3px rgba(0, 0, 0, 0.55),
        0 4px 8px rgba(0, 0, 0, 0.35),
        0 0 0 1px rgba(255, 255, 255, 0.10),
        0 0 12px rgba(245, 200, 66, 0.45),
        inset 0 1px 0 rgba(255, 255, 255, 0.4);
    }
    #t-play:hover {
      background: linear-gradient(180deg, #ffe190, #ffd05a);
      color: #fff;
    }

    .scrub-wrap {
      flex: 1;
      min-width: 180px;
      display: flex;
      align-items: center;
      gap: 10px;
    }

    .scrub-time {
      font-family: var(--mono);
      font-weight: 500;
      font-size: 11.5px;
      color: var(--muted);
      letter-spacing: 0.1em;
      min-width: 78px;
      text-align: right;
      /* Reset button defaults so it visually matches the prior <div>. */
      background: none;
      border: none;
      padding: 2px 4px;
      margin: 0;
      cursor: pointer;
      border-radius: 6px;
      transition: color 0.15s, background 0.15s;
    }

    button.scrub-time:hover {
      color: var(--accent);
      background: rgba(245, 200, 66, 0.08);
    }

    button.scrub-time:focus-visible {
      outline: 1px solid var(--accent);
      outline-offset: 1px;
    }

    input[type="range"].scrub {
      flex: 1;
      -webkit-appearance: none;
      appearance: none;
      height: 4px;
      background: rgba(255, 255, 255, 0.08);
      border-radius: 999px;
      cursor: pointer;
      outline: none;
    }

    input[type="range"].scrub::-webkit-slider-thumb {
      -webkit-appearance: none;
      appearance: none;
      width: 12px;
      height: 12px;
      border-radius: 50%;
      background: #fff;
      cursor: pointer;
      border: none;
      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(245, 200, 66, 0.5);
    }

    input[type="range"].scrub::-moz-range-thumb {
      width: 12px;
      height: 12px;
      border-radius: 50%;
      background: #fff;
      cursor: pointer;
      border: none;
      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(245, 200, 66, 0.5);
    }

    .transport-group {
      display: flex;
      align-items: center;
      gap: 8px;
      padding-left: 14px;
      border-left: 1px solid rgba(255, 255, 255, 0.08);
    }

    .transport-group:first-child { border-left: none; padding-left: 0; }

    /* Locked state — applied while the user is on the Rollover tab.
       Speed / Loop / Repeat / Yoyo affect the master timeline only, so
       they're inert during rollover authoring. The actual inputs carry
       the native `disabled` attribute (cursor: not-allowed comes free);
       this rule greys the surrounding label + wrapper to match. */
    .transport-group.tl-locked {
      opacity: 0.4;
      cursor: not-allowed;
    }
    .transport-group.tl-locked .transport-mini-label,
    .transport-group.tl-locked .transport-hint,
    .transport-group.tl-locked input {
      cursor: not-allowed;
    }

    .transport-mini-label {
      font-family: var(--mono);
      font-weight: 500;
      font-size: 11px;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      color: var(--muted);
    }
    .transport-mini-label-2lines {
      font-size: 9px;
      line-height: 1.1;
      letter-spacing: 0.08em;
      text-align: right;
      white-space: normal;
    }

    .transport-mini-input {
      /* Tightened from 56px → 42px. Both Speed (1, 1.5, 2…) and
         Repeat (0, 1, -1…) are 1-3 char numeric fields; the wider
         box was reading as "what's missing here?" with a single
         digit centered in dead space. 42px comfortably fits "-1"
         or "1.5" + the native spinner buttons on Chromium. */
      width: 42px;
      background: var(--input-bg);
      border: 1px solid var(--border);
      border-radius: 6px;
      color: var(--text);
      font-family: var(--mono);
      font-weight: 500;
      font-size: 11.5px;
      padding: 5px 7px;
      box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2);
    }

    .transport-mini-input:focus {
      outline: none;
      border-color: var(--accent);
      box-shadow: 0 0 0 3px var(--accent-soft), inset 0 1px 2px rgba(0, 0, 0, 0.2);
    }

    .transport-mini-input:disabled,
    #t-yoyo:disabled {
      opacity: 0.35;
      cursor: not-allowed;
    }

    .transport-hint {
      font-family: var(--mono);
      font-size: 0.5rem;
      letter-spacing: 0.1em;
      color: var(--muted);
      text-transform: uppercase;
      transition: color 0.2s;
    }
    /* Two-line variant — tightens line-height + keeps the hint
       compact next to its sibling controls. Used for "needs Repeat
       ≥ 1" where the single-line version was sucking up horizontal
       space in the transport bar. */
    .transport-hint-2lines {
      line-height: 1.1;
      text-align: left;
      white-space: normal;
    }

    .transport-hint.flash {
      color: var(--accent);
    }

    .infinite-badge {
      display: none;
      font-family: var(--mono);
      font-size: 0.55rem;
      letter-spacing: 0.1em;
      color: var(--accent);
      border: 1px solid var(--accent);
      background: rgba(245, 200, 66,0.06);
      padding: 3px 7px;
      border-radius: 6px;
      animation: pulse-badge 1.6s ease-in-out infinite;
    }

    .infinite-badge.show { display: inline-block; }

    @keyframes pulse-badge {
      0%, 100% { opacity: 1; }
      50%      { opacity: 0.55; }
    }

    input[type="range"].scrub:disabled {
      opacity: 0.35;
      cursor: not-allowed;
    }

    .scrub-time.muted {
      color: var(--muted);
    }

    /* ── Generic dialog (timeline length, etc.) ── frosted glass card.
       Body copy stays in sans (DM Sans) for readability of long
       paragraphs; the heading + labels use mono uppercase to match the
       rest of the app's label aesthetic. */
    .layrd-dialog {
      background: rgba(20, 18, 22, 0.78);
      backdrop-filter: var(--glass-blur);
      -webkit-backdrop-filter: var(--glass-blur);
      color: var(--text);
      border: 1px solid rgba(255, 255, 255, 0.10);
      border-radius: 14px;
      padding: 24px;
      min-width: 320px;
      max-width: 420px;
      font-family: var(--sans);
      box-shadow:
        0 30px 80px -20px rgba(0, 0, 0, 0.7),
        inset 0 1px 0 rgba(255, 255, 255, 0.06);
    }
    .layrd-dialog::backdrop {
      background: rgba(0, 0, 0, 0.55);
      backdrop-filter: blur(4px);
      -webkit-backdrop-filter: blur(4px);
    }
    .layrd-dialog h3 {
      font-family: var(--mono);
      font-weight: 500;
      font-size: 0.75rem;
      letter-spacing: 0.18em;
      text-transform: uppercase;
      color: var(--accent);
      margin: 0 0 12px;
    }
    .layrd-dialog-hint {
      font-family: var(--sans);
      font-size: 12.5px;
      line-height: 1.5;
      color: var(--muted);
      margin: 0 0 18px;
      text-transform: none;
      letter-spacing: 0;
    }
    .layrd-dialog-row {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 12px;
      margin-bottom: 12px;
    }
    .layrd-dialog-row span {
      font-family: var(--mono);
      font-weight: 500;
      font-size: 12px;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      color: var(--muted);
    }
    .layrd-dialog-row input[type="number"] {
      width: 100px;
      background: var(--input-bg);
      border: 1px solid var(--border);
      border-radius: 6px;
      color: var(--text);
      font-size: 12.5px;
      padding: 6px 9px;
      box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2);
    }
    .layrd-dialog-row input[type="number"]:focus {
      outline: none;
      border-color: var(--accent);
      box-shadow: 0 0 0 3px var(--accent-soft), inset 0 1px 2px rgba(0, 0, 0, 0.2);
    }
    .layrd-dialog-row input[type="number"]:disabled { opacity: 0.35; cursor: not-allowed; }

    .layrd-dialog-fitbtn {
      display: inline-flex;
      align-items: center;
      gap: 6px;
      font-family: var(--mono);
      font-weight: 500;
      font-size: 11.5px;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      color: var(--accent);
      background: var(--btn-bg);
      border: 1px solid rgba(245, 200, 66, 0.4);
      border-radius: 6px;
      padding: 6px 12px;
      margin: 0 0 14px;
      cursor: pointer;
      transition: all 0.15s;
      backdrop-filter: blur(12px);
      -webkit-backdrop-filter: blur(12px);
    }
    .layrd-dialog-fitbtn:hover {
      background: rgba(245, 200, 66, 0.10);
    }
    .layrd-dialog-fitbtn[hidden] { display: none; }

    .layrd-dialog-checkbox {
      display: flex;
      align-items: center;
      gap: 8px;
      font-size: 12.5px;
      color: var(--text);
      cursor: pointer;
      margin-bottom: 12px;
    }

    .layrd-dialog-warn {
      min-height: 1em;
      font-family: var(--mono);
      font-size: 11.5px;
      color: var(--danger);
      letter-spacing: 0.1em;
      margin: 0 0 12px;
    }

    .layrd-dialog-divider {
      height: 1px;
      background: var(--border);
      margin: 6px -8px 14px;
    }

    .layrd-dialog-actions {
      display: flex;
      justify-content: flex-end;
      gap: 8px;
      margin-top: 8px;
    }
    .layrd-dialog-btn {
      font-family: var(--mono);
      font-weight: 500;
      font-size: 12px;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      padding: 8px 16px;
      background: var(--btn-bg);
      color: var(--text);
      border: 1px solid var(--border);
      border-radius: 8px;
      cursor: pointer;
      transition: all 0.15s;
      backdrop-filter: blur(12px);
      -webkit-backdrop-filter: blur(12px);
    }
    .layrd-dialog-btn:hover {
      background: rgba(255, 255, 255, 0.08);
      border-color: rgba(245, 200, 66, 0.4);
      color: var(--accent);
    }
    .layrd-dialog-btn-primary {
      background: linear-gradient(180deg, #ffd87a, #f5c842);
      color: #2a1d00;
      border-color: rgba(245, 200, 66, 0.6);
      font-weight: 600;
      box-shadow: 0 4px 14px rgba(245, 200, 66, 0.25), inset 0 1px 0 rgba(255,255,255,0.3);
    }
    .layrd-dialog-btn-primary:hover {
      background: linear-gradient(180deg, #ffe190, #ffd05a);
      color: #2a1d00;
      border-color: rgba(245, 200, 66, 0.8);
    }

    /* ── Ease curve visualizer ──
       Stacked layout: dropdown on top, larger curve preview underneath.
       The bigger preview makes the curve's shape readable and gives
       the playback dot room to overshoot on back/elastic eases. */
    .ease-row {
      display: flex;
      flex-direction: column;
      align-items: stretch;
      gap: 6px;
    }

    .ease-row .var-input { width: 100%; min-width: 0; }

    /* Invalid easing input — surfaces when gsap.parseEase throws on a
       malformed cubic-bezier(...) or unknown ease name. Without this
       the SVG curve falls back to linear silently, leaving the
       designer wondering why their tween "looks linear". */
    .ease-row .var-input.ease-invalid {
      border-color: rgba(255, 90, 90, 0.8) !important;
      box-shadow: 0 0 0 1px rgba(255, 90, 90, 0.4) inset,
                  0 0 6px rgba(255, 90, 90, 0.25);
    }

    .ease-curve {
      width: 100%;
      height: 64px;
      background: var(--input-bg);
      border: 1px solid var(--border);
      border-radius: 6px;
      box-sizing: border-box;
      padding: 4px;
    }

    .ease-curve-path {
      fill: none;
      stroke: var(--accent);
      stroke-width: 1.5;
      stroke-linecap: round;
      stroke-linejoin: round;
      vector-effect: non-scaling-stroke;
      opacity: 0.55;
    }

    /* Animated playback dot that loops along the ease curve so the user
       sees the actual motion shape, not just the static curve. Position
       is updated by a rAF loop in JS (see makeEaseField). */
    .ease-curve-dot {
      fill: var(--accent);
      filter: drop-shadow(0 0 3px rgba(245, 200, 66, 0.7));
      pointer-events: none;
    }

    .ease-curve-axis {
      stroke: var(--border);
      stroke-width: 1;
      vector-effect: non-scaling-stroke;
    }

    /* ── Select (ease dropdown, blend dropdown, preset picker, etc.) ──
       Custom caret via inline SVG so it picks up the muted accent color
       in a single visual element. Native arrow is hidden via appearance:none. */
    select.var-input {
      appearance: none;
      -webkit-appearance: none;
      background-color: var(--input-bg);
      background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='rgba(240,238,232,0.55)' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");
      background-repeat: no-repeat;
      background-position: right 8px center;
      padding-right: 24px;
    }
    /* Darken the native dropdown popup so it doesn't read as a washed-
       out grey panel — the select itself uses rgba(0,0,0,0.3) so the
       popup inherited a translucent-feeling background. Force a solid
       dark fill on every option so the popup sits clearly on top of
       the canvas / panel chrome. */
    select option {
      background-color: #0a0a0a;
      color: var(--text);
    }

    /* ── Scrollbar ── */
    ::-webkit-scrollbar { width: 4px; }
    ::-webkit-scrollbar-track { background: transparent; }
    ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 6px; }

    /* ── Mono restorations ── liquid prefers sans almost everywhere, but a
       handful of fields read better in monospace: opaque identifiers,
       URLs, code, kbd-hint chips, numeric track timestamps. */
    .layer-z,
    .kbd-hint,
    .vars-id,
    .export-final-url,
    .keyframe-time,
    .var-input.anim-position,
    .layrd-dialog-row input[type="number"],
    .anim-section-hint code,
    .export-hint code {
      font-family: var(--mono);
    }

    /* ── Custom numeric stepper (forward-compat) ──
       Markup needed: <div class="stepper-wrap"><input ...><div class="stepper">
         <button>↑</button><button>↓</button></div></div>
       Replaces native browser spinners with chevron-styled steppers.
       Currently inert in index.html (no .stepper-wrap markup) — these
       rules sit dormant until the markup is added. */
    .stepper-wrap {
      position: relative;
      display: flex;
      align-items: stretch;
    }
    .stepper-wrap > input {
      width: 100%;
      padding-right: 20px;
    }
    .stepper {
      position: absolute;
      right: 1px;
      top: 1px;
      bottom: 1px;
      width: 16px;
      display: flex;
      flex-direction: column;
      border-left: 1px solid var(--border);
      border-radius: 0 6px 6px 0;
      overflow: hidden;
      pointer-events: none;
    }
    .stepper button {
      flex: 1;
      background: transparent;
      border: none;
      color: var(--muted);
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 0;
      pointer-events: auto;
      transition: background 0.12s ease, color 0.12s ease;
    }
    .stepper button:hover {
      background: rgba(255, 255, 255, 0.04);
      color: var(--accent);
    }
    .stepper button svg,
    .stepper button [data-lucide] {
      width: 9px;
      height: 9px;
      stroke-width: 2.5;
    }

    /* Suppress native browser spinners on type=number — applies globally */
    input[type="number"]::-webkit-inner-spin-button,
    input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
    input[type="number"] { -moz-appearance: textfield; }

    /* ── Custom toggle switch (forward-compat) ──
       Markup needed: <label class="switch"><input type="checkbox">
         <span class="switch-track"></span></label>
       Same dormant rationale as the stepper. */
    .switch {
      position: relative;
      display: inline-block;
      width: 32px;
      height: 18px;
      cursor: pointer;
      flex-shrink: 0;
    }
    .switch input {
      opacity: 0;
      width: 0;
      height: 0;
      position: absolute;
    }
    .switch-track {
      position: absolute;
      inset: 0;
      background: var(--border-strong);
      border: 1px solid var(--border-strong);
      border-radius: 999px;
      transition: background 0.18s ease, border-color 0.18s ease;
    }
    .switch-track::before {
      content: '';
      position: absolute;
      width: 14px;
      height: 14px;
      left: 1px;
      top: 1px;
      background: #fff;
      border-radius: 50%;
      box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 1px 1px rgba(0, 0, 0, 0.15);
      transition: transform 0.18s ease;
    }
    .switch input:checked + .switch-track {
      background: var(--accent);
      border-color: var(--accent);
    }
    .switch input:checked + .switch-track::before {
      transform: translateX(14px);
    }
    .switch input:focus-visible + .switch-track {
      box-shadow: 0 0 0 3px var(--accent-soft);
    }

    /* ── Switch-styled checkboxes ──
       Native <input type="checkbox"> rendered as a Liquid pill toggle
       (yellow track when on, white thumb sliding right). Targets the
       specific checkboxes that act as feature toggles in the editor —
       Loop / Yoyo (transport), Exit phase enable, Rollover enable,
       Visible (var-input on a layer), and the timeline-length dialog
       Auto / Stop-frame None toggles.
       Checkboxes that read better as small ticks (Static-tab transparency,
       per-video options, hidden state-mirror checkboxes) keep native styling. */
    #t-loop,
    #t-yoyo,
    #t-paused-load,
    #cs-bg-transparent,
    #exit-enabled,
    #rollover-enabled,
    #dlg-length-auto,
    #dlg-stopat-none,
    #export-tl-auto,
    #export-tl-stopat-none,
    #import-entry-preserve-time,
    #import-exit-preserve-time,
    #pause-hover-preview,
    .toggle-switch,
    .var-check-wrap > input[type="checkbox"] {
      appearance: none;
      -webkit-appearance: none;
      flex-shrink: 0;
      width: 34px;
      height: 20px;
      margin: 0;
      background: var(--border-strong);
      border: 1px solid var(--border-strong);
      border-radius: 999px;
      position: relative;
      cursor: pointer;
      transition: background 0.18s ease, border-color 0.18s ease;
      vertical-align: middle;
    }
    #t-loop::before,
    #t-yoyo::before,
    #t-paused-load::before,
    #cs-bg-transparent::before,
    #exit-enabled::before,
    #rollover-enabled::before,
    #dlg-length-auto::before,
    #dlg-stopat-none::before,
    #export-tl-auto::before,
    #export-tl-stopat-none::before,
    #import-entry-preserve-time::before,
    #import-exit-preserve-time::before,
    #pause-hover-preview::before,
    .toggle-switch::before,
    .var-check-wrap > input[type="checkbox"]::before {
      content: '';
      position: absolute;
      width: 14px;
      height: 14px;
      left: 2px;
      top: 2px;
      background: #fff;
      border-radius: 50%;
      box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 1px 1px rgba(0, 0, 0, 0.15);
      transition: transform 0.18s ease;
    }
    #t-loop:checked,
    #t-yoyo:checked,
    #t-paused-load:checked,
    #cs-bg-transparent:checked,
    #exit-enabled:checked,
    #rollover-enabled:checked,
    #dlg-length-auto:checked,
    #dlg-stopat-none:checked,
    #export-tl-auto:checked,
    #export-tl-stopat-none:checked,
    #import-entry-preserve-time:checked,
    #import-exit-preserve-time:checked,
    #pause-hover-preview:checked,
    .toggle-switch:checked,
    .var-check-wrap > input[type="checkbox"]:checked {
      background: linear-gradient(180deg, #ffd87a, #f5c842);
      border-color: rgba(245, 200, 66, 0.6);
      box-shadow: 0 0 12px rgba(245, 200, 66, 0.35);
    }
    #t-loop:checked::before,
    #t-yoyo:checked::before,
    #t-paused-load:checked::before,
    #cs-bg-transparent:checked::before,
    #exit-enabled:checked::before,
    #rollover-enabled:checked::before,
    #dlg-length-auto:checked::before,
    #dlg-stopat-none:checked::before,
    #export-tl-auto:checked::before,
    #export-tl-stopat-none:checked::before,
    #import-entry-preserve-time:checked::before,
    #import-exit-preserve-time:checked::before,
    #pause-hover-preview:checked::before,
    .toggle-switch:checked::before,
    .var-check-wrap > input[type="checkbox"]:checked::before {
      transform: translateX(14px);
    }
    #t-loop:focus-visible,
    #t-yoyo:focus-visible,
    #t-paused-load:focus-visible,
    #cs-bg-transparent:focus-visible,
    #exit-enabled:focus-visible,
    #rollover-enabled:focus-visible,
    #dlg-length-auto:focus-visible,
    #dlg-stopat-none:focus-visible,
    #export-tl-auto:focus-visible,
    #export-tl-stopat-none:focus-visible,
    #import-entry-preserve-time:focus-visible,
    #import-exit-preserve-time:focus-visible,
    .var-check-wrap > input[type="checkbox"]:focus-visible {
      outline: none;
      box-shadow: 0 0 0 3px var(--accent-soft);
    }
    #t-loop:disabled,
    #t-yoyo:disabled,
    #t-paused-load:disabled,
    #cs-bg-transparent:disabled,
    #exit-enabled:disabled,
    #rollover-enabled:disabled,
    #dlg-length-auto:disabled,
    #dlg-stopat-none:disabled,
    #export-tl-auto:disabled,
    #export-tl-stopat-none:disabled,
    #import-entry-preserve-time:disabled,
    #import-exit-preserve-time:disabled,
    .var-check-wrap > input[type="checkbox"]:disabled {
      opacity: 0.4;
      cursor: not-allowed;
    }

    /* ── Custom modal (alert / confirm / prompt replacement) ──
       Full-viewport overlay with a slight black wash + blur so the user
       can ONLY interact with the modal until they make a choice. The
       modal card uses the same liquid glass treatment as the timeline-
       length dialog. */
    .app-modal {
      position: fixed;
      inset: 0;
      z-index: 9999;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 20px;
      background: rgba(0, 0, 0, 0.55);
      backdrop-filter: blur(6px);
      -webkit-backdrop-filter: blur(6px);
      animation: app-modal-fade 0.18s ease-out;
    }
    .app-modal[hidden] { display: none; }

    @keyframes app-modal-fade {
      from { opacity: 0; }
      to   { opacity: 1; }
    }

    .app-modal-card {
      /* Bumped from 460px → 720px (capped at viewport - 40px) so the
         confirm dialogs that explain destructive actions ("Continue —
         apply preset", PSD re-import) read on a few clean lines instead
         of hyphenating across 6+ rows in a 460px column, AND so the
         200px-min-width action buttons land side-by-side without one of
         them getting clipped by the card edge. */
      min-width: 320px;
      width: clamp(360px, 90vw, 720px);
      padding: 28px 32px 24px;
      background: rgba(20, 18, 22, 0.85);
      backdrop-filter: var(--glass-blur);
      -webkit-backdrop-filter: var(--glass-blur);
      border: 1px solid rgba(255, 255, 255, 0.10);
      border-radius: 14px;
      box-shadow:
        0 30px 80px -20px rgba(0, 0, 0, 0.7),
        inset 0 1px 0 rgba(255, 255, 255, 0.06);
      animation: app-modal-pop 0.18s ease-out;
    }
    @keyframes app-modal-pop {
      from { opacity: 0; transform: scale(0.96); }
      to   { opacity: 1; transform: scale(1); }
    }

    /* Modal title — rendered above the body when the caller's
       message contains a paragraph break. Larger + heavier so the
       primary question / action reads at a glance; the body details
       (counts, undo hint, etc.) sit underneath at the existing
       size. */
    .app-modal-title {
      font-family: var(--sans);
      font-size: 18px;
      font-weight: 600;
      line-height: 1.3;
      color: var(--text);
      letter-spacing: -0.005em;
      margin-bottom: 10px;
      white-space: pre-wrap;
    }
    .app-modal-title[hidden] { display: none; }

    .app-modal-message {
      font-family: var(--sans);
      font-size: 14px;
      line-height: 1.55;
      color: var(--text);
      letter-spacing: 0;
      text-transform: none;
      margin-bottom: 18px;
      white-space: pre-wrap;
    }
    /* Slightly soften the body when paired with a title — the title
       carries the primary weight; the body becomes supporting detail. */
    .app-modal-title:not([hidden]) + .app-modal-message {
      color: var(--muted);
    }

    .app-modal-input {
      width: 100%;
      margin-bottom: 16px;
      font-size: 13px;
      padding: 8px 10px;
    }
    .app-modal-input[hidden] { display: none; }

    /* (Auth modal styles removed — the `#auth-modal` markup was
       deleted when the full-screen sign-in wall (`#signin-wall`)
       replaced it. The .auth-modal-card / -label / -input / -hint
       classes have no matching markup. Deleted in the 2026-05-28
       audit cleanup. Live sign-in styles live under .signin-* /
       .signin-wall / .signin-card.) */

    /* Optional toggle cluster between the modal message and the
       action buttons. Used by the preset-paste flow to gate
       "Replace text" and "Replace custom code". Each row is a
       two-column grid: label + hint stacked on the left, pill-style
       toggle on the right. Pill is the shared .toggle-switch styling
       so the modal feels consistent with the rest of the editor's
       Loop / Yoyo / Pause-hover toggles. */
    .app-modal-extras {
      display: flex;
      flex-direction: column;
      gap: 4px;
      margin-bottom: 18px;
      padding: 4px 0;
      border-top: 1px solid var(--border, rgba(255, 255, 255, 0.1));
      border-bottom: 1px solid var(--border, rgba(255, 255, 255, 0.1));
    }
    .app-modal-extras[hidden] { display: none; }
    .app-modal-extra-row {
      display: grid;
      grid-template-columns: 1fr auto;
      gap: 14px;
      align-items: center;
      padding: 12px 4px;
      cursor: pointer;
      transition: background 0.12s ease;
      border-radius: 4px;
    }
    .app-modal-extra-row:hover {
      background: rgba(255, 255, 255, 0.03);
    }
    .app-modal-extra-row + .app-modal-extra-row {
      border-top: 1px solid var(--border, rgba(255, 255, 255, 0.08));
    }
    .app-modal-extra-text {
      display: flex;
      flex-direction: column;
      gap: 4px;
      min-width: 0;
    }
    .app-modal-extra-label {
      font-family: var(--mono);
      font-size: 12px;
      font-weight: 600;
      letter-spacing: 0.05em;
      color: var(--text);
      line-height: 1.35;
    }
    .app-modal-extra-hint {
      font-family: var(--mono);
      font-size: 11px;
      color: var(--muted);
      line-height: 1.45;
      letter-spacing: 0.02em;
    }

    .app-modal-actions {
      display: flex;
      justify-content: flex-end;
      gap: 8px;
      flex-wrap: nowrap;
    }

    .app-modal-btn {
      font-family: var(--mono);
      font-weight: 500;
      font-size: 11.5px;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      /* Minimum width keeps the action buttons readable + symmetrical even
         with terse labels ("OK"); the labels themselves stay on a single
         line via white-space: nowrap so longer prompts ("Continue with
         import") don't wrap to a 2nd line in the narrow modal. */
      min-width: 200px;
      padding: 10px 18px;
      background: var(--btn-bg);
      color: var(--text);
      border: 1px solid var(--border);
      border-radius: 8px;
      cursor: pointer;
      transition: all 0.15s;
      backdrop-filter: blur(12px);
      -webkit-backdrop-filter: blur(12px);
      white-space: nowrap;
    }
    .app-modal-btn[hidden] { display: none; }
    .app-modal-btn:hover {
      background: rgba(255, 255, 255, 0.08);
      border-color: rgba(245, 200, 66, 0.4);
      color: var(--accent);
    }
    .app-modal-btn-primary {
      background: linear-gradient(180deg, #ffd87a, #f5c842);
      color: #2a1d00;
      border-color: rgba(245, 200, 66, 0.6);
      font-weight: 600;
      box-shadow: 0 4px 14px rgba(245, 200, 66, 0.25), inset 0 1px 0 rgba(255,255,255,0.3);
    }
    .app-modal-btn-primary:hover {
      background: linear-gradient(180deg, #ffe190, #ffd05a);
      color: #2a1d00;
      border-color: rgba(245, 200, 66, 0.8);
    }

    /* ── Keyboard shortcuts modal ─────────────────────────────────────
       Re-skins .app-modal-card into a wider, scrollable reference panel
       with a 2-column grid of category groups. Each shortcut row is a
       flex row: keys on the left, description on the right. */
    .shortcuts-modal-card {
      width: clamp(520px, 90vw, 880px);
      max-height: calc(100vh - 80px);
      display: flex;
      flex-direction: column;
      padding: 20px 24px 18px;
    }
    .shortcuts-modal-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 16px;
      margin-bottom: 14px;
      padding-bottom: 12px;
      border-bottom: 1px solid rgba(255, 255, 255, 0.08);
    }
    .shortcuts-modal-title {
      font-family: var(--mono);
      font-size: 12px;
      font-weight: 500;
      letter-spacing: 0.18em;
      text-transform: uppercase;
      color: var(--text);
      margin: 0;
    }
    .shortcuts-modal-close {
      background: transparent;
      border: 1px solid rgba(255,255,255,0.10);
      color: var(--text-muted);
      font-size: 18px;
      line-height: 1;
      width: 28px;
      height: 28px;
      border-radius: 6px;
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 0;
      transition: all 0.15s;
    }
    .shortcuts-modal-close:hover {
      background: rgba(255,255,255,0.06);
      border-color: rgba(245, 200, 66, 0.4);
      color: var(--accent);
    }
    .shortcuts-modal-body {
      display: block;
      overflow-y: auto;
      padding-right: 4px;
    }
    .shortcuts-modal-body::-webkit-scrollbar       { width: 8px; }
    .shortcuts-modal-body::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.10); border-radius: 4px; }
    .shortcuts-modal-body::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.18); }
    /* Two-column shell: shortcuts on the left (wider, themselves a
       2-column grid), color legends on the right rail. Collapses to
       a single stacked column at narrow widths. */
    .shortcuts-modal-cols {
      display: grid;
      grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
      gap: 0 32px;
    }
    .shortcuts-modal-col-left {
      /* Multi-column flow (vs. grid rows) packs groups tightly into
         the two columns instead of leaving gaps when a short group
         sits next to a tall one. break-inside: avoid on .shortcuts-
         group keeps each section intact across the column break. */
      column-count: 2;
      column-gap: 28px;
    }
    .shortcuts-modal-col-right {
      display: flex;
      flex-direction: column;
      gap: 4px;
      border-left: 1px solid rgba(255, 255, 255, 0.06);
      padding-left: 24px;
    }
    @media (max-width: 860px) {
      .shortcuts-modal-cols { grid-template-columns: 1fr; gap: 12px 0; }
      .shortcuts-modal-col-left { grid-template-columns: 1fr; }
      .shortcuts-modal-col-right { border-left: none; padding-left: 0; border-top: 1px solid rgba(255, 255, 255, 0.06); padding-top: 12px; }
    }
    .shortcuts-group {
      break-inside: avoid;
      padding: 10px 0 4px;
    }
    .shortcuts-group-title {
      font-family: var(--mono);
      font-size: 10px;
      font-weight: 600;
      letter-spacing: 0.18em;
      text-transform: uppercase;
      color: var(--accent);
      margin: 0 0 8px;
    }
    .shortcuts-row {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 12px;
      padding: 4px 0;
      font-size: 12px;
      line-height: 1.4;
      color: var(--text);
    }
    .shortcuts-row + .shortcuts-row {
      border-top: 1px dashed rgba(255,255,255,0.05);
    }
    .shortcuts-row .shortcuts-keys {
      display: flex;
      align-items: center;
      gap: 4px;
      flex-shrink: 0;
    }
    .shortcuts-row .shortcuts-desc {
      color: var(--text-muted);
      text-align: right;
      font-size: 11.5px;
      line-height: 1.35;
    }
    .shortcuts-modal-footer {
      margin-top: 14px;
      padding-top: 10px;
      border-top: 1px solid rgba(255, 255, 255, 0.06);
      font-family: var(--mono);
      font-size: 10.5px;
      letter-spacing: 0.06em;
      color: var(--text-muted);
      text-align: center;
    }
    /* Key-cap styling — used inside the modal AND inline elsewhere
       (e.g., the canvas-area discoverability hint banner). */
    .kbd {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      min-width: 22px;
      height: 22px;
      padding: 0 6px;
      font-family: var(--mono);
      font-size: 10.5px;
      font-weight: 500;
      letter-spacing: 0.04em;
      color: var(--text);
      background: rgba(255, 255, 255, 0.06);
      border: 1px solid rgba(255, 255, 255, 0.14);
      border-radius: 5px;
      box-shadow: 0 1px 0 rgba(255, 255, 255, 0.06) inset, 0 1px 1px rgba(0,0,0,0.4);
      white-space: nowrap;
      vertical-align: middle;
    }
    .shortcuts-modal-footer .kbd {
      background: rgba(0,0,0,0.25);
    }
    .kbd-plus {
      color: var(--text-muted);
      font-size: 10px;
      margin: 0 1px;
    }
    /* Separator between paired modifier glyphs (⌘ / Ctrl) when a
       shortcut applies cross-platform. Matches kbd-plus visual
       weight so the key cluster reads as one unit. */
    .kbd-or {
      color: var(--text-muted);
      font-size: 10px;
      margin: 0 2px;
    }

    /* ── PSD-replace remap modal ──────────────────────────────────────
       Shown when the user clicks "Update assets" on a library row and
       picks an edited PSD. Side-by-side diff of old vs new layers with
       inline pairing dropdowns for renames. Reuses .app-modal as the
       overlay; this only re-skins the inner card. */
    .remap-modal-card {
      width: clamp(560px, 92vw, 940px);
      max-height: calc(100vh - 80px);
      display: flex;
      flex-direction: column;
      padding: 0;
    }
    .remap-modal-header {
      display: flex;
      align-items: flex-start;
      justify-content: space-between;
      gap: 16px;
      padding: 22px 28px 16px;
      border-bottom: 1px solid var(--border);
    }
    .remap-modal-title {
      font-family: var(--display);
      font-size: 22px;
      letter-spacing: 0.04em;
      color: var(--text);
      line-height: 1.1;
    }
    .remap-modal-subtitle {
      font-family: var(--mono);
      font-size: 11px;
      letter-spacing: 0.12em;
      text-transform: uppercase;
      color: var(--muted);
      margin-top: 6px;
    }
    .remap-modal-body {
      padding: 16px 28px 4px;
      overflow-y: auto;
      flex: 1;
      min-height: 0;
    }
    .remap-modal-body::-webkit-scrollbar { width: 8px; }
    .remap-modal-body::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.10); border-radius: 4px; }
    .remap-modal-body::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.18); }

    .remap-canvas-warn {
      background: rgba(255, 107, 107, 0.08);
      border: 1px solid rgba(255, 107, 107, 0.30);
      border-radius: 8px;
      padding: 12px 14px;
      margin-bottom: 16px;
      color: rgba(255, 200, 200, 0.95);
      font-size: 12.5px;
      line-height: 1.5;
    }
    .remap-canvas-warn b { color: var(--text); }

    .remap-section {
      margin-bottom: 18px;
    }
    .remap-section-head {
      display: flex;
      align-items: center;
      gap: 10px;
      padding: 8px 0;
      cursor: pointer;
      user-select: none;
    }
    .remap-section-head .remap-section-title {
      font-family: var(--mono);
      font-size: 11px;
      letter-spacing: 0.14em;
      text-transform: uppercase;
      font-weight: 500;
    }
    .remap-section-head .remap-section-count {
      font-family: var(--mono);
      font-size: 11px;
      color: var(--muted);
      letter-spacing: 0.06em;
    }
    .remap-section-head .remap-section-chevron {
      font-family: var(--mono);
      color: var(--muted);
      font-size: 10px;
      transition: transform 0.15s;
    }
    .remap-section.collapsed .remap-section-chevron { transform: rotate(-90deg); }
    .remap-section.collapsed .remap-section-body { display: none; }

    .remap-section-matched .remap-section-title { color: var(--color-mask, var(--success)); }
    .remap-section-unmatched .remap-section-title { color: var(--color-exit, #ffaa3c); }
    .remap-section-new .remap-section-title { color: var(--muted); }

    .remap-row {
      display: grid;
      grid-template-columns: 1.4fr auto 1fr;
      align-items: center;
      gap: 12px;
      padding: 8px 12px;
      background: rgba(0, 0, 0, 0.20);
      border: 1px solid var(--border);
      border-radius: 6px;
      margin-bottom: 6px;
      font-size: 12.5px;
    }
    .remap-row .remap-old {
      font-family: var(--mono);
      color: var(--text);
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    .remap-row .remap-old-anim {
      font-family: var(--mono);
      font-size: 10px;
      color: var(--muted);
      letter-spacing: 0.04em;
      margin-top: 2px;
    }
    .remap-row .remap-arrow {
      color: var(--muted);
      font-family: var(--mono);
      letter-spacing: 0.1em;
    }
    .remap-row select {
      width: 100%;
      background: var(--input-bg);
      color: var(--text);
      border: 1px solid var(--border-mid, var(--border));
      border-radius: 6px;
      padding: 6px 8px;
      font-family: var(--mono);
      font-size: 12px;
    }
    .remap-row select:focus {
      outline: none;
      border-color: var(--accent);
    }

    .remap-row.matched {
      background: rgba(94, 198, 132, 0.05);
      border-color: rgba(94, 198, 132, 0.18);
    }
    .remap-row.matched .remap-old-tag {
      color: var(--color-mask, var(--success));
      font-family: var(--mono);
      font-size: 10px;
      letter-spacing: 0.12em;
      text-transform: uppercase;
    }

    .remap-empty {
      font-family: var(--mono);
      font-size: 11.5px;
      color: var(--muted-soft, var(--muted));
      letter-spacing: 0.05em;
      padding: 8px 0;
    }

    .remap-modal-footer {
      padding: 14px 28px 22px;
      border-top: 1px solid var(--border);
      display: flex;
      justify-content: space-between;
      align-items: center;
      gap: 12px;
    }
    .remap-summary {
      font-family: var(--mono);
      font-size: 11.5px;
      letter-spacing: 0.06em;
      color: var(--muted);
    }
    .remap-summary .pass { color: var(--color-mask, var(--success)); }
    .remap-summary .warn { color: var(--color-exit, #ffaa3c); }
    .remap-modal-footer .app-modal-actions {
      margin: 0;
    }

    /* ── Modifier-drag discoverability ─────────────────────────────────
       Two surfaces, both data-driven by the same SHORTCUTS source:
       - .canvas-hint: top-center floating banner shown until dismissed.
       - .phase-mod-hint: tiny chip embedded in the Entry/Exit pills so
         the alt↔START / cmd↔EXIT mapping stays visible after the
         banner is dismissed. */
    .canvas-hint {
      position: absolute;
      /* Moved from top-center to bottom-center — at the top it
         competed with the align/zoom toolbars for the same row and
         overlapped the artboard's top edge on smaller canvases.
         Bottom of the artboard area is empty in most layouts so the
         banner reads cleanly there. */
      bottom: 12px;
      left: 50%;
      transform: translateX(-50%);
      z-index: 50;
      display: flex;
      align-items: center;
      gap: 10px;
      padding: 7px 10px 7px 14px;
      background: rgba(20, 18, 22, 0.85);
      backdrop-filter: var(--glass-blur);
      -webkit-backdrop-filter: var(--glass-blur);
      border: 1px solid rgba(255, 255, 255, 0.10);
      border-radius: 10px;
      /* Shadow points UPWARD now that the banner sits at the bottom
         (was pointing down to the canvas before). */
      box-shadow: 0 -6px 20px -8px rgba(0, 0, 0, 0.6);
      font-family: var(--mono);
      font-size: 11px;
      color: var(--text-muted);
      letter-spacing: 0.04em;
      white-space: nowrap;
      /* Reserve clearance for any bottom-anchored controls so the
         centered banner doesn't slide under them on narrow viewports. */
      max-width: calc(100% - 340px);
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .canvas-hint[hidden] { display: none; }
    #canvas-area.is-empty .canvas-hint { display: none; }
    .canvas-hint-icon {
      flex-shrink: 0;
      font-size: 13px;
      line-height: 1;
    }
    .canvas-hint-text {
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .canvas-hint-tag {
      display: inline-block;
      padding: 1px 5px;
      border-radius: 4px;
      font-size: 9.5px;
      font-weight: 600;
      letter-spacing: 0.1em;
      vertical-align: 1px;
    }
    .canvas-hint-tag-from {
      background: #2a2a2a;
      color: #f5c842;
      border: 1px solid rgba(245, 200, 66, 0.45);
    }
    .canvas-hint-tag-exit {
      background: #2a2a2a;
      color: #ffaa3c;
      border: 1px solid rgba(255, 170, 60, 0.45);
    }
    .canvas-hint-close {
      flex-shrink: 0;
      background: transparent;
      border: none;
      color: var(--text-muted);
      font-size: 16px;
      line-height: 1;
      width: 22px;
      height: 22px;
      border-radius: 5px;
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 0;
      transition: all 0.15s;
    }
    .canvas-hint-close:hover {
      background: rgba(255, 255, 255, 0.06);
      color: var(--text);
    }

    /* Phase-pill modifier hint chips. Tiny, subtle — they're a constant
       reminder once the banner is dismissed without dominating the pill. */
    .phase-mod-hint {
      display: inline-block;
      margin-left: 6px;
      padding: 1px 5px;
      font-size: 9px;
      font-weight: 500;
      letter-spacing: 0.06em;
      text-transform: lowercase;
      color: var(--text-muted);
      background: rgba(255, 255, 255, 0.04);
      border: 1px solid rgba(255, 255, 255, 0.08);
      border-radius: 4px;
      vertical-align: 1px;
      transition: all 0.15s;
    }
    .anim-phase-tab.active .phase-mod-hint {
      color: var(--accent);
      background: rgba(245, 200, 66, 0.10);
      border-color: rgba(245, 200, 66, 0.25);
    }
    .anim-phase-tab.active .phase-mod-hint.phase-mod-hint-exit {
      color: #ffaa3c;
      background: rgba(255, 170, 60, 0.10);
      border-color: rgba(255, 170, 60, 0.25);
    }

    /* ── Toast stack ──────────────────────────────────────────────────
       Bottom-center ephemeral confirmations. Stack grows upward (column-
       reverse) so the newest toast lands closest to the viewport edge
       and older ones slide up out of the way. The stack is pointer-
       events: none so it never intercepts canvas/timeline interaction;
       individual toasts re-enable pointer-events for click-to-dismiss. */
    .toast-stack {
      position: fixed;
      left: 50%;
      bottom: 24px;
      transform: translateX(-50%);
      z-index: 9998;       /* below app-modal (9999), above everything else */
      display: flex;
      flex-direction: column-reverse;
      align-items: center;
      gap: 8px;
      pointer-events: none;
      max-width: calc(100vw - 40px);
    }
    .toast {
      pointer-events: auto;
      display: flex;
      align-items: center;
      gap: 10px;
      padding: 10px 14px;
      max-width: 460px;
      min-width: 220px;
      /* Match the modal card chrome — same glass background, same
         border, same inset highlight — so toasts read as a peer
         surface, not a third-party widget. */
      font-family: var(--mono);
      font-size: 12px;
      line-height: 1.4;
      letter-spacing: 0.02em;
      color: var(--text);
      background: rgba(20, 18, 22, 0.85);
      backdrop-filter: var(--glass-blur);
      -webkit-backdrop-filter: var(--glass-blur);
      border: 1px solid rgba(255, 255, 255, 0.10);
      border-radius: 12px;
      box-shadow:
        0 12px 30px -10px rgba(0,0,0,0.6),
        inset 0 1px 0 rgba(255,255,255,0.06);
      cursor: pointer;
      animation: toast-slide-up 0.22s cubic-bezier(0.2, 0.7, 0.3, 1.2);
      transition: opacity 0.18s, transform 0.18s, border-color 0.15s;
    }
    .toast:hover {
      border-color: rgba(245, 200, 66, 0.25);
    }
    .toast.toast-leaving {
      opacity: 0;
      transform: translateY(8px);
      pointer-events: none;
    }
    @keyframes toast-slide-up {
      from { opacity: 0; transform: translateY(16px); }
      to   { opacity: 1; transform: translateY(0); }
    }
    /* Tone is a small color-coded dot to the left of the message —
       same affordance as the layer-status dots elsewhere in the UI.
       No colored borders or filled icons that would clash with the
       app's flat-glass language. */
    .toast-icon {
      flex-shrink: 0;
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-muted);
      box-shadow: 0 0 8px currentColor;
      color: var(--text-muted);
      font-size: 0;        /* hide any text fallback inside the span */
      line-height: 0;
    }
    .toast-message {
      flex: 1 1 auto;
      line-height: 1.4;
    }
    /* In-message kbd chips inherit the global .kbd style but tighten
       up so they don't overpower the 12px body text. */
    .toast-message kbd,
    .toast-message .kbd {
      font-size: 10px;
      padding: 0 4px;
      min-width: 18px;
      height: 18px;
      vertical-align: 0;
    }
    /* Tone variants — only the dot color changes. The body text and
       border stay neutral so success/warn/info/error all visually
       belong to the same family. */
    .toast-info    .toast-icon { background: rgba(200, 220, 255, 0.75); color: rgba(200, 220, 255, 0.4); }
    .toast-success .toast-icon { background: rgba(120, 220, 140, 0.85); color: rgba(120, 220, 140, 0.5); }
    .toast-warn    .toast-icon { background: var(--accent);              color: rgba(245, 200, 66, 0.5); }
    .toast-error   .toast-icon { background: rgba(240, 90, 90, 0.9);     color: rgba(240, 90, 90, 0.5); }

    /* ── Editor-mode toggle (Simple / Advanced) ──
       Two-pill switch in the vars-header next to the layer-id chip.
       Active pill carries .editor-mode-active. Clicking either pill
       runs setEditorMode() in JS, which writes localStorage and flips
       the `body.editor-mode-simple` class that drives the show/hide
       rules below. */
    .vars-header-top {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 10px;
      order: -1;
      padding: 4px 0 2px;
      flex-wrap: wrap;
    }

    .editor-mode-toggle {
      display: inline-flex;
      align-items: center;
      gap: 2px;
      background: rgba(0, 0, 0, 0.3);
      border: 1px solid var(--border);
      border-radius: 999px;
      padding: 2px;
      flex-shrink: 0;
    }
    .editor-mode-pill {
      font-family: var(--mono);
      font-size: 9.5px;
      font-weight: 500;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      color: var(--muted);
      background: transparent;
      border: none;
      border-radius: 999px;
      padding: 4px 10px;
      cursor: pointer;
      /* Sits above the sliding .toggle-active-indicator. */
      position: relative;
      z-index: 1;
      /* color transition only — transform/background are owned by the
         indicator (animated by GSAP). 'all' here would smear text-color
         alongside the slide and read as laggy. */
      transition: color 0.24s ease-out;
    }
    /* Hover-to-white only applies to the INACTIVE pill — the active
       pill keeps its dark text (#2a1d00) so the gold indicator + dark
       label stay stable while the cursor passes over it. */
    .editor-mode-pill:hover:not(:disabled):not(.editor-mode-active) { color: var(--text); }
    .editor-mode-pill.editor-mode-active {
      /* Background + box-shadow now live on .toggle-active-indicator
         which GSAP slides into place. Active pill just darkens text. */
      color: #2a1d00;
    }
    /* One-shot glow on the newly-active pill after a mode switch.
       Helps users notice the panel content swap when sections
       appear/disappear wholesale. Fades out over 1.6s. */
    @keyframes editor-mode-pill-glow {
      0%   { box-shadow: 0 0 0 0   rgba(245, 200, 66, 0.9), inset 0 1px 0 rgba(255, 255, 255, 0.3); }
      30%  { box-shadow: 0 0 12px 4px rgba(245, 200, 66, 0.55), inset 0 1px 0 rgba(255, 255, 255, 0.3); }
      100% { box-shadow: 0 0 0 0   rgba(245, 200, 66, 0),   inset 0 1px 0 rgba(255, 255, 255, 0.3); }
    }
    .editor-mode-pill.just-switched {
      animation: editor-mode-pill-glow 1.6s ease-out;
    }

    /* ── Sliding active-indicator for animated segmented toggles ──
       One indicator element per toggle group, inserted as the first
       child of the toggle container by animated-toggles.js. GSAP
       tweens its x/y/width/height so it appears to slide between
       buttons whenever the active class moves. Buttons sit above
       it via position: relative; z-index: 1. */
    .toggle-active-indicator {
      position: absolute;
      top: 0;
      left: 0;
      width: 0;
      height: 0;
      pointer-events: none;
      /* No CSS transition — GSAP owns motion. A transition here would
         double-animate every frame GSAP touches the inline transform. */
      transition: none;
      z-index: 0;
    }
    /* Pill — for .header-editor-mode-toggle (Simple / Advanced).
       Mirrors the old .editor-mode-pill.editor-mode-active fill. */
    .toggle-active-indicator--pill {
      background: linear-gradient(180deg, #ffd87a, #f5c842);
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3);
      border-radius: 999px;
    }
    /* Phase — for .anim-phase-tabs (Entry / Exit). Mirrors the old
       .anim-phase-tab.active fill. */
    .toggle-active-indicator--phase {
      background: rgba(245, 200, 66, 0.14);
      border: 1px solid rgba(245, 200, 66, 0.4);
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06),
                  0 0 6px -2px rgba(245, 200, 66, 0.4);
      border-radius: 4px;
    }
    /* Underline — for .vars-tabs (Static / Animation / Rollover).
       Pinned to the bottom edge of the row by the helper. Mirrors
       the old .vars-tab.active border-bottom: 2px solid var(--accent). */
    .toggle-active-indicator--underline {
      background: var(--accent);
      border-radius: 1px;
    }

    /* ── Simple-mode show/hide ──
       Hide every advanced control inside the Animation + Rollover tabs.
       The Static tab stays untouched in both modes — that's properties,
       not motion. */
    body.editor-mode-simple #entry-meta-section,
    body.editor-mode-simple #from-rest-section,
    body.editor-mode-simple .keyframes-section,
    body.editor-mode-simple #exit-meta-section,
    body.editor-mode-simple #exit-to-section { display: none; }

    /* Vector-edit panel — Simple mode dims the kf-authoring affordances
       (toggle pill, + Add capture button, kf cards list) so the user
       can see the kf data exists but can't edit it. Clicks fall to a
       capture-phase handler that shows a "switch to Advanced" toast.
       Presets grid, anchor inputs, repeat/yoyo, and stroke-draw stay
       fully interactive — those are valid Simple-mode actions. */
    body.editor-mode-simple #mask-anim-kf-toggle,
    body.editor-mode-simple #mask-anim-add-kf,
    body.editor-mode-simple #mask-anim-kf-list {
      opacity: 0.42;
      cursor: not-allowed;
      filter: grayscale(0.4);
    }
    body.editor-mode-simple #mask-anim-kf-toggle:hover,
    body.editor-mode-simple #mask-anim-add-kf:hover {
      background: rgba(94, 198, 132, 0.06);
      border-color: rgba(94, 198, 132, 0.25);
    }
    body.editor-mode-simple #mask-anim-kf-list * {
      cursor: not-allowed !important;
      pointer-events: none;
    }
    /* JS sets #rollover-config.style.display='block' when rollover is enabled
       (line ~6364 in index.html). Inline styles beat ID selectors, so
       we need !important to keep the meta/hover-values grid hidden in Simple
       even when the layer has rollover turned on. */
    body.editor-mode-simple #rollover-config { display: none !important; }

    /* Bottom-of-phase "× Remove (entry|exit) animation" CTA. Used twice —
       once at the bottom of #anim-entry-content, once at the bottom of
       #anim-exit-content. Visible in both Simple and Advanced; centered,
       full-width, separated from the sections above by a thin divider. */
    .anim-clear-footer {
      margin-top: 14px;
      padding-top: 14px;
      border-top: 1px solid var(--border);
    }
    .anim-clear-footer .clear-all-anim-btn-inline {
      width: 100%;
      text-align: center;
      padding: 10px 12px;
    }
    /* Save / Import preset pair — sits above the Remove button. Two
       equal-width glass-style buttons; reuses the existing .preset-io-btn
       look but with tighter padding + smaller type so both labels fit on
       a single line in the 340px right-panel column (the previous default
       wrapped "↓ IMPORT PRESET" to two rows, leaving the row visually
       asymmetric next to single-line "↑ SAVE PRESET"). */
    .anim-preset-io-row {
      display: flex;
      gap: 8px;
      margin-bottom: 8px;
    }
    .anim-preset-io-row .preset-io-btn {
      flex: 1;
      margin-bottom: 0;
      padding: 8px 10px;
      text-align: center;
      font-size: 11px;
      letter-spacing: 0.06em;
      white-space: nowrap;
    }
    /* Preserve-time toggle — styled as a pill switch matching the LOOP /
       YOYO transport toggles, so it visually reads as a real on/off
       affordance instead of a stray native checkbox dropped in the panel.
       The actual switch CSS lives in the shared #t-loop, … selector list
       below; this rule just lays out the label + switch + text. */
    .anim-preset-time-toggle {
      display: flex;
      align-items: center;
      gap: 8px;
      margin: 0 0 10px;
      font-family: var(--sans);
      font-size: 11.5px;
      color: var(--muted);
      cursor: pointer;
      user-select: none;
    }
    .anim-preset-time-toggle:hover { color: var(--text); }
    .anim-preset-time-toggle input[type="checkbox"] { margin: 0; cursor: pointer; }

    /* The dropdown row (text select + Apply button) is what the
       preset-grid replaces in Simple. Hide it in Simple mode; show in
       Advanced. */
    body.editor-mode-simple #entry-preset-section .preset-row,
    body.editor-mode-simple #exit-preset-section  .preset-row,
    body.editor-mode-simple #rollover-preset-section .preset-row { display: none; }

    /* The grid is the inverse — hidden in Advanced, visible in Simple.
       In Simple it's either:
         • a flat 3-column grid (when no categories supplied, e.g. the
           rollover grid)
         • a flex column of <details class="preset-category"> sections,
           each containing its own internal 3-column grid (entry, exit)
       Both modes share a max-height + internal scroll so the panel
       below doesn't have to grow with the section list. */
    .preset-grid { display: none; }
    body.editor-mode-simple .preset-grid {
      display: grid;
      grid-template-columns: repeat(3, minmax(0, 1fr));
      gap: 8px;
      margin-top: 8px;
      margin-bottom: 10px;
      max-height: 520px;
      overflow-y: auto;
      padding-right: 4px;
      /* Promote the scroller to its own composited layer so scrolling
         doesn't force the vars-panel's backdrop-filter to re-rasterize
         per frame — that's the source of the laggy thumb (browser is
         spending most of the frame budget recomputing the panel blur
         instead of repainting the scroll position). transform alone
         is enough on Blink/WebKit; will-change just hints the same. */
      transform: translateZ(0);
      will-change: scroll-position;
    }
    body.editor-mode-simple .preset-grid::-webkit-scrollbar       { width: 8px; }
    body.editor-mode-simple .preset-grid::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.10); border-radius: 4px; }
    body.editor-mode-simple .preset-grid::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.18); }

    /* Categorized layout: container goes flex-column to stack sections;
       each .preset-category-body restores the 3-column card grid. */
    body.editor-mode-simple .preset-grid:has(> .preset-category) {
      display: flex;
      flex-direction: column;
      gap: 4px;
    }
    .preset-category {
      border: 1px solid rgba(255, 255, 255, 0.05);
      border-radius: 8px;
      background: rgba(255, 255, 255, 0.015);
      overflow: hidden;
      /* Keep natural height inside the capped flex-column container —
         without this, flex-shrink: 1 (the default) would proportionally
         compress sections to fit the max-height cap, squishing cards
         instead of letting overflow-y: auto kick in. */
      flex-shrink: 0;
      /* Skip painting + GSAP-tween work for sections currently scrolled
         off-screen. contain-intrinsic-size reserves space so the
         scrollbar still measures correctly even before the section is
         painted. Sized as: row(150) × number of preset-thumb rows. */
      content-visibility: auto;
      contain-intrinsic-size: auto 200px;
    }
    .preset-category-summary {
      display: flex;
      align-items: center;
      gap: 8px;
      padding: 8px 10px;
      cursor: pointer;
      user-select: none;
      list-style: none;
      font-family: var(--mono);
      font-size: 10px;
      font-weight: 600;
      letter-spacing: 0.18em;
      text-transform: uppercase;
      color: var(--muted);
      transition: background 0.12s, color 0.12s;
    }
    .preset-category-summary::-webkit-details-marker { display: none; }
    .preset-category-summary::marker { content: ''; }
    .preset-category-summary:hover {
      background: rgba(245, 200, 66, 0.04);
      color: var(--text);
    }
    .preset-category-chevron {
      display: inline-block;
      font-size: 9px;
      color: var(--muted);
      transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
      transform-origin: 50% 50%;
    }
    .preset-category[open] > .preset-category-summary .preset-category-chevron {
      transform: rotate(90deg);
    }
    /* Animated expand/collapse using max-height + opacity. The body is
       always laid out as a grid (so the columns don't reflow on open)
       and uses max-height: 0 / overflow: hidden when collapsed. No JS
       measurement needed and no HTML wrapper — the only cost is the
       max-height transition itself, which runs for 0.22s and then
       settles. content-visibility:auto on the parent (.preset-category
       above) already skips paint for collapsed sections that are also
       off-screen, so the CPU footprint stays minimal even with many
       categories. The vertical-padding portion of the transition gives
       a soft "settle" feel — without it, collapsed sections still hold
       their 4px+10px gap and the close looks abrupt. */
    .preset-category-body {
      display: grid;
      grid-template-columns: repeat(3, minmax(0, 1fr));
      gap: 8px;
      max-height: 0;
      padding: 0 8px;
      opacity: 0;
      overflow: hidden;
      transition: max-height 0.22s cubic-bezier(0.4, 0, 0.2, 1),
                  padding    0.22s cubic-bezier(0.4, 0, 0.2, 1),
                  opacity    0.16s ease-out;
    }
    .preset-category[open] > .preset-category-body {
      /* Generous cap — preset sections rarely exceed ~600px. Setting it
         too tight clips long sections; too loose has no cost beyond a
         slightly faster-feeling close on short sections. 1400px covers
         every current category (5 rows × ~150px + gaps + padding). */
      max-height: 1400px;
      padding: 4px 8px 10px;
      opacity: 1;
    }
    /* Honor prefers-reduced-motion: skip the height/opacity transition
       so the user gets an instant toggle instead of motion they didn't
       ask for. */
    @media (prefers-reduced-motion: reduce) {
      .preset-category-body,
      .preset-category-chevron { transition: none; }
    }

    /* ── Preset thumbnail card ── */
    .preset-thumb {
      display: flex;
      flex-direction: column;
      align-items: stretch;
      gap: 4px;
      padding: 6px 6px 8px;
      background: rgba(255, 255, 255, 0.025);
      border: 1px solid var(--border);
      border-radius: 8px;
      cursor: pointer;
      transition: all 0.12s;
      min-width: 0;
      overflow: hidden;
      text-align: center;
    }
    .preset-thumb:hover {
      background: rgba(245, 200, 66, 0.06);
      border-color: rgba(245, 200, 66, 0.3);
    }
    /* Two states share the gold-wrap visual:
       • .selected → user has picked but not committed (label flips to "APPLY")
       • .applied  → preset is the layer's CURRENT animation (label = preset name)
       The label transformation is .selected-only. */
    .preset-thumb.selected,
    .preset-thumb.applied {
      border-color: var(--accent);
      background: rgba(245, 200, 66, 0.10);
      box-shadow: 0 0 0 1px rgba(245, 200, 66, 0.5),
                  0 0 12px -2px rgba(245, 200, 66, 0.4);
    }
    .preset-thumb-stage {
      width: 100%;
      aspect-ratio: 16 / 10;
      background: rgba(0, 0, 0, 0.30);
      border-radius: 6px;
      box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.3);
      overflow: hidden;
      position: relative;
    }
    .preset-thumb-stage svg {
      width: 100%;
      height: 100%;
      display: block;
    }
    .preset-thumb-ball {
      fill: var(--accent);
      filter: drop-shadow(0 0 3px rgba(245, 200, 66, 0.6));
    }
    /* Base shape style — the per-card inline style.fill / .stroke from
       _thumbTintFor() overrides the colors here, but the drop-shadow
       glow + stroke-width fall through unchanged. (Inline style beats
       a class rule, but only for properties it explicitly sets.) */
    .preset-thumb-icon {
      fill: #f5c842;
      stroke: rgba(255, 226, 122, 0.9);
      stroke-width: 0.6;
      filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.4))
              drop-shadow(0 0 4px rgba(245, 200, 66, 0.45));
    }
    /* Soft circle — heavier blur so the shape reads as "fuzzy / out of
       focus", visually distinct from a sharp filled circle. */
    .preset-thumb-icon.preset-thumb-soft {
      filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.4))
              drop-shadow(0 0 6px rgba(245, 200, 66, 0.55))
              blur(1.2px);
    }
    /* Hollow ring — thinner glow, no fill (set via inline style.fill='none'). */
    .preset-thumb-icon.preset-thumb-ring {
      filter: drop-shadow(0 0 5px rgba(245, 200, 66, 0.55));
    }
    .preset-thumb-label {
      font-family: var(--sans);
      font-weight: 500;
      font-size: 10px;
      color: var(--text);
      letter-spacing: 0;
      text-transform: none;
      line-height: 1.25;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      transition: all 0.15s;
    }
    /* When the card is selected, the label re-purposes as an "Apply"
       CTA — yellow gradient pill matching the app's primary buttons.
       Clicking the card again (or the label specifically) commits the
       preset; double-click anywhere on the card is a one-step shortcut.
       JS swaps `textContent` to "Apply" alongside .selected. */
    .preset-thumb.selected .preset-thumb-label {
      font-family: var(--mono);
      font-weight: 600;
      font-size: 9.5px;
      letter-spacing: 0.12em;
      text-transform: uppercase;
      color: #2a1d00;
      background: linear-gradient(180deg, #ffd87a, #f5c842);
      border: 1px solid rgba(245, 200, 66, 0.6);
      border-radius: 6px;
      padding: 4px 6px;
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3);
    }
    .preset-thumb.selected:hover .preset-thumb-label {
      background: linear-gradient(180deg, #ffe190, #ffd05a);
    }

    /* ── ag-psd beta toggle + dev inspection panel ──────────────────────
       Surfaces for the client-side ag-psd parser that runs alongside
       the server upload when the beta toggle is on. Reused by the
       collapsible "ag-psd parser" panel under the upload section. */
    .agpsd-beta-toggle {
      margin-top: 12px;
      padding: 8px 10px;
      border: 1px dashed rgba(255, 255, 255, 0.12);
      border-radius: 8px;
      background: rgba(255, 255, 255, 0.02);
    }
    .agpsd-beta-toggle label {
      display: flex;
      align-items: center;
      gap: 8px;
      cursor: pointer;
      user-select: none;
    }
    .agpsd-beta-toggle input[type="checkbox"] { cursor: pointer; }
    .agpsd-beta-text {
      font-family: var(--mono);
      font-size: 11px;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      color: var(--text);
      display: inline-flex;
      align-items: center;
      gap: 6px;
    }
    .agpsd-beta-tag {
      display: inline-block;
      padding: 1px 5px;
      border-radius: 3px;
      background: rgba(245, 200, 66, 0.18);
      color: var(--accent);
      font-weight: 600;
      font-size: 9px;
      letter-spacing: 0.1em;
    }
    .agpsd-beta-hint {
      margin-top: 4px;
      font-size: 10px;
      color: var(--muted);
      letter-spacing: 0.04em;
      padding-left: 22px;
      line-height: 1.4;
    }

    .panel-label-tag {
      display: inline-block;
      padding: 1px 5px;
      border-radius: 3px;
      background: rgba(245, 200, 66, 0.18);
      color: var(--accent);
      font-weight: 600;
      font-size: 9px;
      letter-spacing: 0.1em;
      margin-left: 6px;
    }

    .agpsd-summary {
      background: rgba(255, 255, 255, 0.03);
      border-radius: 6px;
      padding: 6px 10px;
      margin-bottom: 10px;
    }
    .agpsd-summary-row {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 4px 0;
      font-family: var(--mono);
      font-size: 11px;
    }
    .agpsd-summary-row + .agpsd-summary-row {
      border-top: 1px solid rgba(255, 255, 255, 0.04);
    }
    .agpsd-k {
      color: var(--muted);
      letter-spacing: 0.06em;
      text-transform: uppercase;
      font-size: 9.5px;
    }
    .agpsd-v { color: var(--text); }

    .agpsd-text-layers {
      display: flex;
      flex-direction: column;
      gap: 8px;
    }
    .agpsd-text-card {
      padding: 8px 10px;
      background: rgba(255, 255, 255, 0.03);
      border-left: 2px solid var(--accent);
      border-radius: 4px;
    }
    .agpsd-text-name {
      font-family: var(--mono);
      font-size: 9.5px;
      color: var(--muted);
      letter-spacing: 0.06em;
      text-transform: uppercase;
      margin-bottom: 4px;
      word-break: break-word;
    }
    .agpsd-text-content {
      font-size: 13px;
      color: var(--text);
      margin-bottom: 6px;
      word-break: break-word;
      white-space: pre-wrap;
      line-height: 1.35;
    }
    .agpsd-text-meta {
      display: flex;
      flex-wrap: wrap;
      gap: 4px;
    }
    .agpsd-meta-chip {
      font-family: var(--mono);
      font-size: 9.5px;
      padding: 2px 6px;
      border-radius: 3px;
      background: rgba(255, 255, 255, 0.06);
      color: var(--text);
      letter-spacing: 0.03em;
      display: inline-flex;
      align-items: center;
      gap: 4px;
    }
    .agpsd-swatch {
      display: inline-block;
      width: 10px;
      height: 10px;
      border-radius: 2px;
      border: 1px solid rgba(255, 255, 255, 0.2);
    }

    .agpsd-empty {
      font-size: 11px;
      color: var(--muted);
      font-style: italic;
      text-align: center;
      padding: 12px;
    }
    .agpsd-error {
      font-family: var(--mono);
      font-size: 11px;
      color: rgb(255, 120, 120);
      padding: 8px 10px;
      background: rgba(255, 0, 0, 0.05);
      border-left: 2px solid rgb(255, 120, 120);
      border-radius: 4px;
    }
    .agpsd-console-hint {
      margin-top: 10px;
      font-size: 9.5px;
      color: var(--muted);
      letter-spacing: 0.04em;
      text-align: center;
    }
    .agpsd-console-hint code {
      font-family: var(--mono);
      background: rgba(255, 255, 255, 0.05);
      padding: 1px 4px;
      border-radius: 3px;
    }

    /* Apply / Restore controls for the ag-psd dev panel. */
    .agpsd-actions {
      display: flex;
      flex-direction: column;
      gap: 6px;
      margin-top: 12px;
    }
    .agpsd-btn {
      font-family: var(--mono);
      font-size: 11px;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      padding: 8px 10px;
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      background: rgba(255, 255, 255, 0.04);
      color: var(--text);
      cursor: pointer;
      transition: background 0.12s, border-color 0.12s, color 0.12s;
    }
    .agpsd-btn:hover {
      background: rgba(255, 255, 255, 0.08);
      border-color: rgba(255, 255, 255, 0.2);
    }
    .agpsd-btn-primary {
      background: rgba(245, 200, 66, 0.14);
      border-color: rgba(245, 200, 66, 0.45);
      color: var(--accent);
    }
    .agpsd-btn-primary:hover {
      background: rgba(245, 200, 66, 0.22);
      border-color: var(--accent);
      color: rgb(255, 235, 175);
    }
    .agpsd-actions-hint {
      font-size: 10px;
      color: var(--muted);
      letter-spacing: 0.04em;
      line-height: 1.4;
      padding: 4px 2px 0;
    }

    /* Live text editor section — renders below the Apply controls only
       when an ag-psd manifest is currently applied to the editor. */
    .agpsd-live-text-header {
      margin-top: 14px;
      font-family: var(--mono);
      font-size: 11px;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      color: var(--text);
    }
    .agpsd-live-text-hint {
      margin-top: 2px;
      margin-bottom: 8px;
      font-size: 10px;
      color: var(--muted);
      letter-spacing: 0.04em;
      line-height: 1.4;
    }
    .agpsd-live-text-empty {
      margin-top: 12px;
    }
    .agpsd-live-text-cards {
      display: flex;
      flex-direction: column;
      gap: 10px;
    }
    .agpsd-live-text-card {
      padding: 10px;
      background: rgba(255, 255, 255, 0.03);
      border-left: 2px solid var(--accent);
      border-radius: 4px;
      display: flex;
      flex-direction: column;
      gap: 8px;
    }
    .agpsd-live-text-name {
      font-family: var(--mono);
      font-size: 9.5px;
      color: var(--muted);
      letter-spacing: 0.06em;
      text-transform: uppercase;
      word-break: break-word;
    }
    .agpsd-field {
      display: flex;
      flex-direction: column;
      gap: 3px;
    }
    .agpsd-field-row {
      display: flex;
      gap: 6px;
    }
    .agpsd-field-grow { flex: 1; }
    .agpsd-field-label {
      font-family: var(--mono);
      font-size: 9px;
      color: var(--muted);
      letter-spacing: 0.06em;
      text-transform: uppercase;
    }
    .agpsd-input {
      font-family: var(--mono);
      font-size: 11px;
      padding: 5px 7px;
      border-radius: 4px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      background: rgba(0, 0, 0, 0.3);
      color: var(--text);
      width: 100%;
      box-sizing: border-box;
      outline: none;
      transition: border-color 0.12s;
    }
    .agpsd-input:focus {
      border-color: rgba(245, 200, 66, 0.5);
    }
    textarea.agpsd-input {
      resize: vertical;
      line-height: 1.35;
      min-height: 44px;
    }
    input[type="color"].agpsd-input {
      padding: 2px;
      height: 26px;
      width: 44px;
      cursor: pointer;
    }
    input[type="number"].agpsd-input {
      width: 60px;
    }
    .agpsd-live-text-status {
      font-family: var(--mono);
      font-size: 10px;
      letter-spacing: 0.04em;
      min-height: 14px;
    }
    .agpsd-live-text-status-busy { color: var(--muted); }
    .agpsd-live-text-status-ok   { color: rgb(120, 220, 150); }
    .agpsd-live-text-status-err  { color: rgb(255, 120, 120); }

