    :root {
      --bg:         #0c0d0f;
      --surface:    #13151a;
      --card:       #1a1d24;
      --border:     #252830;
      --accent:     #e8a020;
      --accent2:    #10b981;
      --accent3:    #9f7aea;
      --green:      #4ade80;
      --amber:      #fbbf24;
      --red:        #f87171;
      --gray:       #6b7280;
      --text:       #d4d6dc;
      --text-bright:#f1f2f5;
      --muted:      #7a808a;
      --mono:       'Space Mono', monospace;
      --sans:       'Barlow', sans-serif;
    }
    * { box-sizing: border-box; margin: 0; padding: 0; }
    html, body { height: 100%; overflow: hidden; }
    body { background: var(--bg); color: var(--text); font-family: var(--sans); font-size: 14px; -webkit-font-smoothing: antialiased; }

    header {
      background: var(--surface);
      border-bottom: 1px solid var(--border);
      height: 44px;
      display: flex; align-items: center; gap: 8px;
      padding: 0 8px 0 0;
      position: relative; z-index: 10;
      /* Single-row guarantee — pills stay on one line; if the bar gets
         crowded the right edge clips with a scroll instead of wrapping
         over into the canvas. */
      flex-wrap: nowrap;
      overflow-x: auto;
      overflow-y: hidden;
      white-space: nowrap;
    }
    header::-webkit-scrollbar { height: 0; width: 0; }
    /* Logo cluster — matches the LCBE / Settings / Hub topnav pattern.
       24×24 amber logo-mark + Bebas-Neue "LENSCOWBOY · LINK" with
       LENS muted, COWBOY in the ecosystem-wide brand amber #C8881A,
       and the product suffix in the product's own accent (muted
       #10b981 for LINK — a muted yellow-green, tasteful next to LCBE's
       brighter green; distinct from PAL/Hub amber, SPECTACLE cyan,
       Comfy purple). 44px topnav height, 220px-wide nav-logo block with
       right-border separating it from the rest of the header. */
    .nav-logo {
      display: flex; align-items: center; gap: 9px;
      padding: 0 16px;
      width: 220px;
      border-right: 1px solid var(--border);
      height: 100%;
      text-decoration: none;
      flex-shrink: 0;
    }
    .logo-mark {
      width: 24px; height: 24px;
      background: #C8881A;
      border-radius: 3px;
      display: flex; align-items: center; justify-content: center;
      font-family: var(--mono); font-size: 9px; font-weight: 500; color: #000;
      flex-shrink: 0;
    }
    .logo-name {
      font-family: 'Bebas Neue', sans-serif;
      font-size: 15px; letter-spacing: 0.18em;
      color: var(--text); display: inline-flex; align-items: center;
    }
    .logo-name .bm-cowboy { color: #C8881A; }
    .logo-name .bm-sep    { color: var(--muted); margin: 0 6px; }
    .logo-name .bm-prod   { color: var(--accent2); }   /* LINK = muted emerald */
    .header-meta {
      margin-left: auto;
      padding: 0 14px;
      border-left: 1px solid var(--border);
      height: 100%;
      display: flex; align-items: center; gap: 14px;
      font-family: var(--mono); font-size: 10px; color: var(--muted);
    }
    .header-meta a { color: var(--muted); text-decoration: none; }
    .header-meta a:hover { color: var(--text-bright); }
    #hotkeyBtn {
      width: 20px; height: 20px; border-radius: 50%;
      border: 1px solid var(--border); background: transparent;
      color: var(--muted); font-family: var(--mono); font-size: 11px;
      font-weight: 700; cursor: pointer; line-height: 1; padding: 0;
      transition: border-color 0.12s, color 0.12s;
    }
    #hotkeyBtn:hover { border-color: var(--accent2); color: var(--accent2); }

    /* Hotkeys cheat-sheet overlay */
    #hotkey-overlay {
      position: fixed; inset: 0; z-index: 500;
      background: rgba(0,0,0,0.6);
      display: none; align-items: center; justify-content: center;
    }
    #hotkey-overlay.open { display: flex; }
    .hotkey-card {
      width: min(560px, 92vw); max-height: 86vh;
      background: var(--card); border: 1px solid var(--border);
      border-radius: 10px; box-shadow: 0 18px 60px rgba(0,0,0,0.7);
      display: flex; flex-direction: column; overflow: hidden;
    }
    .hotkey-head {
      display: flex; align-items: center; justify-content: space-between;
      padding: 14px 18px; border-bottom: 1px solid var(--border);
      font-family: var(--mono); font-size: 11px; letter-spacing: 0.2em;
      text-transform: uppercase; color: var(--accent2);
    }
    .hotkey-head button {
      background: transparent; border: none; color: var(--muted);
      font-size: 20px; cursor: pointer; line-height: 1;
    }
    .hotkey-head button:hover { color: var(--text-bright); }
    .hotkey-body { padding: 14px 18px; overflow-y: auto; }
    .hotkey-section {
      font-family: var(--mono); font-size: 9px; letter-spacing: 0.18em;
      text-transform: uppercase; color: var(--muted);
      margin: 14px 0 6px; padding-bottom: 4px;
      border-bottom: 1px solid var(--border);
    }
    .hotkey-section:first-child { margin-top: 0; }
    .hotkey-row {
      display: flex; align-items: baseline; gap: 10px;
      padding: 4px 0; font-size: 13px; color: var(--text);
    }
    .hotkey-row span { color: var(--muted); font-size: 12px; }
    .hotkey-row kbd {
      font-family: var(--mono); font-size: 11px;
      background: var(--bg); border: 1px solid var(--border);
      border-radius: 3px; padding: 1px 6px; color: var(--text-bright);
      white-space: nowrap;
    }

    button.run, button.reset {
      border: none; border-radius: 5px;
      /* Tighter padding so pills don't elbow each other off the topbar.
         line-height: 1 + white-space: nowrap together prevent text from
         wrapping to a second row even when a label gets long. */
      padding: 6px 11px;
      line-height: 1;
      white-space: nowrap;
      flex-shrink: 0;
      font-family: var(--mono); font-size: 11px; font-weight: 700;
      letter-spacing: 0.08em; text-transform: uppercase;
      cursor: pointer; transition: opacity 0.15s;
    }
    button.run    { background: var(--accent2); color: #000; }
    button.reset  { background: transparent; color: var(--muted); border: 1px solid var(--border); }
    button.run:hover, button.reset:hover { opacity: 0.85; }
    button.run:disabled { opacity: 0.4; cursor: not-allowed; }

    /* Run graph total-cost chip (build 7). Sits flush right of the
       Run button when at least one vendor node has a credit chip;
       hidden when the graph would cost zero credits. Tints to amber
       on graphs >100 credits as a "heads-up this isn't cheap" cue. */
    .run-cost-chip {
      display: inline-block;
      margin-left: 6px;
      padding: 6px 10px;
      border-radius: 5px;
      background: rgba(16, 185, 129,0.12);
      color: var(--accent2);
      font-family: var(--mono);
      font-size: 11px;
      font-weight: 700;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      line-height: 1;
      white-space: nowrap;
      flex-shrink: 0;
      border: 1px solid rgba(16, 185, 129,0.35);
    }
    .run-cost-chip.heavy {
      background: rgba(245,158,11,0.14);
      color: #f59e0b;
      border-color: rgba(245,158,11,0.45);
    }

    /* Header roll-up menu (⋯ More). Trigger is a normal .reset button; the
       flyout drops below it as a column of the relocated buttons. Buttons keep
       their ids so their JS handlers are untouched — this only moves the DOM. */
    .hdr-menu { position: relative; display: inline-flex; }
    .hdr-menu-trigger.active {
      color: var(--text-bright); border-color: var(--accent2);
    }
    .hdr-menu-flyout {
      display: none;
      /* position: fixed so the header's overflow-y: hidden doesn't clip it.
         JS sets top/left from the trigger button's bounding rect on open. */
      position: fixed; top: 48px; left: 8px;
      flex-direction: column; gap: 4px;
      min-width: 150px; padding: 6px;
      background: var(--card); border: 1px solid var(--border);
      border-radius: 7px; box-shadow: 0 12px 40px rgba(0,0,0,0.5);
      z-index: 60;
    }
    .hdr-menu.open .hdr-menu-flyout { display: flex; }
    .hdr-menu-flyout button.reset {
      width: 100%; text-align: left; padding: 7px 12px;
    }
    .hdr-menu-flyout button.reset:hover {
      opacity: 1; background: rgba(255,255,255,0.05); color: var(--text-bright);
    }
    .hdr-menu-sep { height: 1px; margin: 2px 4px; background: var(--border); }
    .hdr-menu-danger { color: var(--red, #f87171) !important; }
    .hdr-menu-danger:hover {
      background: rgba(248,113,113,0.12) !important; color: #fca5a5 !important;
    }

    .status-pill {
      font-family: var(--mono); font-size: 10px;
      letter-spacing: 0.14em; text-transform: uppercase;
      padding: 5px 11px; border-radius: 11px;
      border: 1px solid var(--border); color: var(--muted);
    }
    .status-pill.idle      { color: var(--muted); }
    .status-pill.running   { color: var(--amber); border-color: var(--amber); }
    .status-pill.succeeded { color: var(--green); border-color: var(--green); }
    .status-pill.failed    { color: var(--red);   border-color: var(--red); }
    .status-pill.cancelled { color: var(--accent3); border-color: var(--accent3); }
    .graph-id { font-family: var(--mono); font-size: 10px; color: var(--muted); margin-left: auto; }
    .meta-row { font-family: var(--mono); font-size: 10px; color: var(--muted); }
    .meta-row code { color: var(--accent2); background: rgba(16, 185, 129,0.10); padding: 1px 5px; border-radius: 3px; }

    /* --drawer-h is updated by JS when the log drawer collapses/expands.
       --right-props-w drives the right-side properties panel width
       (280px expanded → 24px collapsed). --palette-w is the icon-column
       width on the LEFT; the flyout (items list) opens on top of stage
       on click, so the column itself stays narrow. Stage left/right
       anchor to these so the canvas reclaims space when panels collapse. */
    :root {
      --drawer-h: 200px;
      --timeline-h: 34px;   /* bottom timeline strip; JS sets the live height */
      --right-props-w: 280px;
      --palette-w: 56px;
      /* Viewer dimensions — drag-resizable (see .viewer-resize handles).
         --viewer-w = side-dock width, --viewer-h = top-dock height,
         --vf-* = floating-window geometry. */
      --viewer-w: 360px;
      --viewer-h: 320px;
      --vf-top: 88px;
      --vf-left: 120px;
      --vf-w: 520px;
      --vf-h: 380px;
    }
    body.props-collapsed { --right-props-w: 24px; }

    #stage {
      position: absolute;
      top: 44px; left: var(--palette-w); right: var(--right-props-w); bottom: calc(var(--drawer-h) + var(--timeline-h, 34px));
      background: var(--bg);
      transition: bottom 0.22s ease, right 0.22s ease;
    }
    #stage canvas { display: block; }

    #palette {
      position: absolute; top: 44px; left: 0; width: var(--palette-w);
      /* The log drawer never covers the palette column (drawer left edge =
         --palette-w), so the palette must NOT reserve --drawer-h or it leaves
         an empty block below itself when the drawer expands. It only clears
         the bottom timeline strip. */
      bottom: var(--timeline-h, 34px);
      background: var(--surface); border-right: 1px solid var(--border);
      overflow-y: auto; overflow-x: hidden;
      padding: 6px 0;
      transition: bottom 0.22s ease;
    }
    .palette-group-btn {
      width: var(--palette-w);
      height: 48px;
      display: flex; align-items: center; justify-content: center;
      cursor: pointer;
      border-left: 2px solid transparent;
      color: var(--muted);
      font-size: 22px;
      transition: background 0.12s, color 0.12s, border-color 0.12s;
      user-select: none;
    }
    .palette-group-btn:hover {
      background: rgba(16, 185, 129,0.10);
      color: var(--text-bright);
      border-left-color: var(--accent2);
    }
    .palette-group-btn.active {
      background: rgba(16, 185, 129,0.18);
      color: var(--accent2);
      border-left-color: var(--accent2);
    }
    /* Thin divider that sets the Looks action button apart from the
       node-group icons above it. */
    .palette-sep {
      width: 60%; height: 1px; margin: 6px auto;
      background: var(--border);
    }
    /* Looks — an action menu trigger, not a node group. Purple to match the
       cinematic-finish branding; hover/active glow in purple, not green. */
    .palette-looks-btn {
      color: var(--accent3, #a78bfa);
      font-size: 24px;
    }
    .palette-looks-btn:hover {
      background: rgba(167,139,250,0.12);
      color: #c4b5fd;
      border-left-color: var(--accent3, #a78bfa);
    }

    /* Flyout: opens to the right of the palette icon column when an
       icon is clicked. Closes on outside-click, Escape, or drag-start.
       Width matches the old fixed palette width so item rows stay
       readable. */
    #palette-flyout {
      position: absolute;
      top: 44px; left: var(--palette-w);
      width: 220px;
      max-height: calc(100vh - 44px - var(--timeline-h, 34px) - 12px);
      background: var(--card);
      border: 1px solid var(--border);
      border-left: none;
      border-radius: 0 6px 6px 0;
      box-shadow: 6px 0 24px rgba(0,0,0,0.45);
      z-index: 50;
      display: none;
      flex-direction: column;
      overflow: hidden;
    }
    #palette-flyout.open { display: flex; }
    .palette-flyout-head {
      padding: 10px 14px;
      border-bottom: 1px solid var(--border);
      font-family: var(--mono); font-size: 10px;
      letter-spacing: 0.18em; text-transform: uppercase;
      color: var(--accent2);
      background: var(--surface);
      flex-shrink: 0;
    }
    .palette-flyout-list {
      overflow-y: auto;
      padding: 6px 0;
    }
    /* Brand divider inside a vendor flyout (Vendor · Image / Vendor · Video).
       Quieter than the head — slightly muted, smaller, less letter-spaced. */
    .palette-flyout-section {
      padding: 8px 14px 4px;
      font-family: var(--mono); font-size: 9px;
      letter-spacing: 0.12em; text-transform: uppercase;
      color: var(--muted);
      border-top: 1px solid var(--border);
      margin-top: 4px;
    }
    .palette-flyout-section:first-child {
      border-top: 0; margin-top: 0;
    }

    /* Two-level vendor flyout: the vendor NAME is a collapsible parent, its
       models the sublevel. Collapsed by default — clicking a video/image icon
       shows a clean list of vendor names; click one to reveal its models. */
    .palette-flyout-group { border-top: 1px solid var(--border); }
    .palette-flyout-group:first-child { border-top: 0; }
    .palette-flyout-brand {
      display: flex; align-items: center; gap: 8px;
      padding: 8px 12px; cursor: pointer; user-select: none;
      font-family: var(--mono); font-size: 10px;
      letter-spacing: 0.10em; text-transform: uppercase;
      color: var(--muted);
    }
    .palette-flyout-brand:hover { color: var(--text, #cdd2da); background: var(--surface); }
    .palette-brand-caret {
      display: inline-block; font-size: 9px; color: var(--muted);
      transition: transform 0.12s ease;
    }
    .palette-flyout-group.open > .palette-flyout-brand .palette-brand-caret { transform: rotate(90deg); }
    .palette-flyout-group.open > .palette-flyout-brand { color: var(--accent2); }
    .palette-brand-name { flex: 1; }
    .palette-brand-count {
      font-size: 9px; color: var(--muted);
      background: var(--surface); border: 1px solid var(--border);
      border-radius: 8px; padding: 0 6px; line-height: 14px;
    }
    .palette-flyout-brand-items { display: none; padding: 1px 0 4px; }
    .palette-flyout-group.open > .palette-flyout-brand-items { display: block; }

    /* Properties panel — right-side column, always present, collapsible.
       Operator-edits parameters per the NODE_PARAMS schema for the
       focused node's kind/ref. Empty-state message when nothing is
       selected so the panel never disappears entirely. Collapse handle
       is a 24px vertical strip at the LEFT edge of the panel (mirrors
       the bottom drawer's handle pattern, rotated). */
    #properties {
      position: absolute;
      right: 0;
      /* Full-height right rail. The timeline strip stops at the properties'
         left edge (right:--right-props-w), and the log drawer never reaches
         here, so properties owns the bottom-right corner down to 0 — no
         --drawer-h reserve, no empty gap below it. */
      top: 44px; bottom: 0;
      width: var(--right-props-w);
      background: var(--surface);
      border-left: 1px solid var(--border);
      overflow: hidden;
      transition: width 0.22s ease;
      display: flex; flex-direction: row;
      z-index: 4;
    }
    .props-handle {
      width: 24px;
      flex-shrink: 0;
      display: flex; align-items: center; justify-content: center;
      gap: 8px;
      cursor: pointer; user-select: none;
      background: var(--surface);
      border-right: 1px solid var(--border);
      font-family: var(--mono); font-size: 9px;
      color: var(--muted); letter-spacing: 0.2em; text-transform: uppercase;
      writing-mode: vertical-rl;
      transform: rotate(180deg);
    }
    .props-handle:hover { background: var(--card); color: var(--text-bright); }
    .props-handle .props-grip {
      width: 3px; height: 28px; border-radius: 2px;
      background: rgba(16, 185, 129,0.55);
    }
    .props-handle .props-caret {
      font-size: 9px; color: var(--muted);
      transition: transform 0.22s ease;
    }
    body.props-collapsed .props-handle .props-caret { transform: rotate(180deg); }
    .props-content {
      flex: 1; min-width: 0;
      display: flex; flex-direction: column;
    }
    body.props-collapsed .props-content { display: none; }
    .props-head {
      height: 30px;
      border-bottom: 1px solid var(--border);
      display: flex; align-items: center; gap: 8px;
      padding: 0 12px;
      font-family: var(--mono); font-size: 10px;
      color: var(--muted); letter-spacing: 0.16em; text-transform: uppercase;
      flex-shrink: 0;
    }
    .props-title { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    #propsBody {
      flex: 1; min-height: 0;
      overflow-y: auto;
      padding: 12px;
    }
    .prop-field {
      display: flex; flex-direction: column;
      gap: 4px;
      margin-bottom: 11px;
    }
    /* half:true fields render at 50% width so consecutive halves (X/Y pairs)
       sit on one row — keeps the inherited column direction from .prop-field. */
    .prop-half { display: inline-flex; width: 50%; box-sizing: border-box; vertical-align: top; padding-right: 6px; }
    .prop-half .prop-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .prop-third { display: inline-flex; width: 33.333%; box-sizing: border-box; vertical-align: top; padding-right: 6px; }
    .prop-third .prop-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .prop-label {
      font-family: var(--mono); font-size: 9px;
      color: var(--muted); letter-spacing: 0.12em; text-transform: uppercase;
    }
    .prop-input, .prop-select, .prop-textarea {
      background: var(--bg);
      border: 1px solid var(--border);
      border-radius: 3px;
      padding: 6px 8px;
      font-family: var(--mono); font-size: 11px;
      color: var(--text);
      outline: none;
      transition: border-color 0.12s;
    }
    .prop-input:focus, .prop-select:focus, .prop-textarea:focus {
      border-color: var(--accent2);
    }
    .prop-textarea {
      min-height: 64px;
      resize: vertical;
      font-family: var(--sans); font-size: 12px; line-height: 1.45;
    }

    /* ── Checkbox / toggle-switch styling ────────────────────────────
       Renders as a horizontal row: [switch] [label]. The default
       browser checkbox is hidden and replaced with a pill-shaped track
       + sliding knob driven by `appearance: none`. */
    .prop-field-checkbox {
      flex-direction: row !important;
      align-items: center;
      gap: 10px;
    }
    .prop-field-checkbox .prop-label {
      margin: 0;
      flex: 1;
      cursor: pointer;
      user-select: none;
    }
    .prop-switch {
      appearance: none;
      -webkit-appearance: none;
      width: 30px;
      height: 16px;
      background: #2a2e38;
      border: 1px solid var(--border);
      border-radius: 999px;
      position: relative;
      cursor: pointer;
      outline: none;
      transition: background-color 0.15s, border-color 0.15s;
      flex-shrink: 0;
      margin: 0;
    }
    .prop-switch::before {
      content: '';
      position: absolute;
      top: 1px; left: 1px;
      width: 12px; height: 12px;
      background: #cdd2da;
      border-radius: 999px;
      transition: transform 0.15s, background-color 0.15s;
    }
    .prop-switch:checked {
      background: var(--accent2);
      border-color: var(--accent2);
    }
    .prop-switch:checked::before {
      transform: translateX(14px);
      background: #0e1015;
    }
    .prop-switch:hover { border-color: var(--accent2); }
    .prop-switch:focus { box-shadow: 0 0 0 2px rgba(var(--accent2-rgb, 245, 196, 0), 0.25); }
    .prop-button {
      width: 100%;
      padding: 9px 12px;
      background: rgba(16, 185, 129,0.12);
      border: 1px solid var(--accent2);
      border-radius: 3px;
      color: var(--accent2);
      font-family: var(--mono); font-size: 11px;
      letter-spacing: 0.18em; text-transform: uppercase;
      cursor: pointer;
      transition: background 0.12s, color 0.12s;
    }
    .prop-button:hover { background: var(--accent2); color: var(--bg); }
    .prop-mask-preview {
      border: 1px solid var(--border);
      border-radius: 3px;
      background:
        repeating-conic-gradient(#1a1d24 0% 25%, #14171d 0% 50%) 50% / 14px 14px;
      min-height: 90px;
      display: flex; align-items: center; justify-content: center;
      overflow: hidden;
    }
    .prop-mask-preview img {
      max-width: 100%;
      max-height: 180px;
      display: block;
    }
    .prop-mask-empty {
      font-family: var(--mono); font-size: 10px;
      color: var(--muted); letter-spacing: 0.12em;
      padding: 24px 12px;
    }
    .props-empty {
      padding: 18px 12px;
      font-family: var(--mono); font-size: 10px;
      color: var(--muted); line-height: 1.6;
    }
    .palette-group-label {
      font-family: var(--mono); font-size: 9px;
      letter-spacing: 0.18em; text-transform: uppercase;
      color: var(--muted);
      padding: 8px 14px 6px;
    }
    .palette-item {
      display: flex; align-items: center; gap: 10px;
      padding: 9px 14px;
      cursor: grab;
      border-left: 2px solid transparent;
      transition: background 0.15s, border-color 0.15s;
      user-select: none;
    }
    .palette-item:hover { background: rgba(16, 185, 129,0.10); border-left-color: var(--accent2); }
    .palette-item.dragging { opacity: 0.4; }
    /* Plan-gated (studio+) — dimmed, not draggable; the ref label reads "studio+". */
    .palette-item.gated { opacity: 0.4; cursor: not-allowed; }
    .palette-item.gated:hover { background: none; border-left-color: transparent; }
    .palette-item.gated .palette-item-ref { color: var(--amber); }
    .palette-item-swatch {
      width: 10px; height: 10px; border-radius: 2px;
      background: var(--accent2); flex-shrink: 0;
    }
    .palette-item.kind-vendor    .palette-item-swatch { background: var(--accent); }
    .palette-item.kind-link_comp .palette-item-swatch { background: var(--accent2); }
    .palette-item.kind-comfyui   .palette-item-swatch { background: var(--accent3); }
    .palette-item.kind-pal       .palette-item-swatch { background: #f5c400; }  /* PAL amber */
    .palette-item.kind-local     .palette-item-swatch { background: #4ade80; }  /* Local green */
    .palette-item.kind-character .palette-item-swatch { background: #10b981; }  /* LCBE green */
    .palette-item-label {
      font-family: var(--mono); font-size: 11px;
      color: var(--text); letter-spacing: 0.5px;
    }
    .palette-item-ref {
      font-family: var(--mono); font-size: 10px;
      color: var(--muted); margin-left: auto;
    }

    /* Floating ghost element shown while a palette item is being dragged. */
    .drag-ghost {
      position: fixed; pointer-events: none; z-index: 100;
      background: var(--card); border: 2px solid var(--accent2);
      border-radius: 6px;
      padding: 8px 14px;
      font-family: var(--mono); font-size: 11px;
      color: var(--text-bright); letter-spacing: 0.5px;
      box-shadow: 0 4px 16px rgba(0,0,0,0.5);
      transform: translate(-50%, -50%);
    }

    /* Right-click context menu on empty canvas. */
    /* Graphs panel (save / open saved graphs). */
    #graphs-panel {
      position: fixed; top: 50px; left: 12px; z-index: 220;
      width: 324px; max-height: 70vh; display: none; flex-direction: column;
      background: var(--card); border: 1px solid var(--border);
      border-radius: 8px; box-shadow: 0 10px 34px rgba(0,0,0,0.6);
      font-family: var(--mono);
    }
    #graphs-panel.open { display: flex; }
    /* LINK Local panel */
    #local-panel {
      position: fixed; top: 50px; left: 12px; z-index: 221;
      width: 340px; max-height: 76vh; display: none; flex-direction: column;
      background: var(--card); border: 1px solid var(--border);
      border-radius: 8px; box-shadow: 0 10px 34px rgba(0,0,0,0.6);
      font-family: var(--mono);
    }
    #local-panel.open { display: flex; }
    .lp-head {
      padding: 10px 12px; font-size: 10px; letter-spacing: 0.18em;
      text-transform: uppercase; color: var(--muted);
      border-bottom: 1px solid var(--border);
      display: flex; justify-content: space-between; align-items: center;
    }
    .lp-head button { background: none; border: none; color: var(--muted); cursor: pointer; font-size: 13px; }
    .lp-body { overflow-y: auto; padding: 12px; display: flex; flex-direction: column; gap: 10px; }
    .lp-note { color: var(--muted); font-size: 11px; line-height: 1.6; margin: 0; }
    .lp-cmd {
      background: var(--bg); border: 1px solid var(--border); border-radius: 4px;
      color: var(--accent2); font-size: 11px; padding: 8px 10px; margin: 0; overflow-x: auto;
    }
    #local-panel input {
      background: var(--bg); border: 1px solid var(--border); border-radius: 4px;
      color: var(--text); padding: 7px 9px; font-family: var(--mono); font-size: 12px;
    }
    #local-panel input:focus { outline: none; border-color: var(--accent2); }
    #lpConnect, #lpDisconnect {
      background: var(--accent2); color: #000; border: none; border-radius: 4px;
      padding: 7px 14px; font-family: var(--mono); font-size: 11px; font-weight: 700;
      letter-spacing: 0.08em; cursor: pointer;
    }
    #lpDisconnect { background: transparent; color: var(--muted); border: 1px solid var(--border); padding: 3px 9px; }
    .lp-status { font-size: 11px; color: var(--muted); min-height: 14px; }
    .lp-status.err { color: var(--red); }
    .lp-step { font-size: 10px; color: var(--muted); margin: 8px 0 4px; display: flex; align-items: center; gap: 6px; }
    .lp-stepn { display: inline-flex; align-items: center; justify-content: center; width: 15px; height: 15px;
      border-radius: 50%; background: var(--accent2); color: #000; font-size: 9px; font-weight: 700; flex: 0 0 auto; }
    .lp-cmd-row { display: flex; gap: 6px; align-items: stretch; }
    .lp-cmd-row .lp-cmd { flex: 1 1 auto; margin: 0; }
    .lp-copy { flex: 0 0 auto; background: var(--bg); border: 1px solid var(--border); border-radius: 4px;
      color: var(--text); font-family: var(--mono); font-size: 10px; padding: 0 10px; cursor: pointer; white-space: nowrap; }
    .lp-copy:hover { border-color: var(--accent2); color: var(--accent2); }
    .lp-adv { font-size: 10px; }
    .lp-adv summary { color: var(--muted); cursor: pointer; user-select: none; padding: 2px 0; list-style: revert; }
    .lp-adv summary:hover { color: var(--accent2); }
    .lp-adv #lpUrl { width: 100%; box-sizing: border-box; margin-top: 6px; }
    .lp-hint { color: var(--muted); font-size: 10px; line-height: 1.5; margin-top: 4px; }
    .lp-conn { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--text); }
    .lp-conn .lp-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); box-shadow: 0 0 6px var(--green); }
    .lp-conn button { margin-left: auto; }
    .lp-roots { font-size: 10px; color: var(--muted); line-height: 1.5; word-break: break-all; }
    .lp-crumb { font-size: 11px; color: var(--accent2); margin-bottom: 6px; word-break: break-all; }
    .lp-listing { display: flex; flex-direction: column; max-height: 38vh; overflow-y: auto; border: 1px solid var(--border); border-radius: 4px; }
    .lp-entry {
      display: flex; align-items: center; gap: 8px; padding: 6px 10px; cursor: pointer;
      color: var(--text); font-size: 12px; border-bottom: 1px solid rgba(255,255,255,0.03);
    }
    .lp-entry:hover { background: rgba(255,255,255,0.04); }
    .lp-entry .lp-ic { color: var(--muted); width: 14px; text-align: center; }
    .lp-entry.is-dir .lp-ic { color: var(--accent2); }
    .lp-entry.is-seq .lp-ic { color: var(--green); }
    .lp-entry.is-seq .lp-name { color: var(--green); }
    .lp-entry .lp-sz { margin-left: auto; color: var(--muted); font-size: 10px; }
    .gp-head {
      padding: 10px 12px; font-size: 10px; letter-spacing: 0.18em;
      text-transform: uppercase; color: var(--muted);
      border-bottom: 1px solid var(--border);
    }
    .gp-save { display: flex; gap: 6px; padding: 10px 12px; border-bottom: 1px solid var(--border); min-width: 0; }
    .gp-save input {
      flex: 1 1 0; min-width: 0;
      background: var(--bg); border: 1px solid var(--border);
      border-radius: 4px; color: var(--text); padding: 6px 8px;
      font-family: var(--mono); font-size: 12px;
    }
    .gp-save input:focus { outline: none; border-color: var(--accent2); }
    .gp-save button {
      flex: 0 0 auto; white-space: nowrap;
      background: var(--accent2); color: #000; border: none; border-radius: 4px;
      padding: 0 10px; font-family: var(--mono); font-size: 11px; font-weight: 700;
      letter-spacing: 0.05em; cursor: pointer;
    }
    /* Load is the secondary action (outline) so Save reads as primary. */
    .gp-save #gpLoadBtn {
      background: transparent; color: var(--accent2);
      border: 1px solid rgba(16, 185, 129,0.5);
    }
    .gp-save #gpLoadBtn:disabled {
      color: var(--muted); border-color: var(--border); cursor: default; opacity: 0.6;
    }
    .gp-list { overflow-y: auto; padding: 4px 0; }
    .gp-empty { padding: 16px 12px; color: var(--muted); font-size: 11px; text-align: center; }
    .gp-row {
      display: flex; align-items: center; gap: 8px; padding: 7px 12px;
      cursor: pointer; color: var(--text); font-size: 12px;
    }
    .gp-row:hover { background: rgba(16, 185, 129,0.08); }
    .gp-row .gp-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .gp-row .gp-when { color: var(--muted); font-size: 9px; }
    .gp-row .gp-del {
      color: var(--muted); border: 1px solid var(--border); border-radius: 3px;
      padding: 1px 7px; font-size: 11px; cursor: pointer;
    }
    .gp-row .gp-del:hover { color: var(--red); border-color: var(--red); }
    .gp-row.current { border-left: 2px solid var(--accent2); background: rgba(16, 185, 129,0.12); }

    #ctx-menu {
      position: fixed; z-index: 200;
      min-width: 200px;
      background: var(--card); border: 1px solid var(--border);
      border-radius: 5px;
      padding: 4px 0;
      box-shadow: 0 6px 24px rgba(0,0,0,0.55);
      font-family: var(--mono); font-size: 11px;
      display: none;
    }
    #ctx-menu.open { display: block; }
    .ctx-item {
      padding: 7px 14px; cursor: pointer; color: var(--text);
      display: flex; align-items: center; gap: 8px;
      letter-spacing: 0.04em;
    }
    .ctx-item:hover { background: rgba(16, 185, 129,0.11); color: var(--text-bright); }
    .ctx-item.disabled { color: var(--muted); cursor: default; }
    .ctx-item.disabled:hover { background: transparent; }
    .ctx-item .check { width: 12px; display: inline-block; color: var(--accent2); }
    .ctx-sep {
      height: 1px; background: var(--border);
      margin: 4px 0;
    }
    .ctx-group-label {
      font-size: 9px; letter-spacing: 0.18em; text-transform: uppercase;
      color: var(--muted); padding: 7px 14px 3px;
      pointer-events: none;
    }

    /* Bottom log drawer — mirrors the LCBE dashboard drawer pattern.
       Collapsed: only the 26px handle strip is visible (label + grip +
       chevron). Expanded: the log scroller takes up DRAWER_OPEN_H px
       above the handle. The handle stays clickable in both states. */
    /* Timeline dock — sits just above the log drawer; collapses to its
       control strip. Playhead + dope dots ride the scrub track. */
    #timeline {
      position: absolute; left: 0; right: var(--right-props-w, 280px);
      /* Bottom-most strip — the log drawer sits ABOVE it (bottom:var(--timeline-h)).
         --timeline-h is set by JS from the timeline's measured height so the log
         + canvas reserve exactly the right amount of space below them. */
      bottom: 0; z-index: 40;
      background: var(--surface); border-top: 1px solid var(--border);
      border-bottom: 1px solid var(--border);
      font-family: var(--mono, monospace); user-select: none;
    }
    #timeline.collapsed .tl-track { display: none; }
    .tl-controls { display: flex; align-items: center; gap: 6px; padding: 4px 8px; }
    .tl-handle {
      background: none; border: 1px solid var(--border); color: var(--accent2, #10b981);
      font: 10px var(--mono, monospace); letter-spacing: 0.1em; text-transform: uppercase;
      padding: 3px 8px; border-radius: 4px; cursor: pointer;
    }
    .tl-btn {
      /* inline-flex centres glyph+text vertically regardless of glyph
         baseline metrics. ⌫ (U+232B) and ⎘ (U+2398) both sit lower
         than ASCII baseline in most fonts, so a plain inline-block
         button would render the icon below the text. flex + gap also
         gives consistent 4px between glyph and label. (Brad
         2026-06-09: "the cache and curves icones/text looks out of
         vertical alignment".) */
      display: inline-flex; align-items: center; gap: 4px;
      background: none; border: 1px solid var(--border); color: var(--text, #ddd);
      font-size: 11px; padding: 2px 7px; border-radius: 3px; cursor: pointer; min-width: 24px;
      line-height: 1;
    }
    .tl-btn:hover { border-color: var(--accent2, #10b981); color: var(--accent2, #10b981); }
    .tl-btn.playing { color: var(--accent2, #10b981); border-color: var(--accent2, #10b981); }
    /* Autokey armed — amber to match the keyframe indicators (◆), distinct from
       the green transport/play states. */
    .tl-autokey.armed { color: #ffd86b; border-color: #ffd86b; }
    .tl-autokey.armed:hover { color: #ffe48f; border-color: #ffe48f; }
    .tl-num { font-size: 10px; color: var(--muted, #888); display: inline-flex; align-items: center; gap: 3px; }
    .tl-num input {
      width: 46px; background: var(--bg, #14171a); color: var(--text, #ddd);
      border: 1px solid var(--border); border-radius: 3px; font: 11px var(--mono, monospace);
      padding: 2px 4px;
    }
    .tl-track {
      position: relative; height: 40px; margin: 0 8px 6px;
      background: var(--bg, #14171a); border: 1px solid var(--border); border-radius: 3px;
      cursor: pointer; overflow: hidden;
    }
    /* Frame ruler — ticks + numbers along the top of the track. */
    .tl-ruler { position: absolute; inset: 0; pointer-events: none; }
    .tl-tick { position: absolute; top: 0; width: 1px; height: 6px; background: var(--border); }
    /* Minor per-frame tick — shorter, dimmer so the major ticks still read as
       the labelled increments. Only drawn when each frame has ≥ 2.5 px of
       horizontal space (see _drawRuler). */
    .tl-tick-minor { height: 3px; opacity: 0.55; }
    /* First/last-frame markers — taller + accent-coloured so the range
       endpoints jump out, regardless of step size or minor-tick state. */
    .tl-tick-first,
    .tl-tick-last {
      width: 2px; height: 9px;
      background: var(--accent2, #10b981);
      opacity: 0.95;
    }
    .tl-tick-last { transform: translateX(-100%); }    /* anchor to right edge */
    /* In/out labels — always-on text under the major-tick row so the
       operator never has to count from a tick to know the range
       endpoints. Accent-coloured so they read as the "anchors" of the
       timeline, not just one tick among many. */
    .tl-endpoint-label {
      position: absolute; top: 17px;
      font: 9px var(--mono, monospace);
      color: var(--accent2, #10b981);
      opacity: 0.85;
      letter-spacing: 0.05em;
      white-space: nowrap;
      pointer-events: none;
    }
    .tl-endpoint-in  { transform: translateX(2px); }
    .tl-endpoint-out { transform: translateX(calc(-100% - 2px)); }
    .tl-tick-label {
      position: absolute; top: 6px; transform: translateX(-50%);
      font: 9px var(--mono, monospace); color: var(--muted, #888); opacity: 0.9; white-space: nowrap;
    }
    .tl-playhead { position: absolute; top: 0; bottom: 0; width: 2px; background: var(--accent2, #10b981); pointer-events: none; }
    .tl-playhead::after {
      content: ''; position: absolute; top: 0; left: -4px; border-left: 5px solid transparent;
      border-right: 5px solid transparent; border-top: 6px solid var(--accent2, #10b981);
    }
    .tl-cache { position: absolute; inset: 0; pointer-events: none; }   /* EXR proxy cache fill */
    .tl-dope { position: absolute; inset: 0; pointer-events: none; }
    .tl-key {
      /* Centered vertically in the 40px track so the operator can hit it
         cleanly. Was top: 28px (near the bottom edge) which left only ~6px
         of track underneath the key — easy to miss + the track caught the
         click + scrubbed. pointer-events:auto so the parent's
         pointer-events:none doesn't bleed through. z-index keeps the
         diamond above the playhead's stroke. */
      position: absolute; top: 50%; width: 12px; height: 12px;
      background: #ffd86b;
      border: 1px solid #000; transform: translate(-50%, -50%) rotate(45deg);
      cursor: ew-resize;
      pointer-events: auto;
      z-index: 3;
    }
    /* Selected = brighter + larger so a marquee selection in the dope strip
       reads at a glance. JS toggles .sel on the diamonds that are in the
       curves-editor `sel[]` for the current frame. */
    .tl-key.sel {
      background: var(--accent2, #10b981);
      width: 10px; height: 10px;
      box-shadow: 0 0 4px rgba(16, 185, 129, 0.7);
    }
    /* .selected = click-selected in the bottom strip (Delete removes them).
       Different look from .sel above so the two selection sources don't
       collide visually — this one's a white ring around the amber so
       it reads as "armed for deletion, still keyed at the original colour." */
    .tl-key.selected {
      width: 14px; height: 14px;
      box-shadow: 0 0 0 2px #fff, 0 0 6px rgba(255, 255, 255, 0.6);
      z-index: 5;
    }
    /* Shape-aware dimming. When the mask editor has a shape selected, the
       diamonds that BELONG to that shape (its mask:<id>:* channels) get
       the bright accent — other diamonds dim. Operator can scan the dope
       strip and immediately see which keys go with which shape. When no
       shape is selected, neither class is applied → all diamonds at the
       default amber so the strip behaves identically to before. */
    .tl-key.shape-active {
      background: var(--accent2, #10b981);
      box-shadow: 0 0 6px rgba(16, 185, 129, 0.85);
    }
    .tl-key.shape-inactive {
      background: #5c4f1c;        /* desaturated amber so it reads as "exists, not focused" */
      opacity: 0.55;
    }
    /* Key-set beat — when a value is keyed (◆ button, autokey, 'i' shortcut)
       the dope diamond at that frame briefly pulses + glows so the operator
       has visual confirmation the action landed. Class is added by JS for
       ~450 ms then removed; the animation runs once per add. */
    .tl-key.kf-beat {
      animation: kf-beat .45s ease-out;
      box-shadow: 0 0 12px 2px rgba(16, 185, 129, 0.85);
    }
    @keyframes kf-beat {
      0%   { transform: translate(-50%, -50%) rotate(45deg) scale(1); }
      35%  { transform: translate(-50%, -50%) rotate(45deg) scale(2.2); background: #10b981; }
      100% { transform: translate(-50%, -50%) rotate(45deg) scale(1); }
    }
    /* Same beat for the curves editor's gutter ◆ button + the mask modal's
       ◆ Key button. Visual feedback when the operator hits the button —
       it's not just a visited-state colour flick, it's "yes, a key was
       set." Both classes target the same animation so the operator gets
       identical feedback regardless of where they keyed from. */
    .tle-key2.kf-beat,
    .mask-btn.kf-beat {
      animation: kf-beat-btn .45s ease-out;
      color: var(--accent2, #10b981);
      border-color: var(--accent2, #10b981);
    }
    @keyframes kf-beat-btn {
      0%   { transform: scale(1); }
      35%  { transform: scale(1.6); color: var(--accent2, #10b981); }
      100% { transform: scale(1); }
    }
    /* Timeline docked inside the mask modal. The bottom-of-page #timeline
       dock is `position: fixed`; when we re-parent it into the mask shell
       it needs to relax that so it flows inside the mask's flex column.
       The `tl-in-mask` class is added/removed by link-mask.js around the
       re-parent so the original docked styles survive the trip back. */
    #timeline.tl-in-mask {
      position: relative;
      bottom: auto; left: auto; right: auto;
      width: 100%;
      flex-shrink: 0;
      border-top: 1px solid var(--border);
    }
    /* When the timeline lives in the mask shell, the mask shell itself
       grows to accommodate it. Cap the timeline area to a sensible chunk
       of the modal so the canvas above stays usable. */
    .mask-shell > #timeline.tl-in-mask {
      max-height: 50vh;
    }
    /* No spinner steppers on number inputs anywhere on the canvas. */
    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; appearance: textfield; }
    /* ◆ keyframe toggle on number params. Three deliberately distinct states so
       the operator reads animation status on the NODE, not just the timeline
       (Brad 2026-06-15):
         • no keys   → dim grey, recedes
         • ANIMATED  → bold amber + glow, clearly "this channel has keyframes"
         • ON A KEY  → electric cyan, bigger + white-hot glow, clearly "the
                       playhead is parked on a key right here" */
    .prop-key {
      background: none; border: none; color: var(--muted, #888); cursor: pointer;
      font-size: 12px; padding: 0 4px; line-height: 1;
      transition: color .1s ease, text-shadow .1s ease, transform .1s ease;
    }
    .prop-key.on {
      color: #ffb01f;
      font-size: 14px;
      text-shadow: 0 0 7px rgba(255, 176, 31, 0.7);
    }
    /* Playhead is parked EXACTLY on a key for this channel — LINK green, bigger +
       glow, so "on a key" reads instantly differently from merely "animated"
       (Brad 2026-06-15: make the keyed state the link green). After .on so it
       wins the cascade. */
    .prop-key.onkey {
      color: #10b981;
      font-size: 15px;
      text-shadow: 0 0 9px rgba(16, 185, 129, 0.95), 0 0 3px rgba(255,255,255,.6);
      transform: scale(1.14);
    }
    .prop-input.animated { border-color: #ffb01f; }
    /* Colour-wheel key CHIP — a labelled "◆ key" button under each wheel (not a
       bare floating diamond), so it's unmistakably tied to its own wheel. State
       colours mirror .prop-key: grey (no keys) / amber (animated) / green (on a
       key). Rules sit AFTER .prop-key.* so they win the shared cascade. */
    .cw-key {
      display: inline-flex; align-items: center; gap: 5px;
      padding: 2px 10px; border-radius: 6px; line-height: 1;
      background: #0c0d0f; border: 1px solid var(--border, #2a2e38);
      color: var(--muted, #8a909a); cursor: pointer;
      font: 9px var(--mono, monospace); letter-spacing: .1em; text-transform: uppercase;
      transition: color .1s ease, border-color .1s ease, box-shadow .1s ease;
    }
    .cw-key:hover { border-color: #3a4350; color: #cdd2da; }
    .cw-key .cw-key-d { font-size: 12px; }
    .cw-key.on {
      color: #ffb01f; border-color: rgba(255, 176, 31, 0.55);
      text-shadow: 0 0 6px rgba(255, 176, 31, 0.55);
    }
    .cw-key.onkey {
      color: #10b981; border-color: rgba(16, 185, 129, 0.65);
      box-shadow: 0 0 8px rgba(16, 185, 129, 0.4); text-shadow: none; transform: none;
    }
    .prop-slider-num {
      width: 48px; background: var(--bg, #14171a); color: var(--text, #ddd);
      border: 1px solid var(--border); border-radius: 3px; font: 10px var(--mono, monospace);
      padding: 2px 4px; text-align: right;
    }
    .cc-tool {
      background: none; border: 1px solid var(--border); color: var(--muted, #888);
      cursor: pointer; font-size: 12px; line-height: 1; padding: 2px 6px; border-radius: 3px;
    }
    .cc-tool:hover { color: var(--accent2, #10b981); border-color: var(--accent2, #10b981); }
    /* Colour-corrector pop-out modal — larger wheels, resizable. */
    #cc-modal {
      position: fixed; top: 14%; left: 50%; transform: translateX(-50%);
      z-index: 70; display: none; min-width: 520px; min-height: 280px;
      width: 720px; height: 360px; resize: both; overflow: hidden;
      background: var(--surface, #1b1f24); border: 1px solid var(--accent2, #10b981);
      border-radius: 8px; box-shadow: 0 14px 50px rgba(0,0,0,0.6);
      font-family: var(--mono, monospace);
    }
    #cc-modal.open { display: flex; flex-direction: column; }
    .cc-modal-head {
      display: flex; align-items: center; justify-content: space-between;
      padding: 8px 12px; border-bottom: 1px solid var(--border); cursor: move;
      font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent2, #10b981);
    }
    .cc-modal-close { background: none; border: none; color: var(--muted, #888); cursor: pointer; font-size: 14px; }
    .cc-modal-body { flex: 1; display: flex; align-items: center; justify-content: space-around; gap: 20px; padding: 18px; overflow: auto; }
    #log-drawer {
      /* Confined to the graph-viewer (stage) footprint — NOT full width — so it
         rises only under the node canvas, clear of the palette / props / viewer.
         Mirrors #stage's horizontal bounds (incl. the side-docked viewer below). */
      position: absolute; left: var(--palette-w); right: var(--right-props-w); bottom: var(--timeline-h, 34px);
      background: var(--surface); border-top: 1px solid var(--border);
      overflow: hidden;
      transition: height 0.22s ease;
      z-index: 5;
    }
    body.viewer-side #log-drawer { right: calc(var(--right-props-w) + var(--viewer-w)); }
    .drawer-handle {
      height: 26px;
      display: flex; align-items: center; justify-content: center;
      gap: 12px;
      cursor: pointer; user-select: none;
      background: var(--surface);
      font-family: var(--mono); font-size: 9px;
      color: var(--muted); letter-spacing: 0.2em; text-transform: uppercase;
      border-bottom: 1px solid transparent;
    }
    .drawer-handle:hover { background: var(--card); color: var(--text-bright); }
    #log-drawer.expanded .drawer-handle { border-bottom-color: var(--border); }
    .drawer-grip {
      width: 28px; height: 3px; border-radius: 2px;
      background: rgba(16, 185, 129,0.55);
    }
    .drawer-caret {
      font-size: 9px; color: var(--muted);
      transition: transform 0.22s ease;
    }
    #log-drawer.expanded .drawer-caret { transform: rotate(180deg); }
    .drawer-count {
      font-family: var(--mono); font-size: 9px;
      color: var(--muted); letter-spacing: 0.12em;
    }
    .drawer-action {
      background: transparent;
      border: 1px solid var(--border);
      border-radius: 3px;
      color: var(--muted);
      font-family: var(--mono); font-size: 9px;
      letter-spacing: 0.15em; text-transform: uppercase;
      padding: 3px 9px;
      cursor: pointer;
      transition: background 0.12s, color 0.12s, border-color 0.12s;
    }
    .drawer-action:hover {
      border-color: var(--accent2);
      color: var(--accent2);
      background: rgba(16, 185, 129,0.08);
    }
    .drawer-action.flash {
      border-color: var(--accent2);
      color: var(--bg);
      background: var(--accent2);
    }

    #log {
      position: absolute; left: 0; right: 0; top: 26px; bottom: 0;
      padding: 12px 18px; overflow-y: auto;
      font-family: var(--mono); font-size: 11px; line-height: 1.55;
    }

    /* Viewer panel — preview of the focused node's asset. Toggleable
       from the topnav, switchable between SIDE dock (right, 360px wide)
       and TOP dock (top of canvas area, 320px tall). Stays out of the
       way of the palette + drawer; only contends with #stage for room.
       Closed = body has neither viewer-side nor viewer-top class. */
    #viewer {
      position: absolute;
      top: 44px;
      background: var(--surface);
      display: none;
      flex-direction: column;
      z-index: 4;
      overflow: hidden;
    }
    /* Viewer Side dock sits to the LEFT of the properties panel (whose
       right edge anchors to the viewport). Width stays 360px; its
       `right` offset = current properties width so the two panels
       stack horizontally without overlapping. Top dock spans the
       full width above the canvas, also right-anchored to properties. */
    body.viewer-side #viewer {
      display: flex;
      /* Side-docked viewer is its own column to the left of properties; the
         log drawer does not cover it, so it clears only the timeline strip
         (no --drawer-h) — otherwise the drawer expanding leaves a big empty
         block below the viewer. */
      right: var(--right-props-w); width: var(--viewer-w); bottom: var(--timeline-h, 34px);
      border-left: 1px solid var(--border);
    }
    body.viewer-top #viewer {
      display: flex;
      left: var(--palette-w); right: var(--right-props-w); height: var(--viewer-h);
      border-bottom: 1px solid var(--border);
    }
    body.viewer-side #stage    { right: calc(var(--right-props-w) + var(--viewer-w)); }
    body.viewer-top  #stage    { top: calc(44px + var(--viewer-h)); }
    body.viewer-top  #ctx-menu { /* no change — menu floats above */ }

    /* Floating viewer — pops out of the dock into a draggable, resizable
       window over the canvas. Stage is NOT offset (the window floats).
       Geometry lives in --vf-* vars, updated by drag/resize handlers. */
    body.viewer-float #viewer {
      display: flex;
      top: var(--vf-top); left: var(--vf-left);
      width: var(--vf-w); height: var(--vf-h);
      right: auto; bottom: auto;
      border: 1px solid var(--accent2); border-radius: 8px;
      box-shadow: 0 14px 44px rgba(0,0,0,0.6);
      z-index: 20;
    }
    body.viewer-float .viewer-head { cursor: move; }

    /* Resize grips — one per dock orientation, shown by body class.
       side → left edge (ew), top → bottom edge (ns), float → BR corner. */
    .viewer-resize { position: absolute; z-index: 7; display: none; }
    .viewer-resize:hover { background: var(--accent2); opacity: 0.45; }
    body.viewer-side  .viewer-resize.edge-left   { display: block; left: 0; top: 0; bottom: 0; width: 6px; cursor: ew-resize; }
    body.viewer-top   .viewer-resize.edge-bottom { display: block; left: 0; right: 0; bottom: 0; height: 6px; cursor: ns-resize; }
    body.viewer-float .viewer-resize.corner-br   { display: block; right: 0; bottom: 0; width: 16px; height: 16px; cursor: nwse-resize; }

    .viewer-head {
      height: 30px;
      border-bottom: 1px solid var(--border);
      display: flex; align-items: center; gap: 8px;
      padding: 0 12px;
      font-family: var(--mono); font-size: 10px;
      color: var(--muted); letter-spacing: 0.16em; text-transform: uppercase;
      flex-shrink: 0;
    }
    .viewer-title { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .viewer-btn {
      background: transparent; border: 1px solid var(--border); border-radius: 3px;
      padding: 3px 9px; font-family: var(--mono); font-size: 9px;
      color: var(--muted); letter-spacing: 0.1em; cursor: pointer;
      text-transform: uppercase;
    }
    .viewer-btn:hover { color: var(--text-bright); border-color: var(--accent2); }
    .viewer-btn.active { color: var(--accent2); border-color: var(--accent2); }
    .viewer-preview {
      flex: 1; min-height: 0; position: relative;
      display: flex; align-items: center; justify-content: center;
      background: #050505; padding: 8px;
      overflow: hidden;
    }
    .viewer-preview img, .viewer-preview video, .viewer-preview canvas.viewer-gl {
      max-width: 100%; max-height: 100%;
      display: block; object-fit: contain;
      position: relative; z-index: 1;   /* above the canvas-format frame */
      /* Thin neutral outline framing the image edge against the
         canvas-format frame's green outline behind. 50% grey + 0.5 px
         keeps it visible without competing with the image. */
      outline: 0.5px solid rgba(128,128,128,0.5);
      outline-offset: 0;
    }
    /* The GL canvas composites as a TRANSPARENT LAYER over the format frame (its
       empty/overflow regions are alpha 0 so the green canvas + dark bg show through).
       A full-buffer outline would box that transparent extent rather than the image,
       so drop it here — the green format frame is the guide. (Brad 2026-06-17.) */
    .viewer-preview canvas.viewer-gl { outline: none; }
    /* Global canvas — the output-definition frame (WDR §11), drawn behind the
       previewed image. Sized by _renderCanvasFrame() to the globals output
       aspect; the image is boxed into it (letter/pillar-box). */
    #viewerCanvasFrame {
      /* left/top/width/height set by _renderCanvasFrame (centered to coincide
         with the flex-centered image); transform = the live pan/zoom. */
      position: absolute; top: 0; left: 0;
      border: 1px solid rgba(16, 185, 129,0.45);
      background: rgba(16, 185, 129,0.03);
      pointer-events: none; z-index: 0;
    }
    #viewerCanvasFrame::after {
      content: attr(data-fmt);
      position: absolute; bottom: 2px; right: 5px;
      font: 8px var(--mono); color: rgba(16, 185, 129,0.65); letter-spacing: 0.05em;
    }
    .viewer-empty {
      font-family: var(--mono); font-size: 11px;
      color: var(--muted); text-align: center; padding: 24px;
      line-height: 1.7;
    }
    /* Current image resolution, top-left of the preview frame. */
    #viewerRes {
      position: absolute; top: 8px; left: 10px; z-index: 2; pointer-events: none;
      font: 9px var(--mono); color: var(--text); letter-spacing: 0.04em;
      background: rgba(0,0,0,0.5); padding: 2px 6px; border-radius: 3px;
    }
    .viewer-channels {
      display: none; gap: 6px;
      padding: 8px 12px;
      border-top: 1px solid var(--border); background: var(--bg);
      flex-shrink: 0;
    }
    .viewer-channels.visible { display: flex; }
    .viewer-grade {
      display: none; align-items: center; gap: 10px;
      padding: 7px 12px;
      border-top: 1px solid var(--border); background: var(--bg);
      flex-shrink: 0; flex-wrap: wrap;
    }
    .viewer-grade.visible { display: flex; }
    .vg-group { display: flex; align-items: center; gap: 6px; }
    .vg-label {
      font-family: var(--mono); font-size: 9px; letter-spacing: 0.08em;
      color: var(--muted); text-transform: uppercase;
    }
    .vg-group input[type=range] { width: 78px; accent-color: var(--accent2); cursor: pointer; }
    .vg-val {
      font-family: var(--mono); font-size: 9px; color: var(--text);
      min-width: 30px; text-align: right;
    }
    .vg-reset {
      background: transparent; border: 1px solid var(--border); border-radius: 3px;
      color: var(--muted); font-family: var(--mono); font-size: 9px; font-weight: 700;
      width: 18px; height: 18px; line-height: 1; padding: 0; cursor: pointer;
    }
    .vg-reset:hover { color: var(--accent2); border-color: var(--accent2); }
    .viewer-grade select {
      background: var(--bg-2, #111); border: 1px solid var(--border); border-radius: 3px;
      color: var(--text); font-family: var(--mono); font-size: 9px;
      padding: 2px 5px; max-width: 150px;
    }
    .viewer-info {
      background: var(--bg);
      border-top: 1px solid var(--border);
      padding: 10px 12px;
      font-family: var(--mono); font-size: 10px;
      color: var(--text); line-height: 1.55;
      max-height: 180px; overflow-y: auto;
      flex-shrink: 0;
      transition: max-height 0.2s ease, padding 0.2s ease, opacity 0.15s ease;
    }
    /* Alt+V slides the metadata rows away — more room for the image. */
    .viewer-info.collapsed {
      max-height: 0; padding-top: 0; padding-bottom: 0;
      opacity: 0; overflow: hidden; border-top-color: transparent;
    }
    .vi-row { display: flex; gap: 8px; margin-bottom: 3px; }
    .vi-key { color: var(--muted); width: 60px; flex-shrink: 0; }
    .vi-val { color: var(--text); word-break: break-all; }

    /* ── In-node body (always-on controls) ──────────────────────────────
       Each node with params gets a DOM body pinned to its Pixi card and
       scaled with the camera, so the dropdowns/inputs read as part of the
       one node. #node-bodies is the host layer; .node-body is one node's
       controls. Container is pointer-events:none so empty space drags the
       node; the controls inside re-enable pointer events. */
    /* Host matches the #stage box exactly (and follows viewer docking) so
       bodies clip to the canvas area and never bleed over the palette /
       properties / drawer. z-index 2 sits above the Pixi canvas but below
       the chrome panels (which don't overlap this box anyway). */
    #node-bodies {
      position: absolute;
      top: 44px; left: var(--palette-w); right: var(--right-props-w); bottom: calc(var(--drawer-h) + var(--timeline-h, 34px));
      z-index: 2;
      pointer-events: none; overflow: hidden;
    }
    body.viewer-side #node-bodies { right: calc(var(--right-props-w) + var(--viewer-w)); }
    body.viewer-top  #node-bodies { top: calc(44px + var(--viewer-h)); }
    .node-body {
      position: absolute; top: 0; left: 0;
      transform-origin: top left;
      width: 192px;            /* NODE_W(220) − 2*14 padding, unscaled */
      font-family: var(--mono);
      /* Opaque card-coloured bg so when nodes overlap the front body
         (raised via z-index) fully occludes the one behind instead of
         letting controls show through. Matches the Pixi card fill, so a
         lone node still reads as one card. */
      background: var(--card);
      padding-bottom: 4px;
    }
    /* Comfyui-kind body picks the saturated purple to match the Pixi
       bg fill underneath — without this, the HTML overlay paints grey
       over the purple Pixi card and only the border reads purple.
       (Brad 2026-06-10: "Let's make the entire bg color purple not
       just border".) */
    .node-body.kind-comfyui { background: #2a1f3a; }
    /* Only interactive leaf controls capture pointer events; labels and
       empty space stay click-through so the node can still be dragged by
       its body. */
    .node-body, .node-body * { pointer-events: none; }
    .node-body input,
    .node-body select,
    .node-body textarea,
    .node-body button,
    .node-body .cw-disc,
    .node-body .node-body-toggle,
    .node-body .node-body-preview,
    .node-body .node-body-preview * { pointer-events: auto; }
    /* thumbnail node = "just the image". Unlike video/compare previews it has
       NO interactive media, so make the still + its box CLICK-THROUGH: the
       pointer falls to the Pixi card and the WHOLE node drags by its image.
       The image is also un-draggable natively (no stray image-node on drag).
       Only the expand pip stays clickable. Overrides the rule above (later +
       more specific). */
    .node-body .node-body-preview.lc-thumb-body,
    .node-body .node-body-preview.lc-thumb-body img { pointer-events: none; }
    .node-body .node-body-preview.lc-thumb-body img {
      -webkit-user-drag: none; user-select: none; display: block;
      width: 100%; height: auto;
    }
    .node-body .node-body-preview.lc-thumb-body .lc-thumb-expand {
      pointer-events: auto; position: absolute; top: 4px; right: 4px;
      width: 20px; height: 20px; padding: 0; line-height: 18px; text-align: center;
      background: rgba(6,8,12,0.72); color: #cdd3dd;
      border: 1px solid rgba(255,255,255,0.18); border-radius: 4px;
      font: 12px/18px 'Space Mono', monospace; cursor: pointer;
      opacity: 0; transition: opacity 0.12s ease;
    }
    .node-body .node-body-preview.lc-thumb-body:hover .lc-thumb-expand { opacity: 1; }
    .node-body .node-body-preview.lc-thumb-body .lc-thumb-expand:hover {
      background: rgba(12,16,22,0.92); color: #fff;
    }
    .node-body .prop-field { margin-bottom: 6px; }
    .node-body .prop-label { font-size: 8px; color: var(--muted); }
    .node-body .prop-input,
    .node-body .prop-select,
    .node-body .prop-textarea { font-size: 11px; }
    /* Preview roll-out */
    .node-body-toggle {
      display: flex; align-items: center; gap: 6px; cursor: pointer;
      pointer-events: auto;
      font-size: 9px; letter-spacing: 0.12em; text-transform: uppercase;
      color: var(--muted); padding: 3px 0 6px; user-select: none;
    }
    .node-body-toggle:hover { color: var(--accent2); }
    .node-body-toggle .caret { transition: transform 0.15s ease; }
    .node-body-toggle.open .caret { transform: rotate(90deg); }
    .node-body-preview {
      position: relative;
      width: 100%; background: #050505; border: 1px solid var(--border);
      border-radius: 5px; margin-bottom: 8px; overflow: hidden;
      display: flex; align-items: center; justify-content: center;
      min-height: 64px;
    }
    .node-body-preview .dim-badge {
      position: absolute; left: 6px; bottom: 6px;
      background: rgba(0,0,0,0.62); color: #fff;
      font-size: 10px; letter-spacing: 0.04em; padding: 1px 7px;
      border-radius: 3px; pointer-events: none;
    }
    .node-body-preview img, .node-body-preview video {
      display: block; max-width: 100%; max-height: 200px; object-fit: contain;
    }
    .node-body-preview .ph {
      color: var(--muted); font-size: 10px; padding: 18px 8px; text-align: center;
    }
    /* thumbnail node — the still IS the node body: no inner frame/margin, the
       card provides the only border. Image defines the height (up to a cap). */
    .node-body-preview.lc-thumb-body {
      border: none; margin: 0; min-height: 40px; background: transparent;
    }
    .node-body-preview.lc-thumb-body img {
      width: 100%; max-height: 300px; object-fit: contain;
    }

    /* Quick-create overlay — Tab to open. Comfy-style: search box on
       top, filtered NODE_CATALOGUE entries underneath. Arrow keys
       navigate, Enter creates at the last known canvas cursor pos. */
    #quick-create {
      position: fixed; top: 22%; left: 50%; transform: translateX(-50%);
      width: 420px; max-height: 56vh;
      background: var(--card); border: 1px solid var(--border);
      border-radius: 8px;
      box-shadow: 0 12px 40px rgba(0,0,0,0.6);
      z-index: 300;
      display: none;
      flex-direction: column;
      overflow: hidden;
    }
    #quick-create.open { display: flex; }
    #qc-search {
      width: 100%;
      background: transparent;
      border: none; border-bottom: 1px solid var(--border);
      padding: 14px 18px;
      font-family: var(--mono); font-size: 13px;
      color: var(--text-bright); letter-spacing: 0.04em;
      outline: none;
    }
    #qc-search::placeholder { color: var(--muted); }
    #qc-list { overflow-y: auto; }
    .qc-item {
      display: flex; align-items: center; gap: 10px;
      padding: 8px 18px;
      cursor: pointer;
      border-left: 2px solid transparent;
      font-family: var(--mono); font-size: 11px;
      color: var(--text);
    }
    .qc-item.focused, .qc-item:hover {
      background: rgba(16, 185, 129,0.10);
      border-left-color: var(--accent2);
      color: var(--text-bright);
    }
    .qc-swatch { width: 9px; height: 9px; border-radius: 2px; flex-shrink: 0; }
    .qc-swatch.kind-vendor    { background: var(--accent); }
    .qc-swatch.kind-link_comp { background: var(--accent2); }
    .qc-swatch.kind-comfyui   { background: var(--accent3); }
    .qc-label { flex: 1; }
    .qc-ref { color: var(--muted); font-size: 10px; }
    .qc-empty {
      padding: 18px; text-align: center;
      font-family: var(--mono); font-size: 11px;
      color: var(--muted);
    }
    .qc-hint {
      padding: 9px 18px;
      border-top: 1px solid var(--border);
      font-family: var(--mono); font-size: 9px;
      color: var(--muted); letter-spacing: 0.12em;
      display: flex; justify-content: space-between;
    }
    /* ─── Roto editor modal ──────────────────────────────────────
       Full-screen overlay with a paintable HTML canvas, brush size
       slider, brush/erase toggle, clear + save. Opened from the
       properties panel button on link_comp:mask nodes. Save writes
       the canvas PNG dataURL back onto node.parameters.mask_dataurl. */
    #mask-editor {
      /* No dark backdrop, no centred lock-in — the shell floats over the
         canvas + timeline, leaving everything else interactive. Operators
         can scrub the timeline while the editor is open and watch the
         keyframed shapes re-pose live in the modal preview (the editor
         already subscribes to LinkTL.onFrame; only the blocking overlay
         was in the way). Top-aligned + bottom-padded shell so the
         timeline strip at the bottom of the page stays uncovered. */
      position: fixed; inset: 0;
      z-index: 400;
      display: none;
      align-items: flex-start; justify-content: center;
      padding-top: 6vh;
      pointer-events: none;
    }
    #mask-editor.open { display: flex; }
    .mask-shell {
      /* position: relative so the absolutely-positioned ⤢ Maximise button
         (top: 6px / right: 8px) anchors to THIS shell, not bubble up to
         viewport and land off-screen. Without this the button rendered
         outside the modal on first load — invisible until the operator
         dragged the modal (.pinned set position: absolute, which gave
         the button an anchor). */
      position: relative;
      /* Open at ~80% of the viewport (Brad 2026-06-25 — the old 1200px cap made
         it tiny on big displays). Operator can still drag the SE corner to
         resize, or Maximise to fill the screen. */
      width: 80vw;
      height: 80vh;
      /* Safety ceiling that still leaves the page-bottom timeline strip clear. */
      max-height: calc(100vh - 110px);
      background: var(--card);
      border: 1px solid var(--border);
      border-radius: 8px;
      box-shadow: 0 18px 60px rgba(0,0,0,0.85);
      display: flex; flex-direction: column;
      overflow: hidden;
      pointer-events: auto;   /* shell catches its own clicks; rest is click-through */
      resize: both;            /* drag the SE corner to resize */
      min-width: 720px;
      min-height: 420px;
    }
    /* Once the operator drags the shell, JS flips it to position: absolute
       with inline left/top so the flex-centering on #mask-editor stops
       fighting the move. The pinned class is the marker for that state. */
    .mask-shell.pinned {
      position: absolute;
      max-height: none;        /* user controls the size via resize: both */
    }
    /* Maximize: take the whole viewport. Clears any pinned left/top from
       a prior drag so the maximised state is always full-screen. Inline
       width/height get blown away by the !important here. The fixed
       positioning + 100vw/100vh override .pinned without removing it,
       so toggling maximise off returns to whatever drag-position the
       operator had before. */
    .mask-shell.maximised {
      position: fixed !important;
      top: 0 !important; left: 0 !important;
      right: 0 !important; bottom: 0 !important;
      width: 100vw !important; height: 100vh !important;
      max-width: none !important; max-height: none !important;
      border-radius: 0;          /* edge-to-edge — no rounded corners at the viewport edges */
      resize: none;               /* no resize corner when maximised */
    }
    /* Maximise button — top-right corner of the shell, above everything
       (including the header). Visible at a glance: full accent colour,
       chunky enough to spot from anywhere on the modal, with a hover
       lift. Sits in the corner so it never clashes with the toolbar. */
    .mask-max-btn {
      position: absolute;
      top: 6px; right: 8px;
      z-index: 10;
      width: 32px; height: 28px;
      padding: 0;
      background: rgba(16, 185, 129, 0.15);
      border: 1.5px solid var(--accent2, #10b981);
      border-radius: 4px;
      color: var(--accent2, #10b981);
      font-family: var(--mono);
      font-size: 18px; line-height: 22px;
      font-weight: bold;
      cursor: pointer;
      transition: color 0.12s, border-color 0.12s, background 0.12s, transform 0.12s;
      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
    }
    .mask-max-btn:hover {
      background: var(--accent2, #10b981);
      color: var(--bg, #0a0c0f);
      transform: scale(1.08);
    }
    /* When the timeline lives inside the mask, the canvas page no longer
       needs to reserve --timeline-h at the bottom — the timeline isn't
       there. Set the var to 0 on the body so the canvas + log drawer use
       the full viewport height instead of leaving a fat empty strip. */
    body.tl-detached {
      --timeline-h: 0px !important;
    }
    .mask-head {
      padding: 9px 12px;
      border-bottom: 1px solid var(--border);
      display: flex; align-items: center; gap: 6px 12px;
      flex-wrap: wrap;
      cursor: move;            /* whole header is the drag handle */
      user-select: none;
    }
    /* Interactive controls inside the header (buttons, inputs, sliders) keep
       their default cursor so they don't read as drag handles. */
    .mask-head button, .mask-head input, .mask-head select, .mask-head label {
      cursor: default;
    }
    .mask-head button { cursor: pointer; }
    /* Group separators between tool clusters so a wrapped, denser bar still
       reads cleanly as the tool count grows. */
    .mask-tool + .mask-tool { border-left: 1px solid var(--border); padding-left: 12px; }
    .mask-title {
      font-family: var(--mono); font-size: 11px;
      letter-spacing: 0.2em; text-transform: uppercase;
      color: var(--accent2);
      width: 100%;
    }
    .mask-tool {
      display: flex; align-items: center; gap: 5px;
      font-family: var(--mono); font-size: 10px;
      color: var(--muted);
    }
    .mask-tool input[type="range"] {
      width: 74px;
      accent-color: var(--accent2);
    }
    .mask-btn {
      padding: 5px 8px;
      background: transparent;
      border: 1px solid var(--border);
      border-radius: 3px;
      color: var(--text);
      font-family: var(--mono); font-size: 10px;
      letter-spacing: 0.06em; text-transform: uppercase;
      cursor: pointer;
      transition: background 0.12s, color 0.12s, border-color 0.12s;
    }
    .mask-btn:hover { border-color: var(--accent2); color: var(--accent2); }
    .mask-btn.active {
      background: rgba(16, 185, 129,0.15);
      border-color: var(--accent2);
      color: var(--accent2);
    }
    .mask-btn.primary {
      background: var(--accent2);
      color: var(--bg);
      border-color: var(--accent2);
    }
    .mask-btn.primary:hover { background: var(--accent2); color: var(--bg); opacity: 0.9; }
    .mask-canvas-wrap {
      flex: 1; min-height: 0;
      background:
        repeating-conic-gradient(#161920 0% 25%, #11141a 0% 50%) 50% / 24px 24px;
      /* zoom/pan: the canvas is positioned absolutely inside a clipped wrap;
         JS sets transform = translate(panX, panY) scale(zoom). Centering at
         rest is handled by setting panX/panY in the Fit / 1:1 handlers. */
      position: relative;
      overflow: hidden;
    }
    .mask-canvas-wrap canvas {
      cursor: crosshair;
      border: 1px solid var(--border);
      box-shadow: 0 0 0 1px rgba(16, 185, 129,0.18);
      background: transparent;
      display: block;
      position: absolute;
      left: 0; top: 0;
      transform-origin: 0 0;
      will-change: transform;
    }
    .mask-canvas-wrap.panning canvas { cursor: grabbing; }
    .mask-foot {
      padding: 10px 18px;
      border-top: 1px solid var(--border);
      display: flex; align-items: center; gap: 12px;
      font-family: var(--mono); font-size: 10px;
      color: var(--muted); letter-spacing: 0.12em;
    }
    .mask-hint { flex: 1; }

    /* Roto body = canvas + layers side panel */
    .mask-body { flex: 1; min-height: 0; display: flex; }
    .mask-btn.sm { padding: 2px 8px; font-size: 12px; letter-spacing: 0; line-height: 1; }
    .mask-tool input[type="color"] {
      width: 26px; height: 22px; padding: 0; border: 1px solid var(--border);
      border-radius: 3px; background: transparent; cursor: pointer;
    }
    .mask-layers {
      width: 212px; flex: 0 0 212px; border-left: 1px solid var(--border);
      display: flex; flex-direction: column; background: var(--bg-2, #14171a);
      min-height: 0;
    }
    .mask-layers-head {
      display: flex; align-items: center; justify-content: space-between;
      padding: 8px 10px; border-bottom: 1px solid var(--border);
      font-family: var(--mono); font-size: 9px; letter-spacing: 0.15em;
      text-transform: uppercase; color: var(--muted);
    }
    .mask-layers-list { flex: 1; overflow-y: auto; min-height: 0; }
    .mask-layer {
      display: flex; align-items: center; gap: 6px; padding: 5px 8px;
      border-bottom: 1px solid rgba(255,255,255,0.05); cursor: pointer;
      font-family: var(--mono); font-size: 10px; color: var(--text-2, #bbb);
    }
    .mask-layer.active { background: rgba(16, 185, 129,0.12); }
    .mask-layer .eye { width: 13px; flex: 0 0 13px; cursor: pointer; opacity: 0.85; text-align: center; }
    .mask-layer .thumb {
      width: 26px; height: 18px; flex: 0 0 26px; border: 1px solid var(--border);
      background:
        repeating-conic-gradient(#161920 0% 25%, #11141a 0% 50%) 50% / 8px 8px;
      object-fit: contain; image-rendering: pixelated;
    }
    .mask-layer .nm { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .mask-layer .nm input {
      width: 100%; background: var(--bg, #0c0d0f); color: var(--text, #ddd);
      border: 1px solid var(--accent2); border-radius: 2px; font: 10px var(--mono); padding: 1px 3px;
    }
    .mask-layer .lbtn { cursor: pointer; opacity: 0.55; padding: 0 2px; flex: 0 0 auto; }
    .mask-layer .lbtn:hover { opacity: 1; color: var(--accent2); }
    .mask-layer .del:hover { color: var(--red-t, #ef4444); }
    .mask-layer-opacity {
      padding: 8px 10px; border-top: 1px solid var(--border);
      display: flex; align-items: center; gap: 6px;
      font-family: var(--mono); font-size: 9px; color: var(--muted);
    }
    .mask-layer-opacity input[type="range"] { flex: 1; accent-color: var(--accent2); }

    /* --demo-banner-h is retained at 0px so the legacy top-offset calcs
       below resolve to a no-op (demo-mode is never applied now). */
    :root { --demo-banner-h: 0px; }

    /* Sign-in gate — full-screen blocking overlay shown when no session
       JWT is present. LINK is no longer publicly runnable. */
    /* PAL viewport embed — slide-over iframe over the whole canvas. */
    #pal-embed {
      position: fixed; inset: 24px; z-index: 9000; display: none;
      flex-direction: column;
      background: var(--bg); border: 1px solid var(--accent);
      border-radius: 10px; overflow: hidden;
      box-shadow: 0 24px 80px rgba(0,0,0,0.7);
    }
    #pal-embed.open { display: flex; }
    .pal-embed-head {
      height: 38px; flex-shrink: 0;
      display: flex; align-items: center; gap: 12px; padding: 0 12px;
      background: var(--surface); border-bottom: 1px solid var(--border);
      font-family: var(--mono); font-size: 11px; color: var(--muted);
      letter-spacing: 0.08em;
    }
    .pal-embed-head .pal-embed-title { flex: 1; }
    .pal-embed-close {
      background: transparent; border: 1px solid var(--border); border-radius: 4px;
      color: var(--text); padding: 4px 12px; cursor: pointer;
      font-family: var(--mono); font-size: 11px;
    }
    .pal-embed-close:hover { border-color: var(--accent); color: var(--accent); }
    #palEmbedFrame { flex: 1; width: 100%; border: 0; background: #000; }

    #auth-gate {
      position: fixed; inset: 0; z-index: 9999;
      display: none;
      align-items: center; justify-content: center;
      background: rgba(8,9,11,0.82);
      backdrop-filter: blur(6px);
    }
    #auth-gate.visible { display: flex; }
    .auth-gate-card {
      width: min(440px, 90vw);
      background: var(--card); border: 1px solid var(--border);
      border-radius: 12px; padding: 30px 30px 26px;
      box-shadow: 0 20px 60px rgba(0,0,0,0.6);
      text-align: center;
    }
    .auth-gate-card .pill {
      display: inline-block; padding: 3px 11px; margin-bottom: 14px;
      background: rgba(16, 185, 129,0.14); color: var(--accent2);
      border: 1px solid rgba(16, 185, 129,0.5); border-radius: 4px;
      font-family: var(--mono); font-size: 10px;
      letter-spacing: 0.28em; text-transform: uppercase;
    }
    .auth-gate-card h2 {
      margin: 0 0 10px; font-size: 19px; font-weight: 600;
      color: var(--text-bright); letter-spacing: 0.01em;
    }
    .auth-gate-card p {
      margin: 0 0 22px; font-size: 13px; line-height: 1.6;
      color: var(--muted);
    }
    .auth-gate-btn {
      display: inline-block; padding: 10px 26px;
      background: var(--accent2); color: #000; text-decoration: none;
      border-radius: 6px; font-family: var(--mono); font-size: 12px;
      letter-spacing: 0.1em; text-transform: uppercase; font-weight: 600;
    }
    .auth-gate-btn:hover { filter: brightness(1.08); }
    /* Shift everything that anchors to top:44px down by --demo-banner-h.
       Stage, palette, viewer, properties all use top: 44px today; the
       banner is 38px tall when visible. Drawer + ctx-menu are bottom-
       anchored and unaffected. */
    body.demo-mode #stage,
    body.demo-mode #palette,
    body.demo-mode #properties,
    body.demo-mode #palette-flyout,
    body.demo-mode #viewer {
      top: calc(44px + var(--demo-banner-h));
    }
    body.demo-mode.viewer-top #stage {
      top: calc(44px + var(--viewer-h) + var(--demo-banner-h));
    }
    /* Float window stays free-positioned even with the demo banner up —
       the grouped demo-mode top-offset above must not pin it to the banner. */
    body.demo-mode.viewer-float #viewer { top: var(--vf-top); }

    .log-line { padding: 1px 0; color: var(--text); }
    .log-line.kind-queued     { color: var(--gray); }
    .log-line.kind-cache_miss { color: var(--muted); }
    .log-line.kind-cold_start { color: var(--accent3); }
    .log-line.kind-running    { color: var(--amber); }
    .log-line.kind-cache_hit  { color: var(--accent2); }
    .log-line.kind-succeeded  { color: var(--green); }
    .log-line.kind-failed     { color: var(--red); }
    .log-line.kind-blocked    { color: var(--gray); }
    .log-line.kind-cancelled  { color: var(--accent3); }
    .log-line.kind-retrying   { color: var(--accent); }
    .log-line.kind-stream_complete { color: var(--accent2); font-weight: 700; }
    .log-line.kind-meta       { color: var(--muted); font-style: italic; }
    .log-time { color: var(--muted); margin-right: 8px; }
    .log-node { color: var(--text-bright); margin-right: 8px; }
