feat(web-ui): style global search dropdown

Adds .search-wrap / .search-results / .search-result styles for the
new topbar command-palette dropdown. Per-kind pill colors distinguish
asset / project / recorder / job / user / nav results at a glance.

/* ========== Projects ========== */
.projects-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 14px;
}
.project-card {
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  overflow: hidden;
  cursor: pointer;
  transition: transform 120ms, border 80ms;
}
.project-card:hover {
  transform: translateY(-2px);
  border-color: var(--border-stronger);
}
.project-thumb-grid {
  display: grid;
  grid-template-columns: 2fr 1fr;
  grid-template-rows: 1fr 1fr;
  gap: 2px;
  aspect-ratio: 16 / 9;
  background: var(--bg-2);
}
.project-thumb-grid .project-thumb-cell:first-child { grid-row: span 2; }
.project-thumb-cell { position: relative; overflow: hidden; background: var(--bg-2); }
.project-thumb-cell .thumb-svg { width: 100%; height: 100%; }
.project-card-body { padding: 12px 14px; }
.project-meta {
  display: flex; gap: 6px;
  font-size: 11.5px; color: var(--text-3);
  margin-top: 4px;
}
.project-bar {
  margin-top: 10px;
  display: flex;
  height: 4px;
  border-radius: 99px;
  overflow: hidden;
  background: var(--bg-3);
  gap: 1px;
}

/* ========== Upload ========== */
.field-label {
  font-size: 10.5px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-4);
  display: block;
  margin-bottom: 6px;
}
.select-faux, .field-input {
  height: 34px;
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  padding: 0 10px;
  display: flex; align-items: center; gap: 8px;
  color: var(--text-1);
  font-size: 12.5px;
  width: 100%;
  cursor: pointer;
}
.field-input { font-size: 12.5px; outline: 0; }
.field-input:focus { border-color: var(--accent); background: var(--bg-2); }
.field-input.select { justify-content: space-between; }

.dropzone {
  background: var(--bg-1);
  border: 1.5px dashed var(--border-stronger);
  border-radius: var(--r-lg);
  padding: 40px 24px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  transition: background 120ms, border 120ms;
}
.dropzone:hover { background: var(--bg-2); border-color: var(--accent); }
.dropzone-formats { display: flex; gap: 6px; margin-top: 8px; flex-wrap: wrap; justify-content: center; }
.upload-row {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 14px;
  border-bottom: 1px solid var(--border);
}
.upload-row:last-child { border-bottom: 0; }

/* ========== Recorders ========== */
.recorders-list { display: flex; flex-direction: column; gap: 10px; }
.recorder-row {
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: 12px;
  display: grid;
  grid-template-columns: 220px 1fr 380px auto;
  align-items: center;
  gap: 16px;
  transition: border 80ms;
}
.recorder-row:hover { border-color: var(--border-stronger); }
.recorder-row.recording { border-color: rgba(255,59,48,0.25); }
.recorder-row.error { border-color: rgba(255,91,91,0.25); }
.recorder-preview {
  height: 56px;
  border-radius: var(--r-sm);
  background: var(--bg-2);
  overflow: hidden;
  position: relative;
}
.recorder-empty {
  height: 100%;
  display: grid; place-items: center;
  color: var(--text-3);
}
.recorder-audio-prev {
  height: 100%;
  display: flex; align-items: center; gap: 10px;
  padding: 0 12px;
}
.recorder-audio-prev .waveform { flex: 1; height: 80%; }
.recorder-info { min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.recorder-sub {
  font-size: 11.5px;
  color: var(--text-3);
  display: flex; gap: 6px;
}
.recorder-sub.mono { font-family: var(--font-mono); font-size: 11px; }
.recorder-stats {
  display: grid;
  grid-template-columns: 90px 90px 1fr;
  gap: 12px;
}
.recorder-stat .stat-label {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 600;
  color: var(--text-4);
  margin-bottom: 3px;
}
.recorder-stat .stat-val { font-size: 12.5px; }
.recorder-actions { display: flex; align-items: center; gap: 4px; }
.rec-dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: currentColor;
}

/* ========== Capture / DeckLink ========== */
.decklink-card {
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  overflow: hidden;
}
.decklink-head {
  padding: 14px 16px;
  display: flex; justify-content: space-between; align-items: center;
  border-bottom: 1px solid var(--border);
}
.decklink-body {
  padding: 24px;
  display: grid;
  grid-template-columns: 60px 1fr;
  gap: 20px;
  align-items: stretch;
  background:
    radial-gradient(ellipse at top, rgba(91,124,250,0.05), transparent 60%),
    var(--bg-0);
}
.decklink-card-face {
  background: linear-gradient(180deg, #1c1f28, #0d0e13);
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  display: flex; flex-direction: column;
  justify-content: space-between;
  padding: 12px 6px;
  position: relative;
}
.decklink-label {
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  font-family: var(--font-mono);
  font-size: 8.5px;
  letter-spacing: 0.12em;
  color: var(--text-3);
  text-align: center;
  margin: 0 auto;
}
.decklink-leds {
  display: flex; gap: 6px;
  justify-content: center;
}
.decklink-led {
  width: 6px; height: 6px;
  border-radius: 50%;
  background: rgba(255,255,255,0.1);
}
.decklink-led.on {
  background: var(--success);
  box-shadow: 0 0 6px var(--success);
}
.bnc-ports { display: flex; flex-direction: column; gap: 10px; }
.bnc-port {
  display: grid;
  grid-template-columns: 32px 1fr auto;
  align-items: center;
  gap: 12px;
  padding: 10px;
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  text-align: left;
  cursor: pointer;
  position: relative;
  transition: background 80ms, border 80ms;
}
.bnc-port:hover { border-color: var(--border-stronger); }
.bnc-port.active { background: var(--bg-3); }
.bnc-port.active.live::before {
  content: "";
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 2px;
  background: var(--success);
}
.bnc-port.recording::before { background: var(--live); }
.bnc-port.active { box-shadow: inset 0 0 0 1px var(--accent); border-color: var(--accent); }
.bnc-connector {
  width: 32px; height: 32px;
  border-radius: 50%;
  background: radial-gradient(circle, #1a1d24, #0a0c11);
  display: grid; place-items: center;
  border: 1.5px solid var(--border-stronger);
  position: relative;
}
.bnc-pin {
  width: 6px; height: 6px;
  background: linear-gradient(135deg, #6b7280, #2a2f3a);
  border-radius: 50%;
}
.bnc-ring {
  position: absolute;
  inset: 4px;
  border: 1px dashed rgba(255,255,255,0.08);
  border-radius: 50%;
}
.bnc-port.live .bnc-pin { background: linear-gradient(135deg, #FFC857, #C8862D); box-shadow: 0 0 8px rgba(255,200,87,0.4); }
.bnc-port.recording .bnc-pin { background: linear-gradient(135deg, #FF5C5C, #C8362D); box-shadow: 0 0 8px rgba(255,92,92,0.5); }
.bnc-info { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.bnc-num { font-size: 12.5px; font-weight: 600; }
.bnc-label { font-size: 11px; color: var(--text-2); }
.bnc-sig { font-size: 10.5px; color: var(--text-3); }
.bnc-port.live .bnc-sig { color: var(--success); }
.bnc-rec {
  width: 10px; height: 10px;
  border-radius: 50%;
  background: var(--live);
  animation: pulse 1.2s ease-in-out infinite;
}
.bnc-signal-bar {
  grid-column: 2 / 4;
  height: 3px;
  background: var(--bg-3);
  border-radius: 99px;
  overflow: hidden;
  margin-top: 4px;
}
.bnc-signal-fill {
  height: 100%;
  background: linear-gradient(90deg, var(--success), var(--warning), var(--danger));
  background-size: 200% 100%;
  background-position: 0% 0;
  transition: width 200ms;
}
.decklink-foot {
  padding: 10px 16px;
  border-top: 1px solid var(--border);
  display: flex; gap: 20px;
  font-size: 11.5px;
  color: var(--text-3);
}
.decklink-foot strong { color: var(--text-1); font-weight: 600; }

.capture-detail {
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: 16px;
}
.capture-detail.empty {
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  padding: 80px 24px;
  color: var(--text-3);
}
.capture-preview {
  position: relative;
  aspect-ratio: 16 / 9;
  border-radius: var(--r-md);
  overflow: hidden;
  background: #000;
}
.capture-overlay-meters {
  position: absolute;
  right: 8px; top: 8px;
  display: flex; gap: 4px;
}
.capture-tc {
  position: absolute;
  bottom: 8px; right: 8px;
  background: rgba(0,0,0,0.7);
  padding: 3px 8px;
  border-radius: 3px;
  font-family: var(--font-mono);
  font-size: 11px;
  color: white;
}
.capture-stats {
  margin-top: 16px;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}
.capture-stat {
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  padding: 8px 10px;
}
.capture-stat-label {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 600;
  color: var(--text-4);
}
.capture-stat-value { font-size: 13px; margin-top: 2px; }

/* ========== Monitors ========== */
.monitors-grid {
  display: grid;
  gap: 10px;
  background: var(--bg-2);
  border-radius: var(--r-lg);
  padding: 10px;
}
.monitor-tile {
  position: relative;
  aspect-ratio: 16 / 9;
  background: #000;
  border-radius: var(--r-sm);
  overflow: hidden;
  border: 1px solid var(--border);
  display: flex; flex-direction: column;
  cursor: pointer;
}
.monitor-tile.audio { background: linear-gradient(135deg, hsl(180 30% 12%), hsl(200 25% 6%)); }
.monitor-tile-label {
  position: absolute;
  bottom: 0; left: 0; right: 0;
  padding: 8px 10px;
  background: linear-gradient(180deg, transparent, rgba(0,0,0,0.8));
  display: flex; align-items: center; gap: 6px;
}
.monitor-tile-label .name {
  color: white;
  font-size: 11.5px;
  font-weight: 500;
}
.monitor-tile-label .time {
  margin-left: auto;
  color: white;
  font-family: var(--font-mono);
  font-size: 10.5px;
}

/* ========== Jobs ========== */
.jobs-stats {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 12px;
}
.job-row {
  display: grid;
  grid-template-columns: 20px 110px 1fr 90px 200px 70px 80px 90px;
  align-items: center;
  gap: 12px;
  padding: 10px 16px;
  border-bottom: 1px solid var(--border);
  font-size: 12.5px;
}
.job-row:last-child { border-bottom: 0; }
.job-row.head {
  font-size: 10.5px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-4);
}
.job-progress-wrap {
  display: flex; align-items: center; gap: 8px;
}
.job-progress-bar {
  flex: 1;
  height: 5px;
  background: var(--bg-3);
  border-radius: 99px;
  overflow: hidden;
  position: relative;
}
.job-progress-fill {
  height: 100%;
  background: linear-gradient(90deg, var(--accent), #7C9EFF);
  background-size: 200% 100%;
  animation: shimmer 2s linear infinite;
  transition: width 300ms;
}

/* ========== Editor ========== */
.editor-shell {
  display: flex; flex-direction: column;
  height: 100%;
  background: var(--bg-0);
}
.editor-topbar {
  height: 44px;
  border-bottom: 1px solid var(--border);
  padding: 0 14px;
  display: flex; align-items: center; gap: 10px;
}
.editor-body {
  flex: 1;
  display: grid;
  grid-template-columns: 240px 1fr 240px;
  min-height: 0;
}
.editor-bins, .editor-insp { background: var(--bg-1); border-right: 1px solid var(--border); overflow-y: auto; }
.editor-insp { border-right: 0; border-left: 1px solid var(--border); }
.editor-bin-item {
  display: flex; align-items: center; gap: 8px;
  padding: 6px;
  border-radius: var(--r-sm);
  cursor: pointer;
}
.editor-bin-item:hover { background: var(--hover); }
.editor-bin-thumb { width: 48px; aspect-ratio: 16/9; border-radius: 3px; overflow: hidden; background: var(--bg-2); flex-shrink: 0; }
.editor-viewer { background: #000; display: flex; flex-direction: column; }
.editor-canvas {
  flex: 1;
  position: relative;
  background: #050608;
  display: grid; place-items: center;
  overflow: hidden;
  max-height: 50vh;
}
.editor-canvas .thumb-svg { width: 100%; height: 100%; }
.editor-transport {
  height: 44px;
  display: flex; align-items: center; gap: 8px;
  padding: 0 14px;
  background: var(--bg-1);
  border-top: 1px solid var(--border);
}
.editor-timeline {
  background: var(--bg-0);
  border-top: 1px solid var(--border);
  height: 240px;
  display: flex; flex-direction: column;
  position: relative;
  overflow: hidden;
}
.editor-timeline-head {
  height: 32px;
  display: flex; align-items: center;
  padding: 0 12px;
  gap: 8px;
  border-bottom: 1px solid var(--border);
}
.timeline-ruler {
  height: 22px;
  display: flex;
  padding-left: 40px;
  border-bottom: 1px solid var(--border);
  background: var(--bg-1);
}
.ruler-tick {
  flex: 1;
  border-left: 1px solid var(--border);
  font-size: 9.5px;
  color: var(--text-4);
  padding: 4px 4px 0;
}
.timeline-track {
  display: grid;
  grid-template-columns: 40px 1fr;
  align-items: stretch;
  height: 42px;
  border-bottom: 1px solid var(--border);
}
.timeline-track-label {
  background: var(--bg-1);
  display: grid; place-items: center;
  font-size: 10.5px;
  font-family: var(--font-mono);
  font-weight: 600;
  color: var(--text-3);
  border-right: 1px solid var(--border);
}
.timeline-track-lane {
  position: relative;
  background:
    repeating-linear-gradient(90deg, transparent 0 49px, rgba(255,255,255,0.02) 49px 50px);
}
.clip {
  position: absolute;
  top: 4px; bottom: 4px;
  border-radius: 3px;
  overflow: hidden;
  display: flex; align-items: center;
  cursor: pointer;
  border: 1px solid rgba(0,0,0,0.3);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.15);
}
.clip-label {
  position: absolute;
  left: 6px; top: 3px;
  font-size: 10px;
  font-weight: 600;
  color: white;
  z-index: 2;
  text-shadow: 0 1px 1px rgba(0,0,0,0.4);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.clip-film { display: flex; flex: 1; height: 100%; }
.clip-film-cell { flex: 1; overflow: hidden; }
.clip-film-cell .thumb-svg { width: 100%; height: 100%; }
.clip.audio { background: var(--success) !important; }
.clip-wave { width: 100%; height: 100%; padding: 4px; opacity: 0.6; }
.clip-wave .waveform { width: 100%; height: 100%; }
.timeline-playhead {
  position: absolute;
  top: 32px; bottom: 0;
  width: 1.5px;
  background: var(--live);
  pointer-events: none;
  z-index: 5;
}
.timeline-playhead::before {
  content: "";
  position: absolute;
  top: -4px; left: 50%;
  width: 10px; height: 10px;
  background: var(--live);
  transform: translateX(-50%) rotate(45deg);
}

/* ========== Admin tables ========== */
/* ── Row popover menu (Users, etc.) ────────────────────────────────── */
.row-menu {
  position: absolute;
  right: 0;
  top: calc(100% + 4px);
  z-index: 20;
  min-width: 180px;
  background: var(--bg-1);
  border: 1px solid var(--border-stronger);
  border-radius: 6px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);
  padding: 4px;
  display: flex;
  flex-direction: column;
}
.row-menu button {
  display: flex;
  align-items: center;
  gap: 8px;
  background: transparent;
  border: 0;
  color: var(--text-1);
  font-size: 12.5px;
  padding: 7px 10px;
  border-radius: 4px;
  cursor: pointer;
  text-align: left;
}
.row-menu button:hover { background: var(--bg-3); }
.row-menu button.danger:hover { background: var(--danger-soft); color: var(--danger); }

.user-row, .token-row, .container-row, .schedule-row {
  display: grid;
  align-items: center;
  gap: 12px;
  padding: 12px 16px;
  border-bottom: 1px solid var(--border);
  font-size: 12.5px;
}
.user-row { grid-template-columns: 1.5fr 100px 1.5fr 120px 40px; }
.token-row { grid-template-columns: 1.4fr 1.4fr 110px 110px 100px 40px; }
.container-row { grid-template-columns: 1.4fr 1.4fr 140px 140px 100px 1.4fr 110px; }
.schedule-row { grid-template-columns: 1.6fr 1.2fr 1.2fr 90px 110px 110px 150px; }
.user-row.head, .token-row.head, .container-row.head, .schedule-row.head {
  font-size: 10.5px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-4);
}
.user-row:last-child, .token-row:last-child, .container-row:last-child, .schedule-row:last-child { border-bottom: 0; }
.token-row.revoked { opacity: 0.5; }

/* ========== Cluster ========== */
.cluster-canvas {
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  overflow: hidden;
}

/* ========== Settings ========== */
.settings-nav {
  display: flex; flex-direction: column; gap: 2px;
  position: sticky; top: 0;
}
.settings-nav-item {
  display: flex; align-items: center; gap: 8px;
  padding: 0 10px;
  height: 32px;
  border-radius: var(--r-sm);
  color: var(--text-2);
  font-size: 12.5px;
  cursor: pointer;
}
.settings-nav-item:hover { background: var(--hover); color: var(--text-1); }
.settings-nav-item.active { background: var(--accent-soft); color: var(--accent-text); }
.settings-card {
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  overflow: hidden;
}
.settings-card-head {
  padding: 16px;
  display: flex; align-items: flex-start; gap: 12px;
  border-bottom: 1px solid var(--border);
}
.settings-card-icon {
  width: 36px; height: 36px;
  border-radius: var(--r-sm);
  background: var(--accent-soft);
  color: var(--accent);
  display: grid; place-items: center;
  flex-shrink: 0;
}
.settings-card-body { padding: 16px; display: flex; flex-direction: column; gap: 12px; }
.field { display: flex; flex-direction: column; gap: 4px; }
.field-input-wrap { display: flex; gap: 6px; align-items: center; }
.checkbox-row {
  display: flex; align-items: center; gap: 8px;
  font-size: 12.5px; color: var(--text-1);
  cursor: pointer;
}
.checkbox-row input { accent-color: var(--accent); }

/* ========== Tokens parody ========== */
.token-hero {
  display: grid;
  grid-template-columns: 2fr 1fr;
  gap: 14px;
  margin-bottom: 16px;
}
.token-burn-card, .token-actual-card {
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: 18px 20px;
  position: relative;
  overflow: hidden;
}
.token-burn-card {
  background:
    radial-gradient(ellipse at top right, rgba(255,91,91,0.12), transparent 60%),
    var(--bg-1);
  border-color: rgba(255,91,91,0.18);
}
.token-actual-card {
  background:
    radial-gradient(ellipse at bottom left, rgba(45,212,168,0.10), transparent 60%),
    var(--bg-1);
  border-color: rgba(45,212,168,0.18);
}
.token-card-label {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-weight: 700;
  color: var(--text-4);
  margin-bottom: 8px;
}
.token-counter {
  display: flex; align-items: baseline; gap: 12px;
}
.token-flame { font-size: 32px; line-height: 1; }
.token-big {
  font-size: 48px;
  font-weight: 700;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
  background: linear-gradient(180deg, #FF9D6B, #FF5B5B);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  line-height: 1.05;
}
.token-rate {
  font-size: 12px;
  display: flex; align-items: baseline;
  font-family: var(--font-mono);
}
.token-actual-amount {
  display: flex; align-items: baseline; gap: 0;
  color: var(--success);
  margin-bottom: 8px;
}

.token-comparison {
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: 0 0 12px;
  margin-bottom: 16px;
  overflow: hidden;
}
.token-compare-chart { padding: 0 16px; }
.token-compare-legend {
  display: flex; gap: 24px;
  padding: 8px 0 0;
  font-size: 12px;
  color: var(--text-2);
}
.token-compare-legend .dot {
  display: inline-block;
  width: 10px; height: 10px;
  border-radius: 50%;
  margin-right: 6px;
  vertical-align: middle;
}

.token-grid {
  display: grid;
  grid-template-columns: 1fr 1.4fr;
  gap: 14px;
}
.token-event {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 14px;
  border-bottom: 1px solid var(--border);
}
.token-event:last-child { border-bottom: 0; }
.token-event.fresh { animation: tokenFresh 800ms ease; }
@keyframes tokenFresh {
  0% { background: rgba(255,91,91,0.15); transform: translateX(-4px); }
  100% { background: transparent; transform: none; }
}
.token-tiers {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
}
.token-tier {
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: 16px 14px;
  display: flex; flex-direction: column;
  gap: 6px;
  position: relative;
}
.token-tier.popular {
  border-color: var(--accent);
  background: linear-gradient(180deg, var(--accent-soft) 0%, var(--bg-1) 40%);
  box-shadow: 0 0 24px rgba(91,124,250,0.10);
}
.token-tier-badge {
  position: absolute;
  top: -8px; right: 12px;
  background: var(--accent);
  color: white;
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.06em;
  padding: 3px 8px;
  border-radius: 99px;
}
.token-tier-name { font-size: 13px; font-weight: 700; letter-spacing: -0.01em; }
.token-tier-desc { font-size: 11px; color: var(--text-3); min-height: 32px; line-height: 1.4; }
.token-tier-price { margin-top: 6px; display: flex; align-items: baseline; }
.token-tier-tokens { font-size: 11px; color: var(--text-3); padding-top: 4px; border-top: 1px solid var(--border); }

.token-footnote {
  margin-top: 20px;
  padding: 14px 16px;
  background: var(--warning-soft);
  border: 1px solid rgba(245,166,35,0.2);
  border-radius: var(--r-md);
  display: flex; gap: 12px;
  font-size: 12.5px;
  color: var(--text-2);
  line-height: 1.55;
}
.token-footnote svg { color: var(--warning); flex-shrink: 0; margin-top: 2px; }
.token-footnote strong { color: var(--warning); }

/* ========== Global Search ========== */
.search-wrap {
  position: relative;
  flex-shrink: 0;
}
.search-wrap .search {
  width: 280px;
}
.search-wrap .search.is-open {
  border-color: var(--accent);
  background: var(--bg-2);
}
.search-results {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  width: 440px;
  max-width: 90vw;
  max-height: 460px;
  overflow-y: auto;
  background: var(--bg-1);
  border: 1px solid var(--border-stronger);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-pop);
  padding: 4px;
  z-index: 60;
}
.search-empty {
  padding: 18px 12px;
  text-align: center;
  font-size: 12px;
  color: var(--text-3);
}
.search-result {
  display: grid;
  grid-template-columns: 24px 1fr auto;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-radius: var(--r-sm);
  cursor: pointer;
  color: var(--text-1);
}
.search-result.active {
  background: var(--accent-soft);
}
.search-result-icon {
  width: 24px; height: 24px;
  border-radius: 5px;
  background: var(--bg-3);
  color: var(--text-2);
  display: grid; place-items: center;
  flex-shrink: 0;
}
.search-result.active .search-result-icon {
  background: var(--accent-soft-2);
  color: var(--accent-text);
}
.search-result-text {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.search-result-label {
  font-size: 12.5px;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.search-result-sub {
  font-size: 11px;
  color: var(--text-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.search-result-kind {
  font-family: var(--font-mono);
  font-size: 9.5px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-4);
  background: var(--bg-2);
  border: 1px solid var(--border);
  padding: 2px 6px;
  border-radius: 99px;
  flex-shrink: 0;
}
.search-result-kind.kind-asset { color: #B4C3FF; border-color: rgba(91,124,250,0.25); }
.search-result-kind.kind-project { color: #FFD89B; border-color: rgba(245,166,35,0.25); }
.search-result-kind.kind-recorder { color: #FFAFAF; border-color: rgba(255,91,91,0.25); }
.search-result-kind.kind-job { color: #9EE7D2; border-color: rgba(45,212,168,0.25); }
.search-result-kind.kind-user { color: #D5B8FF; border-color: rgba(181,124,250,0.25); }
.search-result-kind.kind-nav { color: var(--text-2); }


OK | commit:899876c | Updated services/web-ui/public/styles-rest.css


fix(web-ui): hide search wrapper (with dropdown) on narrow screens

Previously the responsive rule hid only .search, leaving the dropdown
positioned on its own wrapper. Target .search-wrap so input + results
both hide together.

/* responsive + polish fixes */
.page-header h1 { white-space: nowrap; }
.page-header .subtitle {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
  flex-shrink: 1;
}
.page-header { gap: 12px; flex-wrap: wrap; }
.status-pip span { white-space: nowrap; }
.status-pip { white-space: nowrap; }
.rail-item { min-width: 0; }
.rail-item > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rail-item > span.rail-count, .rail-item > .rail-color-dot { overflow: visible; flex-shrink: 0; }

@media (max-width: 1100px) {
  .stat-row { grid-template-columns: repeat(2, 1fr); }
  .jobs-stats { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 1280px) {
  .recorder-row {
    grid-template-columns: 180px 1fr;
    grid-template-rows: auto auto;
  }
  .recorder-stats { grid-column: 2 / 3; grid-row: 2; }
  .recorder-actions { grid-column: 1 / 3; grid-row: 3; justify-content: flex-end; padding-top: 4px; border-top: 1px solid var(--border); }
}

.capture-stat-label, .recorder-stat .stat-label, .stat-card .label, .stat-card .delta {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.topbar > * { flex-shrink: 0; }
.topbar .crumb { min-width: 0; overflow: hidden; }
.topbar .crumb > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.topbar .spacer { flex: 1; min-width: 8px; }

@media (max-width: 1100px) {
  .topbar .search-wrap { display: none; }
}
@media (max-width: 900px) {
  .topbar .status-pip span:not(.dot) { display: none; }
}

.library-toolbar { flex-wrap: wrap; }
.library-toolbar .search { width: 200px; }

@media (max-width: 1280px) {
  .page-body > div[style*="grid-template-columns: 440px"] {
    grid-template-columns: 1fr !important;
  }
}

.audio-meter.v {
  border-radius: 99px;
  background: rgba(255,255,255,0.04);
  padding: 3px;
}

.recorder-preview { min-height: 56px; }

.activity-text .target { word-break: break-word; }

.asset-card .meta .sub { overflow: hidden; }

.stat-card .label, .stat-card .value, .stat-card .delta { position: relative; z-index: 1; }
.stat-card .spark { z-index: 0; }


please finish the search fixes - disregard the right click task.


Wire up search bar and add right-click context menu


OK | commit:630dc75 | Updated services/web-ui/public/styles-fixes.css


TodoWrite

Explore screens to understand searchable data shapes

Exploring screens to understand searchable data shapes

Wire up topbar search bar with results dropdown + ⌘K shortcut

Wiring up topbar search bar with results dropdown + ⌘K shortcut

Add CSS styles for search results dropdown

Adding CSS styles for search results dropdown

Commit changes to forgejo


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable

Explore screens to understand searchable data shapes

Exploring screens to understand searchable data shapes

Wire up topbar search bar with results dropdown + ⌘K shortcut

Wiring up topbar search bar with results dropdown + ⌘K shortcut

Add CSS styles for search results dropdown

Adding CSS styles for search results dropdown

Commit changes to forgejo

Commit changes to forgejo


Search is wired up and pushed to `main`. Four commits on `zgaetano/dragonflight`:

- `45c0e0f` — `shell.jsx`: new `GlobalSearch` component (assets / projects / recorders / jobs / users / nav), `⌘K` / `Ctrl K` focus, arrow keys + Enter + Esc, click handlers route through `onOpenAsset` / `onOpenProject` / `onNavigate`.
- `61d02d5` — `app.jsx`: pass `onOpenAsset` and `onOpenProject` into `Topbar`.
- `899876c` — `styles-rest.css`: dropdown + per-kind result pills.
- `630dc75` — `styles-fixes.css`: responsive rule now hides `.search-wrap` so the dropdown follows the input.

I didn't touch the right-click work — that's still queued for a future session whenever you want it. Hard refresh to get past the browser cache on `app.jsx` / `shell.jsx`.


right click menu looks awful , aslo the search bar at the top of the screen still appears to be broken.


Let me investigate. Need to see what's actually live and what the right-click thing is.


Let me check both the live site and the existing right-click code in parallel.


Let me check the live page in the browser to see exactly what's broken and what they mean by right-click menu.


Error: Error executing tool navigate: net::ERR_NAME_NOT_RESOLVED at https://app.wilddragon.net/


No files found


📄 onboard-node.sh (9101B)
📄 test-api.sh (9729B)
📄 test-cluster.sh (7134B)


File: docker-compose.yml

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    ports:
      - "${PORT_DB:-5432}:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./services/mam-api/src/db/schema.sql:/docker-entrypoint-initdb.d/001-schema.sql:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - wild-dragon

  queue:
    image: redis:7-alpine
    ports:
      - "${PORT_REDIS:-6379}:6379"
    volumes:
      - redis_data:/data
    networks:
      - wild-dragon

  mam-api:
    build: ./services/mam-api
    depends_on:
      db:
        condition: service_healthy
      queue:
        condition: service_started
    ports:
      - "${PORT_MAM_API:-7432}:3000"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /mnt/NVME/MAM/wild-dragon-live:/live
      - /mnt/NVME/MAM/wild-dragon-growing:/growing
      - /mnt/NVME/MAM/sdk:/sdk
      - /dev/shm:/dev/shm
      - /run/dbus:/run/dbus
      - /run/systemd:/run/systemd
      - /usr/bin/nvidia-smi:/usr/bin/nvidia-smi:ro
    environment:
      DATABASE_URL: ${DATABASE_URL}
      REDIS_URL: ${REDIS_URL}
      S3_ENDPOINT: ${S3_ENDPOINT}
      S3_BUCKET: ${S3_BUCKET}
      S3_ACCESS_KEY: ${S3_ACCESS_KEY}
      S3_SECRET_KEY: ${S3_SECRET_KEY}
      S3_REGION: ${S3_REGION:-us-east-1}
      SESSION_SECRET: ${SESSION_SECRET}
      AUTH_ENABLED: ${AUTH_ENABLED:-false}
      DOCKER_NETWORK: wild-dragon_wild-dragon
      NODE_IP: ${NODE_IP}
      NODE_HOSTNAME: ${NODE_HOSTNAME:-}
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]
    networks:
      - wild-dragon

  capture:
    build: ./services/capture
    depends_on:
      - mam-api
    ports:
      - "${PORT_CAPTURE:-7433}:3001"
      - "${PORT_RTMP:-1935}:1935"         # RTMP ingest (listener mode)
      - "${PORT_SRT:-9000}:9000/udp"      # SRT ingest (listener mode)
    privileged: true
    environment:
      S3_ENDPOINT: ${S3_ENDPOINT}
      S3_BUCKET: ${S3_BUCKET}
      S3_ACCESS_KEY: ${S3_ACCESS_KEY}
      S3_SECRET_KEY: ${S3_SECRET_KEY}
      S3_REGION: ${S3_REGION:-us-east-1}
      MAM_API_URL: ${MAM_API_URL:-http://mam-api:3000}
    volumes:
      - /mnt/NVME/MAM/wild-dragon-live:/live
      - /dev/shm:/dev/shm
      - /run/dbus:/run/dbus
      - /run/systemd:/run/systemd
    networks:
      - wild-dragon

  worker:
    build: ./services/worker
    depends_on:
      - queue
      - db
    environment:
      REDIS_URL: ${REDIS_URL}
      DATABASE_URL: ${DATABASE_URL}
      S3_ENDPOINT: ${S3_ENDPOINT}
      S3_BUCKET: ${S3_BUCKET}
      S3_ACCESS_KEY: ${S3_ACCESS_KEY}
      S3_SECRET_KEY: ${S3_SECRET_KEY}
      S3_REGION: ${S3_REGION:-us-east-1}
      GROWING_PATH: /growing
    volumes:
      - /mnt/NVME/MAM/wild-dragon-growing:/growing
    networks:
      - wild-dragon

  web-ui:
    build: ./services/web-ui
    ports:
      - "${PORT_WEB_UI:-7434}:80"
    volumes:
      - /mnt/NVME/MAM/wild-dragon-live:/live
      - /dev/shm:/dev/shm
      - /run/dbus:/run/dbus
      - /run/systemd:/run/systemd
    networks:
      - wild-dragon

  editor:
    build: ./services/editor
    depends_on:
      - mam-api
    ports:
      - "${PORT_EDITOR:-7435}:80"
    networks:
      - wild-dragon

volumes:
  postgres_data:
  redis_data:

networks:
  wild-dragon:
    driver: bridge


Commit changes to forgejo


File: services/web-ui/Dockerfile

# Stage 1: build CSS bundle
FROM node:20-alpine AS css-build
WORKDIR /build

# Copy only the files needed to install deps (better cache layering)
COPY package.json package-lock.json* ./
RUN npm install --no-audit --no-fund

# Copy source CSS + tailwind config + every HTML file (tailwind scans HTML to determine which utilities to emit)
COPY tailwind.config.js postcss.config.js ./
COPY src/ ./src/
COPY public/ ./public/

# Build into public/dist/app.css
RUN npx postcss ./src/css/app.css -o ./public/dist/app.css --env production

# Stage 2: runtime
FROM nginx:alpine
COPY --from=css-build /build/public/ /usr/share/nginx/html/
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80


Agent

Find context menu in dragonflight

Prompt:
Search the forgejo repo zgaetano/dragonflight (use mcp__claude_ai_mcp-server__forgejo_forgejo_get_file and forgejo_list_directory) for any code that implements a right-click / context-menu UI feature. Specifically search the files under services/web-ui/public/ for:

1. The string "onContextMenu" (React handler)
2. The string "oncontextmenu"
3. The string "contextmenu" (event name)
4. The string "context-menu" (CSS class)
5. The string "right-click" or "rightclick"

For each match, report:
- Which file
- A few lines of surrounding context (what's being right-clicked, what menu is shown)

Files to check (all under services/web-ui/public/):
- shell.jsx (current main branch)
- app.jsx
- screens-library.jsx
- screens-projects.jsx
- screens-ingest.jsx
- screens-jobs.jsx
- screens-editor.jsx
- screens-admin.jsx
- screens-asset.jsx
- screens-home.jsx
- tweaks-panel.jsx
- modal-new-recorder.jsx
- visuals.jsx
- icons.jsx
- data.jsx
- styles.css
- styles-rest.css
- styles-fixes.css
- styles-screens.css
- styles-asset.css
- styles-modal.css

Use forgejo_get_file with owner="zgaetano" and repo="dragonflight" for each. Be efficient — you may grep through the content as you fetch. Report ALL findings, even if "nothing found". Keep report under 200 lines.


All files now searched. The screens-admin.jsx was searched programmatically and yielded no matches for any context-menu patterns. I have enough findings to report.

## Summary of findings

The right-click / context-menu feature is implemented exclusively for **assets in the Library view**. One file contains all the logic; one CSS file contains its styling. No other file under `services/web-ui/public/` mentions context menus.

---

### 1. `services/web-ui/public/screens-library.jsx` — main implementation

Found **3 distinct `onContextMenu` handlers** and **1 listener for `contextmenu` events**, all targeting `AssetCard` / list rows. Drives a custom in-app menu component `AssetContextMenu`.

State and dismiss-on-outside-click setup (in `Library`):
```js
const [ctxMenu, setCtxMenu] = React.useState(null); // { asset, x, y }
...
React.useEffect(() => {
  if (!ctxMenu) return;
  const close = () => setCtxMenu(null);
  window.addEventListener('click', close);
  window.addEventListener('contextmenu', close);
  window.addEventListener('scroll', close, true);
  return () => { ... };
}, [ctxMenu]);

const openCtx = (asset, e) => {
  e.preventDefault();
  e.stopPropagation();
  setCtxMenu({ asset, x: e.clientX, y: e.clientY });
};
```

Three call sites use `onContextMenu`:
- **Grid view `AssetCard`**: `<AssetCard ... onContextMenu={function(e) { openCtx(a, e); }} />`
- **List view row**: `<div ... onClick=... onContextMenu={function(e) { openCtx(a, e); }} ...>`
- **AssetCard root element**: `<div className="asset-card" onClick={onOpen} onContextMenu={onContextMenu} ...>`
- **The menu itself** (suppresses native menu inside it): `<div ref={ref} className="ctx-menu" ... onContextMenu={function(e) { e.preventDefault(); e.stopPropagation(); }}>`

The `AssetContextMenu` component renders these actions for a right-clicked asset:
- Open
- Rename… (prompts, then PATCH `/assets/:id` with `display_name`)
- Move to bin (PATCH `/assets/:id` with `bin_id`, plus "Remove from bin")
- Copy asset ID (clipboard)
- Delete permanently (DELETE `/assets/:id?hard=true`, with confirm)

It also positions itself into the viewport via `useLayoutEffect` to avoid going off-screen.

---

### 2. `services/web-ui/public/styles-rest.css` — partial-match note

Contains a `.row-menu` popover class used by a different feature ("Row popover menu (Users, etc.)" — admin tables, not right-click). No `ctx-menu` CSS class is defined in any of the searched stylesheets. The `AssetContextMenu` JSX uses `className="ctx-menu"` and child class names `ctx-header`, `ctx-divider`, `ctx-section-label`, `ctx-empty` — none of which appear in any of the CSS files in `services/web-ui/public/`. **This likely means the ctx-menu is unstyled or styled elsewhere (not in these public CSS files).**

---

### 3. All other files — no matches

Programmatically searched for `onContextMenu`, `oncontextmenu`, `contextmenu`, `context-menu`, `right-click`, `rightclick`, `ctxMenu`, `ctx-menu`:

- `shell.jsx` — none
- `app.jsx` — none
- `screens-projects.jsx` — none
- `screens-ingest.jsx` — none
- `screens-jobs.jsx` — none (has a `<button className="icon-btn" title="Remove job" onClick={() => onDelete(job)}><Icon name="x" /></button>` but no right-click)
- `screens-editor.jsx` — none
- `screens-admin.jsx` — none (searched via saved tool-output file)
- `screens-asset.jsx` — none
- `screens-home.jsx` — none
- `tweaks-panel.jsx` — none
- `modal-new-recorder.jsx` — none
- `visuals.jsx` — none
- `icons.jsx` — none
- `data.jsx` — none
- `styles.css` — none
- `styles-fixes.css` — none
- `styles-screens.css` — none
- `styles-asset.css` — none
- `styles-modal.css` — none

---

### Key takeaway

If you're tracking down a "right-click on X" feature, the only one in the public web-UI is the **library asset context menu** in `screens-library.jsx`. The `ctx-menu` CSS class names referenced there are **not defined** in any of the public CSS files searched — worth noting if styling appears broken.

agentId: a7166e8c60bfd987a (use SendMessage with to: 'a7166e8c60bfd987a' to continue this agent)
<usage>total_tokens: 150226
tool_uses: 24
duration_ms: 109602</usage>

Prompt:
Search the forgejo repo zgaetano/dragonflight (use mcp__claude_ai_mcp-server__forgejo_forgejo_get_file and forgejo_list_directory) for any code that implements a right-click / context-menu UI feature. Specifically search the files under services/web-ui/public/ for:

1. The string "onContextMenu" (React handler)
2. The string "oncontextmenu"
3. The string "contextmenu" (event name)
4. The string "context-menu" (CSS class)
5. The string "right-click" or "rightclick"

For each match, report:
- Which file
- A few lines of surrounding context (what's being right-clicked, what menu is shown)

Files to check (all under services/web-ui/public/):
- shell.jsx (current main branch)
- app.jsx
- screens-library.jsx
- screens-projects.jsx
- screens-ingest.jsx
- screens-jobs.jsx
- screens-editor.jsx
- screens-admin.jsx
- screens-asset.jsx
- screens-home.jsx
- tweaks-panel.jsx
- modal-new-recorder.jsx
- visuals.jsx
- icons.jsx
- data.jsx
- styles.css
- styles-rest.css
- styles-fixes.css
- styles-screens.css
- styles-asset.css
- styles-modal.css

Use forgejo_get_file with owner="zgaetano" and repo="dragonflight" for each. Be efficient — you may grep through the content as you fetch. Report ALL findings, even if "nothing found". Keep report under 200 lines.


The right-click menu exists already in screens-library.jsx — it's just missing CSS. Let me read it and then check why search is broken.


File: services/web-ui/public/screens-library.jsx

// screens-library.jsx

function Library({ navigate, onOpenAsset, openProject }) {
  const { BINS, PROJECTS } = window.ZAMPP_DATA;
  const [view, setView] = React.useState('grid');
  const [filter, setFilter] = React.useState('all');
  const [search, setSearch] = React.useState('');
  // Local state lets us re-render after delete / move-to-bin without forcing
  // a full app reload — keeps ZAMPP_DATA in sync as the cache of record.
  const [allAssets, setAllAssets] = React.useState(window.ZAMPP_DATA.ASSETS || []);
  const [ctxMenu, setCtxMenu] = React.useState(null); // { asset, x, y }

  const refreshAssets = React.useCallback(() => {
    window.ZAMPP_API.fetch('/assets?limit=500')
      .then(r => {
        const list = Array.isArray(r) ? r : (r.assets || []);
        const proj = {};
        (PROJECTS || []).forEach(p => { proj[p.id] = p.name; });
        const normalized = list.map(a => window.normalizeAsset ? window.normalizeAsset(a, proj) : a);
        window.ZAMPP_DATA.ASSETS = normalized;
        setAllAssets(normalized);
      })
      .catch(() => {});
  }, [PROJECTS]);

  // Dismiss the context menu on any outside click (capture phase so clicking
  // a menu item still fires before the menu unmounts).
  React.useEffect(() => {
    if (!ctxMenu) return;
    const close = () => setCtxMenu(null);
    window.addEventListener('click', close);
    window.addEventListener('contextmenu', close);
    window.addEventListener('scroll', close, true);
    return () => {
      window.removeEventListener('click', close);
      window.removeEventListener('contextmenu', close);
      window.removeEventListener('scroll', close, true);
    };
  }, [ctxMenu]);

  const openCtx = (asset, e) => {
    e.preventDefault();
    e.stopPropagation();
    setCtxMenu({ asset, x: e.clientX, y: e.clientY });
  };

  let assets = openProject
    ? allAssets.filter(function(a) { return a.project_id === openProject.id; })
    : allAssets;
  const ALL_ASSETS = allAssets;
  if (filter !== 'all') assets = assets.filter(function(a) { return a.status === filter; });
  if (search) assets = assets.filter(function(a) { return a.name.toLowerCase().includes(search.toLowerCase()); });

  const displayTitle = openProject ? openProject.name : 'All Assets';
  const errorCount = ALL_ASSETS.filter(function(a) { return a.status === 'error'; }).length;
  const recentCount = ALL_ASSETS.filter(function(a) { return (Date.now() - new Date(a.created_at)) < 86400000; }).length;

  return (

Projects

            <div className={`rail-item ${!openProject ? 'active' : ''}`} onClick={function() { navigate('library'); }} style={{ cursor: 'pointer' }}>
              <Icon name="library" size={13} className="rail-icon" />
              All projects
              {ALL_ASSETS.length}

            {PROJECTS.slice(0, 8).map(function(p) {
              return (
                <div key={p.id} className={`rail-item ${openProject && openProject.id === p.id ? 'active' : ''}`} style={{ cursor: 'pointer' }}
                  onClick={function() { navigate('projects'); }}>

                  {p.name}
                  {p.assets}

              );
            })}

        {BINS.length > 0 && (

Bins

              {BINS.map(function(b) {
                return (

                    <Icon name={binIcon(b.icon)} size={13} className="rail-icon" />
                    {b.name}
                    {b.count}

                );
              })}

        )}

Smart filters

            {errorCount > 0 &&
<Icon name="alert" size={13} className="rail-icon" />Errors{errorCount}
}

<Icon name="clock" size={13} className="rail-icon" />Last 24h{recentCount}

<Icon name="check" size={13} className="rail-icon" />Ready{ALL_ASSETS.filter(function(a) { return a.status === 'ready'; }).length}

{displayTitle}

          · {assets.length} assets

            <Icon name="search" className="search-icon" />
            <input value={search} onChange={function(e) { setSearch(e.target.value); }} placeholder="Filter assets…" />

            {['all', 'ready', 'processing', 'live', 'error'].map(function(f) {
              return (
                <button key={f} className={filter === f ? 'active' : ''} onClick={function() { setFilter(f); }}>
                  {f === 'all' ? 'All' : f[0].toUpperCase() + f.slice(1)}
                </button>
              );
            })}

            <button className={view === 'grid' ? 'active' : ''} onClick={function() { setView('grid'); }}><Icon name="grid" size={12} /></button>
            <button className={view === 'list' ? 'active' : ''} onClick={function() { setView('list'); }}><Icon name="list" size={12} /></button>

          <button className="btn primary" onClick={function() { navigate('upload'); }}><Icon name="upload" />Upload</button>

        {assets.length === 0 ? (

No assets match this filter.

        ) : view === 'grid' ? (

            {assets.map(function(a) {
              return <AssetCard key={a.id} asset={a}
                       onOpen={function() { onOpenAsset(a); }}
                       onContextMenu={function(e) { openCtx(a, e); }} />;
            })}

        ) : (

Name

Duration

Resolution

Codec

Size

Updated

            {assets.map(function(a) {
              return (

<AssetThumb asset={a} />

{a.name}

                      <StatusDot status={a.status} />
                      {a.status}

{a.duration}

{a.res}

{a.codec || '—'}

{a.size}

{a.updated}

                  <button className="icon-btn" onClick={function(e) { e.stopPropagation(); openCtx(a, e); }}><Icon name="more" /></button>

              );
            })}

        )}

      {ctxMenu && (
        <AssetContextMenu
          asset={ctxMenu.asset}
          x={ctxMenu.x}
          y={ctxMenu.y}
          bins={BINS}
          onClose={function() { setCtxMenu(null); }}
          onChanged={refreshAssets}
          onOpen={function() { onOpenAsset(ctxMenu.asset); }}
        />
      )}

  );
}

function AssetContextMenu({ asset, x, y, bins, onClose, onChanged, onOpen }) {
  const ref = React.useRef(null);
  // Pin the menu inside the viewport even if the user right-clicked near
  // the bottom-right edge of the grid.
  const [pos, setPos] = React.useState({ left: x, top: y });
  React.useLayoutEffect(() => {
    if (!ref.current) return;
    const r = ref.current.getBoundingClientRect();
    const margin = 8;
    let nx = x, ny = y;
    if (x + r.width  + margin > window.innerWidth)  nx = window.innerWidth  - r.width  - margin;
    if (y + r.height + margin > window.innerHeight) ny = window.innerHeight - r.height - margin;
    setPos({ left: Math.max(margin, nx), top: Math.max(margin, ny) });
  }, [x, y]);

  const rename = function() {
    onClose();
    const next = prompt('Rename asset', asset.display_name || asset.name || '');
    if (next == null) return;
    const trimmed = next.trim();
    if (!trimmed || trimmed === (asset.display_name || asset.name)) return;
    window.ZAMPP_API.fetch('/assets/' + asset.id, { method: 'PATCH', body: JSON.stringify({ display_name: trimmed }) })
      .then(onChanged)
      .catch(function(e) { alert('Rename failed: ' + e.message); });
  };

  const moveToBin = function(binId) {
    onClose();
    window.ZAMPP_API.fetch('/assets/' + asset.id, { method: 'PATCH', body: JSON.stringify({ bin_id: binId }) })
      .then(onChanged)
      .catch(function(e) { alert('Move failed: ' + e.message); });
  };

  const copyId = function() {
    onClose();
    if (navigator.clipboard) navigator.clipboard.writeText(asset.id).catch(function() {});
  };

  const remove = function() {
    onClose();
    if (!confirm('Delete "' + (asset.display_name || asset.name) + '" permanently?\nThis removes the database row and the S3 objects.\nThis cannot be undone.')) return;
    window.ZAMPP_API.fetch('/assets/' + asset.id + '?hard=true', { method: 'DELETE' })
      .then(onChanged)
      .catch(function(e) { alert('Delete failed: ' + e.message); });
  };

  return (

{asset.display_name || asset.name}

      <button onClick={function() { onClose(); onOpen(); }}><Icon name="play" size={11} />Open</button>
      <button onClick={rename}><Icon name="edit" size={11} />Rename…</button>

      {(bins && bins.length > 0) ? (
        <>

Move to bin

          {bins.slice(0, 10).map(function(b) {
            const isCurrent = asset.bin_id === b.id;
            return (
              <button key={b.id} onClick={function() { moveToBin(b.id); }} disabled={isCurrent}>
                <Icon name="folder" size={11} />{b.name}{isCurrent && current}
              </button>
            );
          })}
          {asset.bin_id && (
            <button onClick={function() { moveToBin(null); }}>
              <Icon name="x" size={11} />Remove from bin
            </button>
          )}
        </>
      ) : (

No bins — create one in a project

      )}

      <button onClick={copyId}><Icon name="library" size={11} />Copy asset ID</button>
      <button className="danger" onClick={remove}><Icon name="trash" size={11} />Delete permanently</button>

  );
}

function AssetCard({ asset, onOpen, onContextMenu }) {
  const [hoverStream, setHoverStream] = React.useState(null);
  const [hovered, setHovered] = React.useState(false);
  const timerRef = React.useRef(null);
  const hlsRef = React.useRef(null);
  const videoRef = React.useRef(null);

  const handleMouseEnter = function() {
    timerRef.current = setTimeout(function() {
      setHovered(true);
      if (!hoverStream) {
        window.ZAMPP_API.fetch('/assets/' + asset.id + '/stream')
          .then(function(r) { if (r && r.url) setHoverStream(r); })
          .catch(function() {});
      }
    }, 350);
  };

  const handleMouseLeave = function() {
    clearTimeout(timerRef.current);
    setHovered(false);
  };

  // HLS wiring
  React.useEffect(function() {
    if (!hovered || !hoverStream || hoverStream.type !== 'hls' || !videoRef.current) return;
    if (!window.Hls) return;
    hlsRef.current = new window.Hls({ maxBufferLength: 10 });
    hlsRef.current.loadSource(hoverStream.url);
    hlsRef.current.attachMedia(videoRef.current);
    return function() {
      if (hlsRef.current) { hlsRef.current.destroy(); hlsRef.current = null; }
    };
  }, [hovered, hoverStream]);

  const showVideo = hovered && hoverStream;

  return (

        <AssetThumb asset={asset} />
        {showVideo && (
          <video
            key={hoverStream.url}
            ref={videoRef}
            src={hoverStream.type !== 'hls' ? hoverStream.url : undefined}
            autoPlay
            muted
            loop
            playsInline
            style={{
              position: 'absolute',
              inset: 0,
              width: '100%',
              height: '100%',
              objectFit: 'cover',
              borderRadius: 6,
            }}
          />
        )}

        {asset.status === 'live' && LIVE}
        {asset.status === 'processing' && Processing}
        {asset.status === 'error' && Error}

      {(asset.type === 'video' || !asset.type) && asset.duration !== '—' &&
{asset.duration}
}

{asset.name}

          {asset.res}
          ·
          {asset.size}

  );
}

function binIcon(name) {
  return { grid: 'library', live: 'record', film: 'film', proxy: 'proxy', audio: 'audio', package: 'package' }[name] || 'folder';
}

window.Library = Library;
window.AssetCard = AssetCard;


File: README.md

# Wild Dragon

Self-hosted Media Asset Management platform built to replace Grass Valley AMPP FramelightX.

## Services

| Service | Port | Description |
|---------|------|-------------|
| **web-ui** | 8080 | Browser-based MAM interface + capture controls |
| **mam-api** | 3000 | REST API — assets, projects, bins, jobs |
| **capture** | 3001 | SDI capture daemon (Blackmagic DeckLink + FFmpeg) |
| **worker** | — | Async job processor (proxy gen, thumbnails, conform) |
| **db** | 5432 | PostgreSQL 16 metadata store |
| **queue** | 6379 | Redis 7 job queue (BullMQ) |

## Quick Start

```bash
# Clone
git clone https://forge.wilddragon.net/zgaetano/wild-dragon.git
cd wild-dragon

# Configure
cp .env.example .env
# Edit .env with your S3 credentials and secrets

# Launch
docker compose up -d

# Open
open http://localhost:8080
```

## Architecture

```
SDI Input (DeckLink) → capture service → dual FFmpeg streams
                                          ├─ HiRes (ProRes) → S3
                                          └─ Proxy (H.264)  → S3
                                                                ↓
                        web-ui ← mam-api ← PostgreSQL ← worker (BullMQ)
                                                          ├─ proxy_gen
                                                          ├─ thumbnail
                                                          └─ conform (EDL → FFmpeg → export)
```

## Tech Stack

- **Backend:** Node.js / Express
- **Frontend:** Vanilla HTML/CSS/JS
- **Database:** PostgreSQL 16
- **Queue:** Redis 7 + BullMQ
- **Storage:** S3-compatible (RustFS)
- **Media Processing:** FFmpeg
- **Capture:** Blackmagic DeckLink SDK
- **Deployment:** Docker Compose

## License

MIT


ToolSearch

Question: select:mcp__claude_ai_mcp-server__cloudflare_cloudflare_list_dns_records,mcp__claude_ai_mcp-server__cloudflare_cloudflare_list_zones


Question: select:mcp__claude_ai_mcp-server__cloudflare_cloudflare_list_dns_records,mcp__claude_ai_mcp-server__cloudflare_cloudflare_list_zones


Let me try the live URL via Puppeteer to see what's actually happening:


Error: Error executing tool navigate: net::ERR_NAME_NOT_RESOLVED at https://dragonflight.wilddragon.net/


Error: Error executing tool navigate: net::ERR_NAME_NOT_RESOLVED at https://mam.wilddragon.net/


Let me try to ssh into the TrueNAS host to see what the live deploy actually contains, and check styles.css for z-index conflicts.


Question: select:mcp__claude_ai_mcp-server__ssh_ssh_exec,mcp__claude_ai_mcp-server__ssh_ssh_read_file


Question: select:mcp__claude_ai_mcp-server__ssh_ssh_exec,mcp__claude_ai_mcp-server__ssh_ssh_read_file


TodoWrite

Find the running web-ui container & verify what shell.jsx it serves

Finding the running web-ui container & verifying what shell.jsx it serves

Diagnose why search bar still appears broken

Diagnosing why search bar still appears broken

Add CSS for existing .ctx-menu (right-click menu)

Adding CSS for existing .ctx-menu (right-click menu)

Push fixes to forgejo


Commit changes to forgejo

Find the running web-ui container & verify what shell.jsx it serves

Finding the running web-ui container & verifying what shell.jsx it serves

Diagnose why search bar still appears broken

Diagnosing why search bar still appears broken

Add CSS for existing .ctx-menu (right-click menu)

Adding CSS for existing .ctx-menu (right-click menu)

Push fixes to forgejo


docker ps --format '{{.Names}}	{{.Image}}	{{.Ports}}' | grep -Ei 'web-ui|dragon'


$ docker ps --format '{{.Names}}	{{.Image}}	{{.Ports}}' | grep -Ei 'web-ui|dragon'

wild-dragon-mam-api-1	wild-dragon-mam-api	0.0.0.0:47432->3000/tcp, [::]:47432->3000/tcp
wild-dragon-web-ui-1	wild-dragon-web-ui	0.0.0.0:47434->80/tcp, [::]:47434->80/tcp
wild-dragon-worker-1	wild-dragon-worker
wild-dragon-db-1	postgres:16	0.0.0.0:47436->5432/tcp, [::]:47436->5432/tcp
wild-dragon-queue-1	redis:7-alpine	0.0.0.0:47437->6379/tcp, [::]:47437->6379/tcp
wilddragon-web	nginx:alpine	0.0.0.0:43036->80/tcp, [::]:43036->80/tcp
dragonfork-core	137af1120bb1
dragonfork-grafana	761b48cb57a0	0.0.0.0:3000->3000/tcp, [::]:3000->3000/tcp
dragonfork-webrtc-poc	a2d1be7a60d2


docker exec wild-dragon-web-ui-1 grep -c 'GlobalSearch' /usr/share/nginx/html/shell.jsx; docker exec wild-dragon-web-ui-1 grep -c 'search-wrap' /usr/share/nginx/html/styles-rest.css; docker exec wild-dragon-web-ui-1 ls -la /usr/share/nginx/html/shell.jsx /usr/share/nginx/html/styles-rest.css


$ docker exec wild-dragon-web-ui-1 grep -c 'GlobalSearch' /usr/share/nginx/html/shell.jsx; docker exec wild-dragon-web-ui-1 grep -c 'search-wrap' /usr/share/nginx/html/styles-rest.css; docker exec wild-dragon-web-ui-1 ls -la /usr/share/nginx/html/shell.jsx /usr/share/nginx/html/styles-rest.css

0
0
-rw-r--r--    1 root     root          6239 May 23 03:19 /usr/share/nginx/html/shell.jsx
-rw-r--r--    1 root     root         23292 May 23 03:52 /usr/share/nginx/html/styles-rest.css


The container hasn't been rebuilt — it's still serving the pre-change files. I need to ship the ctx-menu CSS then rebuild. Let me grab the current repo styles-rest.css and add ctx-menu styles.


File: services/web-ui/public/styles-rest.css

/* ========== Projects ========== */
.projects-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 14px;
}
.project-card {
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  overflow: hidden;
  cursor: pointer;
  transition: transform 120ms, border 80ms;
}
.project-card:hover {
  transform: translateY(-2px);
  border-color: var(--border-stronger);
}
.project-thumb-grid {
  display: grid;
  grid-template-columns: 2fr 1fr;
  grid-template-rows: 1fr 1fr;
  gap: 2px;
  aspect-ratio: 16 / 9;
  background: var(--bg-2);
}
.project-thumb-grid .project-thumb-cell:first-child { grid-row: span 2; }
.project-thumb-cell { position: relative; overflow: hidden; background: var(--bg-2); }
.project-thumb-cell .thumb-svg { width: 100%; height: 100%; }
.project-card-body { padding: 12px 14px; }
.project-meta {
  display: flex; gap: 6px;
  font-size: 11.5px; color: var(--text-3);
  margin-top: 4px;
}
.project-bar {
  margin-top: 10px;
  display: flex;
  height: 4px;
  border-radius: 99px;
  overflow: hidden;
  background: var(--bg-3);
  gap: 1px;
}

/* ========== Upload ========== */
.field-label {
  font-size: 10.5px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-4);
  display: block;
  margin-bottom: 6px;
}
.select-faux, .field-input {
  height: 34px;
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  padding: 0 10px;
  display: flex; align-items: center; gap: 8px;
  color: var(--text-1);
  font-size: 12.5px;
  width: 100%;
  cursor: pointer;
}
.field-input { font-size: 12.5px; outline: 0; }
.field-input:focus { border-color: var(--accent); background: var(--bg-2); }
.field-input.select { justify-content: space-between; }

.dropzone {
  background: var(--bg-1);
  border: 1.5px dashed var(--border-stronger);
  border-radius: var(--r-lg);
  padding: 40px 24px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  transition: background 120ms, border 120ms;
}
.dropzone:hover { background: var(--bg-2); border-color: var(--accent); }
.dropzone-formats { display: flex; gap: 6px; margin-top: 8px; flex-wrap: wrap; justify-content: center; }
.upload-row {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 14px;
  border-bottom: 1px solid var(--border);
}
.upload-row:last-child { border-bottom: 0; }

/* ========== Recorders ========== */
.recorders-list { display: flex; flex-direction: column; gap: 10px; }
.recorder-row {
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: 12px;
  display: grid;
  grid-template-columns: 220px 1fr 380px auto;
  align-items: center;
  gap: 16px;
  transition: border 80ms;
}
.recorder-row:hover { border-color: var(--border-stronger); }
.recorder-row.recording { border-color: rgba(255,59,48,0.25); }
.recorder-row.error { border-color: rgba(255,91,91,0.25); }
.recorder-preview {
  height: 56px;
  border-radius: var(--r-sm);
  background: var(--bg-2);
  overflow: hidden;
  position: relative;
}
.recorder-empty {
  height: 100%;
  display: grid; place-items: center;
  color: var(--text-3);
}
.recorder-audio-prev {
  height: 100%;
  display: flex; align-items: center; gap: 10px;
  padding: 0 12px;
}
.recorder-audio-prev .waveform { flex: 1; height: 80%; }
.recorder-info { min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.recorder-sub {
  font-size: 11.5px;
  color: var(--text-3);
  display: flex; gap: 6px;
}
.recorder-sub.mono { font-family: var(--font-mono); font-size: 11px; }
.recorder-stats {
  display: grid;
  grid-template-columns: 90px 90px 1fr;
  gap: 12px;
}
.recorder-stat .stat-label {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 600;
  color: var(--text-4);
  margin-bottom: 3px;
}
.recorder-stat .stat-val { font-size: 12.5px; }
.recorder-actions { display: flex; align-items: center; gap: 4px; }
.rec-dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: currentColor;
}

/* ========== Capture / DeckLink ========== */
.decklink-card {
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  overflow: hidden;
}
.decklink-head {
  padding: 14px 16px;
  display: flex; justify-content: space-between; align-items: center;
  border-bottom: 1px solid var(--border);
}
.decklink-body {
  padding: 24px;
  display: grid;
  grid-template-columns: 60px 1fr;
  gap: 20px;
  align-items: stretch;
  background:
    radial-gradient(ellipse at top, rgba(91,124,250,0.05), transparent 60%),
    var(--bg-0);
}
.decklink-card-face {
  background: linear-gradient(180deg, #1c1f28, #0d0e13);
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  display: flex; flex-direction: column;
  justify-content: space-between;
  padding: 12px 6px;
  position: relative;
}
.decklink-label {
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  font-family: var(--font-mono);
  font-size: 8.5px;
  letter-spacing: 0.12em;
  color: var(--text-3);
  text-align: center;
  margin: 0 auto;
}
.decklink-leds {
  display: flex; gap: 6px;
  justify-content: center;
}
.decklink-led {
  width: 6px; height: 6px;
  border-radius: 50%;
  background: rgba(255,255,255,0.1);
}
.decklink-led.on {
  background: var(--success);
  box-shadow: 0 0 6px var(--success);
}
.bnc-ports { display: flex; flex-direction: column; gap: 10px; }
.bnc-port {
  display: grid;
  grid-template-columns: 32px 1fr auto;
  align-items: center;
  gap: 12px;
  padding: 10px;
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  text-align: left;
  cursor: pointer;
  position: relative;
  transition: background 80ms, border 80ms;
}
.bnc-port:hover { border-color: var(--border-stronger); }
.bnc-port.active { background: var(--bg-3); }
.bnc-port.active.live::before {
  content: "";
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 2px;
  background: var(--success);
}
.bnc-port.recording::before { background: var(--live); }
.bnc-port.active { box-shadow: inset 0 0 0 1px var(--accent); border-color: var(--accent); }
.bnc-connector {
  width: 32px; height: 32px;
  border-radius: 50%;
  background: radial-gradient(circle, #1a1d24, #0a0c11);
  display: grid; place-items: center;
  border: 1.5px solid var(--border-stronger);
  position: relative;
}
.bnc-pin {
  width: 6px; height: 6px;
  background: linear-gradient(135deg, #6b7280, #2a2f3a);
  border-radius: 50%;
}
.bnc-ring {
  position: absolute;
  inset: 4px;
  border: 1px dashed rgba(255,255,255,0.08);
  border-radius: 50%;
}
.bnc-port.live .bnc-pin { background: linear-gradient(135deg, #FFC857, #C8862D); box-shadow: 0 0 8px rgba(255,200,87,0.4); }
.bnc-port.recording .bnc-pin { background: linear-gradient(135deg, #FF5C5C, #C8362D); box-shadow: 0 0 8px rgba(255,92,92,0.5); }
.bnc-info { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.bnc-num { font-size: 12.5px; font-weight: 600; }
.bnc-label { font-size: 11px; color: var(--text-2); }
.bnc-sig { font-size: 10.5px; color: var(--text-3); }
.bnc-port.live .bnc-sig { color: var(--success); }
.bnc-rec {
  width: 10px; height: 10px;
  border-radius: 50%;
  background: var(--live);
  animation: pulse 1.2s ease-in-out infinite;
}
.bnc-signal-bar {
  grid-column: 2 / 4;
  height: 3px;
  background: var(--bg-3);
  border-radius: 99px;
  overflow: hidden;
  margin-top: 4px;
}
.bnc-signal-fill {
  height: 100%;
  background: linear-gradient(90deg, var(--success), var(--warning), var(--danger));
  background-size: 200% 100%;
  background-position: 0% 0;
  transition: width 200ms;
}
.decklink-foot {
  padding: 10px 16px;
  border-top: 1px solid var(--border);
  display: flex; gap: 20px;
  font-size: 11.5px;
  color: var(--text-3);
}
.decklink-foot strong { color: var(--text-1); font-weight: 600; }

.capture-detail {
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: 16px;
}
.capture-detail.empty {
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  padding: 80px 24px;
  color: var(--text-3);
}
.capture-preview {
  position: relative;
  aspect-ratio: 16 / 9;
  border-radius: var(--r-md);
  overflow: hidden;
  background: #000;
}
.capture-overlay-meters {
  position: absolute;
  right: 8px; top: 8px;
  display: flex; gap: 4px;
}
.capture-tc {
  position: absolute;
  bottom: 8px; right: 8px;
  background: rgba(0,0,0,0.7);
  padding: 3px 8px;
  border-radius: 3px;
  font-family: var(--font-mono);
  font-size: 11px;
  color: white;
}
.capture-stats {
  margin-top: 16px;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}
.capture-stat {
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  padding: 8px 10px;
}
.capture-stat-label {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 600;
  color: var(--text-4);
}
.capture-stat-value { font-size: 13px; margin-top: 2px; }

/* ========== Monitors ========== */
.monitors-grid {
  display: grid;
  gap: 10px;
  background: var(--bg-2);
  border-radius: var(--r-lg);
  padding: 10px;
}
.monitor-tile {
  position: relative;
  aspect-ratio: 16 / 9;
  background: #000;
  border-radius: var(--r-sm);
  overflow: hidden;
  border: 1px solid var(--border);
  display: flex; flex-direction: column;
  cursor: pointer;
}
.monitor-tile.audio { background: linear-gradient(135deg, hsl(180 30% 12%), hsl(200 25% 6%)); }
.monitor-tile-label {
  position: absolute;
  bottom: 0; left: 0; right: 0;
  padding: 8px 10px;
  background: linear-gradient(180deg, transparent, rgba(0,0,0,0.8));
  display: flex; align-items: center; gap: 6px;
}
.monitor-tile-label .name {
  color: white;
  font-size: 11.5px;
  font-weight: 500;
}
.monitor-tile-label .time {
  margin-left: auto;
  color: white;
  font-family: var(--font-mono);
  font-size: 10.5px;
}

/* ========== Jobs ========== */
.jobs-stats {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 12px;
}
.job-row {
  display: grid;
  grid-template-columns: 20px 110px 1fr 90px 200px 70px 80px 90px;
  align-items: center;
  gap: 12px;
  padding: 10px 16px;
  border-bottom: 1px solid var(--border);
  font-size: 12.5px;
}
.job-row:last-child { border-bottom: 0; }
.job-row.head {
  font-size: 10.5px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-4);
}
.job-progress-wrap {
  display: flex; align-items: center; gap: 8px;
}
.job-progress-bar {
  flex: 1;
  height: 5px;
  background: var(--bg-3);
  border-radius: 99px;
  overflow: hidden;
  position: relative;
}
.job-progress-fill {
  height: 100%;
  background: linear-gradient(90deg, var(--accent), #7C9EFF);
  background-size: 200% 100%;
  animation: shimmer 2s linear infinite;
  transition: width 300ms;
}

/* ========== Editor ========== */
.editor-shell {
  display: flex; flex-direction: column;
  height: 100%;
  background: var(--bg-0);
}
.editor-topbar {
  height: 44px;
  border-bottom: 1px solid var(--border);
  padding: 0 14px;
  display: flex; align-items: center; gap: 10px;
}
.editor-body {
  flex: 1;
  display: grid;
  grid-template-columns: 240px 1fr 240px;
  min-height: 0;
}
.editor-bins, .editor-insp { background: var(--bg-1); border-right: 1px solid var(--border); overflow-y: auto; }
.editor-insp { border-right: 0; border-left: 1px solid var(--border); }
.editor-bin-item {
  display: flex; align-items: center; gap: 8px;
  padding: 6px;
  border-radius: var(--r-sm);
  cursor: pointer;
}
.editor-bin-item:hover { background: var(--hover); }
.editor-bin-thumb { width: 48px; aspect-ratio: 16/9; border-radius: 3px; overflow: hidden; background: var(--bg-2); flex-shrink: 0; }
.editor-viewer { background: #000; display: flex; flex-direction: column; }
.editor-canvas {
  flex: 1;
  position: relative;
  background: #050608;
  display: grid; place-items: center;
  overflow: hidden;
  max-height: 50vh;
}
.editor-canvas .thumb-svg { width: 100%; height: 100%; }
.editor-transport {
  height: 44px;
  display: flex; align-items: center; gap: 8px;
  padding: 0 14px;
  background: var(--bg-1);
  border-top: 1px solid var(--border);
}
.editor-timeline {
  background: var(--bg-0);
  border-top: 1px solid var(--border);
  height: 240px;
  display: flex; flex-direction: column;
  position: relative;
  overflow: hidden;
}
.editor-timeline-head {
  height: 32px;
  display: flex; align-items: center;
  padding: 0 12px;
  gap: 8px;
  border-bottom: 1px solid var(--border);
}
.timeline-ruler {
  height: 22px;
  display: flex;
  padding-left: 40px;
  border-bottom: 1px solid var(--border);
  background: var(--bg-1);
}
.ruler-tick {
  flex: 1;
  border-left: 1px solid var(--border);
  font-size: 9.5px;
  color: var(--text-4);
  padding: 4px 4px 0;
}
.timeline-track {
  display: grid;
  grid-template-columns: 40px 1fr;
  align-items: stretch;
  height: 42px;
  border-bottom: 1px solid var(--border);
}
.timeline-track-label {
  background: var(--bg-1);
  display: grid; place-items: center;
  font-size: 10.5px;
  font-family: var(--font-mono);
  font-weight: 600;
  color: var(--text-3);
  border-right: 1px solid var(--border);
}
.timeline-track-lane {
  position: relative;
  background:
    repeating-linear-gradient(90deg, transparent 0 49px, rgba(255,255,255,0.02) 49px 50px);
}
.clip {
  position: absolute;
  top: 4px; bottom: 4px;
  border-radius: 3px;
  overflow: hidden;
  display: flex; align-items: center;
  cursor: pointer;
  border: 1px solid rgba(0,0,0,0.3);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.15);
}
.clip-label {
  position: absolute;
  left: 6px; top: 3px;
  font-size: 10px;
  font-weight: 600;
  color: white;
  z-index: 2;
  text-shadow: 0 1px 1px rgba(0,0,0,0.4);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.clip-film { display: flex; flex: 1; height: 100%; }
.clip-film-cell { flex: 1; overflow: hidden; }
.clip-film-cell .thumb-svg { width: 100%; height: 100%; }
.clip.audio { background: var(--success) !important; }
.clip-wave { width: 100%; height: 100%; padding: 4px; opacity: 0.6; }
.clip-wave .waveform { width: 100%; height: 100%; }
.timeline-playhead {
  position: absolute;
  top: 32px; bottom: 0;
  width: 1.5px;
  background: var(--live);
  pointer-events: none;
  z-index: 5;
}
.timeline-playhead::before {
  content: "";
  position: absolute;
  top: -4px; left: 50%;
  width: 10px; height: 10px;
  background: var(--live);
  transform: translateX(-50%) rotate(45deg);
}

/* ========== Admin tables ========== */
/* ── Row popover menu (Users, etc.) ────────────────────────────────── */
.row-menu {
  position: absolute;
  right: 0;
  top: calc(100% + 4px);
  z-index: 20;
  min-width: 180px;
  background: var(--bg-1);
  border: 1px solid var(--border-stronger);
  border-radius: 6px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);
  padding: 4px;
  display: flex;
  flex-direction: column;
}
.row-menu button {
  display: flex;
  align-items: center;
  gap: 8px;
  background: transparent;
  border: 0;
  color: var(--text-1);
  font-size: 12.5px;
  padding: 7px 10px;
  border-radius: 4px;
  cursor: pointer;
  text-align: left;
}
.row-menu button:hover { background: var(--bg-3); }
.row-menu button.danger:hover { background: var(--danger-soft); color: var(--danger); }

.user-row, .token-row, .container-row, .schedule-row {
  display: grid;
  align-items: center;
  gap: 12px;
  padding: 12px 16px;
  border-bottom: 1px solid var(--border);
  font-size: 12.5px;
}
.user-row { grid-template-columns: 1.5fr 100px 1.5fr 120px 40px; }
.token-row { grid-template-columns: 1.4fr 1.4fr 110px 110px 100px 40px; }
.container-row { grid-template-columns: 1.4fr 1.4fr 140px 140px 100px 1.4fr 110px; }
.schedule-row { grid-template-columns: 1.6fr 1.2fr 1.2fr 90px 110px 110px 150px; }
.user-row.head, .token-row.head, .container-row.head, .schedule-row.head {
  font-size: 10.5px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-4);
}
.user-row:last-child, .token-row:last-child, .container-row:last-child, .schedule-row:last-child { border-bottom: 0; }
.token-row.revoked { opacity: 0.5; }

/* ========== Cluster ========== */
.cluster-canvas {
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  overflow: hidden;
}

/* ========== Settings ========== */
.settings-nav {
  display: flex; flex-direction: column; gap: 2px;
  position: sticky; top: 0;
}
.settings-nav-item {
  display: flex; align-items: center; gap: 8px;
  padding: 0 10px;
  height: 32px;
  border-radius: var(--r-sm);
  color: var(--text-2);
  font-size: 12.5px;
  cursor: pointer;
}
.settings-nav-item:hover { background: var(--hover); color: var(--text-1); }
.settings-nav-item.active { background: var(--accent-soft); color: var(--accent-text); }
.settings-card {
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  overflow: hidden;
}
.settings-card-head {
  padding: 16px;
  display: flex; align-items: flex-start; gap: 12px;
  border-bottom: 1px solid var(--border);
}
.settings-card-icon {
  width: 36px; height: 36px;
  border-radius: var(--r-sm);
  background: var(--accent-soft);
  color: var(--accent);
  display: grid; place-items: center;
  flex-shrink: 0;
}
.settings-card-body { padding: 16px; display: flex; flex-direction: column; gap: 12px; }
.field { display: flex; flex-direction: column; gap: 4px; }
.field-input-wrap { display: flex; gap: 6px; align-items: center; }
.checkbox-row {
  display: flex; align-items: center; gap: 8px;
  font-size: 12.5px; color: var(--text-1);
  cursor: pointer;
}
.checkbox-row input { accent-color: var(--accent); }

/* ========== Tokens parody ========== */
.token-hero {
  display: grid;
  grid-template-columns: 2fr 1fr;
  gap: 14px;
  margin-bottom: 16px;
}
.token-burn-card, .token-actual-card {
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: 18px 20px;
  position: relative;
  overflow: hidden;
}
.token-burn-card {
  background:
    radial-gradient(ellipse at top right, rgba(255,91,91,0.12), transparent 60%),
    var(--bg-1);
  border-color: rgba(255,91,91,0.18);
}
.token-actual-card {
  background:
    radial-gradient(ellipse at bottom left, rgba(45,212,168,0.10), transparent 60%),
    var(--bg-1);
  border-color: rgba(45,212,168,0.18);
}
.token-card-label {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-weight: 700;
  color: var(--text-4);
  margin-bottom: 8px;
}
.token-counter {
  display: flex; align-items: baseline; gap: 12px;
}
.token-flame { font-size: 32px; line-height: 1; }
.token-big {
  font-size: 48px;
  font-weight: 700;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
  background: linear-gradient(180deg, #FF9D6B, #FF5B5B);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  line-height: 1.05;
}
.token-rate {
  font-size: 12px;
  display: flex; align-items: baseline;
  font-family: var(--font-mono);
}
.token-actual-amount {
  display: flex; align-items: baseline; gap: 0;
  color: var(--success);
  margin-bottom: 8px;
}

.token-comparison {
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: 0 0 12px;
  margin-bottom: 16px;
  overflow: hidden;
}
.token-compare-chart { padding: 0 16px; }
.token-compare-legend {
  display: flex; gap: 24px;
  padding: 8px 0 0;
  font-size: 12px;
  color: var(--text-2);
}
.token-compare-legend .dot {
  display: inline-block;
  width: 10px; height: 10px;
  border-radius: 50%;
  margin-right: 6px;
  vertical-align: middle;
}

.token-grid {
  display: grid;
  grid-template-columns: 1fr 1.4fr;
  gap: 14px;
}
.token-event {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 14px;
  border-bottom: 1px solid var(--border);
}
.token-event:last-child { border-bottom: 0; }
.token-event.fresh { animation: tokenFresh 800ms ease; }
@keyframes tokenFresh {
  0% { background: rgba(255,91,91,0.15); transform: translateX(-4px); }
  100% { background: transparent; transform: none; }
}
.token-tiers {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
}
.token-tier {
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: 16px 14px;
  display: flex; flex-direction: column;
  gap: 6px;
  position: relative;
}
.token-tier.popular {
  border-color: var(--accent);
  background: linear-gradient(180deg, var(--accent-soft) 0%, var(--bg-1) 40%);
  box-shadow: 0 0 24px rgba(91,124,250,0.10);
}
.token-tier-badge {
  position: absolute;
  top: -8px; right: 12px;
  background: var(--accent);
  color: white;
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.06em;
  padding: 3px 8px;
  border-radius: 99px;
}
.token-tier-name { font-size: 13px; font-weight: 700; letter-spacing: -0.01em; }
.token-tier-desc { font-size: 11px; color: var(--text-3); min-height: 32px; line-height: 1.4; }
.token-tier-price { margin-top: 6px; display: flex; align-items: baseline; }
.token-tier-tokens { font-size: 11px; color: var(--text-3); padding-top: 4px; border-top: 1px solid var(--border); }

.token-footnote {
  margin-top: 20px;
  padding: 14px 16px;
  background: var(--warning-soft);
  border: 1px solid rgba(245,166,35,0.2);
  border-radius: var(--r-md);
  display: flex; gap: 12px;
  font-size: 12.5px;
  color: var(--text-2);
  line-height: 1.55;
}
.token-footnote svg { color: var(--warning); flex-shrink: 0; margin-top: 2px; }
.token-footnote strong { color: var(--warning); }

/* ========== Global Search ========== */
.search-wrap {
  position: relative;
  flex-shrink: 0;
}
.search-wrap .search {
  width: 280px;
}
.search-wrap .search.is-open {
  border-color: var(--accent);
  background: var(--bg-2);
}
.search-results {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  width: 440px;
  max-width: 90vw;
  max-height: 460px;
  overflow-y: auto;
  background: var(--bg-1);
  border: 1px solid var(--border-stronger);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-pop);
  padding: 4px;
  z-index: 60;
}
.search-empty {
  padding: 18px 12px;
  text-align: center;
  font-size: 12px;
  color: var(--text-3);
}
.search-result {
  display: grid;
  grid-template-columns: 24px 1fr auto;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-radius: var(--r-sm);
  cursor: pointer;
  color: var(--text-1);
}
.search-result.active {
  background: var(--accent-soft);
}
.search-result-icon {
  width: 24px; height: 24px;
  border-radius: 5px;
  background: var(--bg-3);
  color: var(--text-2);
  display: grid; place-items: center;
  flex-shrink: 0;
}
.search-result.active .search-result-icon {
  background: var(--accent-soft-2);
  color: var(--accent-text);
}
.search-result-text {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.search-result-label {
  font-size: 12.5px;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.search-result-sub {
  font-size: 11px;
  color: var(--text-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.search-result-kind {
  font-family: var(--font-mono);
  font-size: 9.5px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-4);
  background: var(--bg-2);
  border: 1px solid var(--border);
  padding: 2px 6px;
  border-radius: 99px;
  flex-shrink: 0;
}
.search-result-kind.kind-asset { color: #B4C3FF; border-color: rgba(91,124,250,0.25); }
.search-result-kind.kind-project { color: #FFD89B; border-color: rgba(245,166,35,0.25); }
.search-result-kind.kind-recorder { color: #FFAFAF; border-color: rgba(255,91,91,0.25); }
.search-result-kind.kind-job { color: #9EE7D2; border-color: rgba(45,212,168,0.25); }
.search-result-kind.kind-user { color: #D5B8FF; border-color: rgba(181,124,250,0.25); }
.search-result-kind.kind-nav { color: var(--text-2); }


Pushing the ctx-menu styles, then rebuilding the container so all my prior changes actually go live.