foxygit / Subtitles Log in
commits tags

/index.html · 109.01 KB

raw
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SubTok — TikTok Subtitles</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Anton&family=Bangers&family=Bebas+Neue&family=Boogaloo&family=Cabin+Sketch:wght@700&family=Cinzel:wght@900&family=Fredoka+One&family=Lilita+One&family=Lobster&family=Montserrat:wght@900&family=Oswald:wght@700&family=Pacifico&family=Permanent+Marker&family=Playfair+Display:ital,wght@0,900;1,900&family=Press+Start+2P&family=Righteous&family=Russo+One&family=Teko:wght@700&family=Titan+One&display=swap" rel="stylesheet">
<style>
  :root {
    --bg: #0a0a0f;
    --surface: #13131a;
    --surface2: #1c1c26;
    --border: #2a2a3a;
    --accent: #fe2c55;
    --accent2: #25f4ee;
    --text: #f0f0f0;
    --muted: #888;
    --radius: 12px;
  }
  * { box-sizing: border-box; margin: 0; padding: 0; }
  body { background: var(--bg); color: var(--text); font-family: 'Inter', -apple-system, sans-serif; min-height: 100vh; }

  header {
    display: flex; align-items: center; gap: 12px;
    padding: 20px 32px; border-bottom: 1px solid var(--border);
    background: var(--surface);
  }
  .logo { font-size: 24px; font-weight: 900; background: linear-gradient(135deg, var(--accent), var(--accent2)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
  .logo-sub { font-size: 13px; color: var(--muted); font-weight: 400; }

  .layout { display: grid; grid-template-columns: 380px 1fr; height: calc(100vh - 65px); overflow: hidden; }

  .sidebar {
    background: var(--surface); border-right: 1px solid var(--border);
    overflow-y: auto; display: flex; flex-direction: column; gap: 0;
  }
  .section { padding: 20px; border-bottom: 1px solid var(--border); }
  .section-title { font-size: 11px; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 14px; }

  .main { display: flex; flex-direction: column; overflow: hidden; }

  /* Upload zone */
  .upload-zone {
    border: 2px dashed var(--border); border-radius: var(--radius);
    padding: 32px 20px; text-align: center; cursor: pointer;
    transition: all 0.2s; background: var(--surface2);
  }
  .upload-zone:hover, .upload-zone.drag { border-color: var(--accent); background: rgba(254,44,85,0.05); }
  .upload-icon { font-size: 36px; margin-bottom: 10px; }
  .upload-label { font-size: 14px; font-weight: 600; margin-bottom: 4px; }
  .upload-sub { font-size: 12px; color: var(--muted); }
  .upload-zone input { display: none; }

  /* Controls */
  label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 6px; margin-top: 12px; }
  label:first-of-type { margin-top: 0; }
  select, input[type=range], input[type=color], input[type=number] {
    width: 100%; background: var(--surface2); border: 1px solid var(--border);
    color: var(--text); border-radius: 8px; padding: 8px 10px; font-size: 13px;
    appearance: none; outline: none;
  }
  select:focus, input:focus { border-color: var(--accent2); }
  input[type=range] { padding: 4px 0; cursor: pointer; accent-color: var(--accent); }
  .row { display: flex; gap: 8px; }
  .row > * { flex: 1; }

  .color-row { display: flex; gap: 8px; align-items: center; }
  .color-row input[type=color] { width: 42px; height: 36px; padding: 2px; cursor: pointer; border-radius: 8px; flex-shrink: 0; }
  .color-row span { font-size: 12px; color: var(--muted); }

  /* Preset chips */
  .presets { display: flex; flex-wrap: wrap; gap: 6px; }
  .preset-chip {
    padding: 6px 12px; border-radius: 20px; font-size: 12px; font-weight: 600;
    border: 1px solid var(--border); cursor: pointer; transition: all 0.15s;
    background: var(--surface2);
  }
  .preset-chip:hover { border-color: var(--accent2); color: var(--accent2); }
  .preset-chip.active { background: var(--accent); border-color: var(--accent); color: white; }

  /* Btn */
  .btn {
    width: 100%; padding: 12px; border-radius: var(--radius); font-size: 14px;
    font-weight: 700; border: none; cursor: pointer; transition: all 0.15s;
    display: flex; align-items: center; justify-content: center; gap: 8px;
  }
  .btn-primary { background: linear-gradient(135deg, var(--accent), #ff3b5c); color: white; }
  .btn-primary:hover { transform: translateY(-1px); box-shadow: 0 4px 20px rgba(254,44,85,0.4); }
  .btn-secondary { background: var(--surface2); color: var(--text); border: 1px solid var(--border); }
  .btn-secondary:hover { border-color: var(--accent2); color: var(--accent2); }
  .btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none !important; }

  /* Progress */
  .progress-bar { height: 4px; background: var(--border); border-radius: 2px; overflow: hidden; margin: 12px 0; }
  .progress-fill { height: 100%; background: linear-gradient(90deg, var(--accent), var(--accent2)); border-radius: 2px; transition: width 0.3s; width: 0; }
  .status-text { font-size: 12px; color: var(--muted); text-align: center; min-height: 18px; }

  /* Main content */
  .preview-area { flex: 1; display: flex; gap: 0; overflow: hidden; }

  .video-panel {
    flex: 1; min-height: 0; display: flex; flex-direction: column; align-items: center;
    justify-content: center; padding: 24px; background: #050508; position: relative; overflow: hidden;
  }
  .video-wrapper { position: relative; max-width: 100%; max-height: 100%; display: flex; align-items: center; justify-content: center; }
  video { max-width: 100%; max-height: 100%; border-radius: var(--radius); display: block; }
  .video-placeholder {
    width: 300px; height: 500px; border-radius: var(--radius);
    background: var(--surface2); border: 2px dashed var(--border);
    display: flex; align-items: center; justify-content: center;
    flex-direction: column; gap: 12px; color: var(--muted);
  }
  .video-placeholder .ph-icon { font-size: 48px; }

  /* Subtitle canvas overlay */
  #sub-canvas {
    position: absolute; top: 0; left: 0; width: 100%; height: 100%;
    pointer-events: none; border-radius: var(--radius);
  }

  /* Segments panel */
  .segments-panel {
    width: 340px; background: var(--surface); border-left: 1px solid var(--border);
    display: flex; flex-direction: column; overflow: hidden;
  }
  .segments-header {
    padding: 16px 20px; border-bottom: 1px solid var(--border);
    display: flex; align-items: center; justify-content: space-between;
  }
  .segments-header h3 { font-size: 13px; font-weight: 700; }
  .seg-count { font-size: 11px; color: var(--muted); background: var(--surface2); padding: 2px 8px; border-radius: 10px; }
  .segments-list { flex: 1; overflow-y: auto; padding: 8px; }
  .segment-card {
    padding: 10px 12px; border-radius: 8px; margin-bottom: 4px;
    background: var(--surface2); border: 1px solid transparent;
    cursor: pointer; transition: all 0.15s;
  }
  .segment-card:hover { border-color: var(--border); }
  .segment-card.active { border-color: var(--accent2); background: rgba(37,244,238,0.05); }
  .seg-time { font-size: 10px; color: var(--accent2); font-family: monospace; margin-bottom: 4px; }
  .seg-text { font-size: 13px; line-height: 1.4; }
  .seg-words { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }
  .seg-word {
    font-size: 11px; padding: 2px 7px; border-radius: 4px;
    background: var(--border); color: var(--muted);
  }
  .seg-word.highlight { background: var(--accent); color: white; }

  /* Export bar */
  .export-bar {
    padding: 16px 20px; border-top: 1px solid var(--border);
    background: var(--surface); display: flex; gap: 8px; align-items: center;
  }
  .export-bar .btn { width: auto; padding: 10px 18px; font-size: 13px; }

  /* Toggle */
  .toggle-row { display: flex; align-items: center; justify-content: space-between; margin-top: 10px; }
  .toggle-label { font-size: 13px; }
  .toggle {
    width: 40px; height: 22px; background: var(--border); border-radius: 11px;
    cursor: pointer; position: relative; transition: background 0.2s;
  }
  .toggle.on { background: var(--accent); }
  .toggle::after {
    content: ''; position: absolute; width: 16px; height: 16px;
    background: white; border-radius: 50%; top: 3px; left: 3px; transition: left 0.2s;
  }
  .toggle.on::after { left: 21px; }

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

  /* Animations */
  @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.5} }
  .pulsing { animation: pulse 1.5s infinite; }

  .lang-badge {
    display: inline-block; padding: 2px 10px; border-radius: 10px; font-size: 11px;
    background: rgba(37,244,238,0.15); color: var(--accent2); margin-left: 8px;
  }

  /* Emoji */
  .seg-emoji-row { display: flex; align-items: center; gap: 6px; margin-top: 8px; flex-wrap: wrap; }
  .seg-emoji-badge {
    font-size: 20px; cursor: pointer; border-radius: 6px; padding: 2px 4px;
    background: var(--border); transition: transform 0.1s;
    position: relative;
  }
  .seg-emoji-badge:hover { transform: scale(1.2); }
  .seg-emoji-badge .remove-emoji {
    display: none; position: absolute; top: -6px; right: -6px;
    background: var(--accent); color: white; border-radius: 50%;
    width: 14px; height: 14px; font-size: 9px; line-height: 14px; text-align: center;
  }
  .seg-emoji-badge:hover .remove-emoji { display: block; }
  .add-emoji-btn {
    font-size: 14px; padding: 3px 8px; border-radius: 6px; cursor: pointer;
    background: var(--surface2); border: 1px dashed var(--border); color: var(--muted);
    transition: all 0.15s;
  }
  .add-emoji-btn:hover { border-color: var(--accent2); color: var(--accent2); }

  .emoji-picker {
    position: fixed; z-index: 1000; background: var(--surface); border: 1px solid var(--border);
    border-radius: var(--radius); padding: 12px; width: 280px;
    box-shadow: 0 8px 32px rgba(0,0,0,0.5);
  }
  .emoji-picker-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
  .emoji-picker-title { font-size: 11px; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: 1px; }
  .emoji-picker-close { background: none; border: none; color: var(--muted); cursor: pointer; font-size: 16px; padding: 0; }
  .emoji-category { font-size: 10px; color: var(--muted); margin: 8px 0 4px; text-transform: uppercase; letter-spacing: 0.5px; }
  .emoji-grid { display: flex; flex-wrap: wrap; gap: 4px; }
  .emoji-opt {
    font-size: 22px; padding: 4px; border-radius: 6px; cursor: pointer;
    transition: background 0.1s; line-height: 1;
  }
  .emoji-opt:hover { background: var(--surface2); transform: scale(1.15); }
  .emoji-opt.selected { background: rgba(254,44,85,0.2); outline: 1px solid var(--accent); }
  .emoji-autodetect-btn { margin-bottom: 10px; }
  .emoji-toggle-row { margin-top: 10px; }

  /* GIF */
  .gif-search-row { display: flex; gap: 6px; margin-bottom: 10px; }
  .gif-search-row input { flex: 1; }
  .gif-search-btn { background: var(--accent2); color: #000; border: none; border-radius: 8px; padding: 8px 12px; cursor: pointer; font-size: 14px; flex-shrink: 0; font-weight: 700; }
  .gif-search-btn:hover { opacity: 0.85; }
  .gif-results { display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px; max-height: 200px; overflow-y: auto; margin-top: 8px; }
  .gif-thumb { border-radius: 6px; overflow: hidden; cursor: pointer; aspect-ratio: 1; background: var(--surface2); position: relative; border: 2px solid transparent; transition: border-color 0.15s; }
  .gif-thumb:hover { border-color: var(--accent2); }
  .gif-thumb.selected { border-color: var(--accent); }
  .gif-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
  .gif-assign-target { margin-bottom: 10px; }
  .seg-gif-badge { display: flex; align-items: center; gap: 6px; margin-top: 6px; background: var(--border); border-radius: 6px; padding: 4px 6px; }
  .seg-gif-badge img { width: 36px; height: 36px; object-fit: cover; border-radius: 4px; }
  .seg-gif-badge .gif-remove { background: var(--accent); border: none; color: white; border-radius: 50%; width: 16px; height: 16px; font-size: 10px; cursor: pointer; margin-left: auto; flex-shrink: 0; }
  .gif-overlay-item { position: absolute; pointer-events: none; border-radius: 4px; overflow: visible; transform-origin: center center; }
  .gif-overlay-item video { width: 100%; height: auto; display: block; border-radius: 4px; }

  @keyframes anim-fadein  { from { opacity:0 } to { opacity:1 } }
  @keyframes anim-slideup { from { opacity:0; transform:translateY(40px) } to { opacity:1; transform:translateY(0) } }
  @keyframes anim-bounceup { 0%{opacity:0;transform:translateY(50px)} 60%{transform:translateY(-12px)} 80%{transform:translateY(6px)} 100%{opacity:1;transform:translateY(0)} }
  @keyframes anim-zoomin  { from { opacity:0; transform:scale(0.2) } to { opacity:1; transform:scale(1) } }
  @keyframes anim-spin    { from { opacity:0; transform:rotate(-180deg) scale(0.2) } to { opacity:1; transform:rotate(0deg) scale(1) } }
  @keyframes anim-shake   { 0%,100%{transform:translateX(0)} 20%{transform:translateX(-10px)} 40%{transform:translateX(10px)} 60%{transform:translateX(-6px)} 80%{transform:translateX(6px)} }
  .gif-anim-fadein   { animation: anim-fadein   0.35s ease-out both }
  .gif-anim-slideup  { animation: anim-slideup  0.35s ease-out both }
  .gif-anim-bounceup { animation: anim-bounceup 0.5s  ease-out both }
  .gif-anim-zoomin   { animation: anim-zoomin   0.35s ease-out both }
  .gif-anim-spin     { animation: anim-spin     0.45s ease-out both }
  .gif-anim-shake    { animation: anim-shake    0.5s  ease-in-out both }

  /* Crop */
  .crop-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; display: none; border-radius: var(--radius); }
  .crop-darken { position: absolute; inset: 0; background: rgba(0,0,0,0.55); }
  .crop-window { position: absolute; background: transparent; box-shadow: 0 0 0 9999px rgba(0,0,0,0.55); border: 2px solid var(--accent2); box-sizing: border-box; }
  .crop-label { position: absolute; top: 6px; left: 50%; transform: translateX(-50%); background: var(--accent2); color: #000; font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 4px; white-space: nowrap; }
  .crop-aspect-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
  .crop-chip { padding: 6px 10px; border-radius: 20px; font-size: 12px; font-weight: 700; border: 1px solid var(--border); cursor: pointer; background: var(--surface2); transition: all 0.15s; }
  .crop-chip:hover { border-color: var(--accent2); }
  .crop-chip.active { background: var(--accent); border-color: var(--accent); color: white; }
  .crop-export-btn { margin-top: 12px; }
  .gif-search-section { margin-top: 10px; }

  /* Panel tabs */
  .panel-tabs { display:flex; gap:2px; }
  .panel-tab { background:none; border:none; border-bottom:2px solid transparent; color:var(--muted); cursor:pointer; font-size:12px; font-weight:600; padding:4px 10px; transition:all 0.15s; white-space:nowrap; }
  .panel-tab:hover { color:var(--text); }
  .panel-tab.active { color:var(--text); border-bottom-color:var(--accent); }
  #reaction-panel { flex:1; overflow-y:auto; padding:8px 10px; flex-direction:column; gap:6px; }

  /* Reaction editor panel */
  .re-item { display:grid; grid-template-columns:auto 1fr 1fr auto; gap:6px; align-items:center; background:var(--surface2); border-radius:8px; padding:7px 10px; font-size:12px; }
  .re-item-emoji { background:rgba(254,232,0,.1); border-color:rgba(254,232,0,.3); }
  .re-item-gif   { background:rgba(100,180,255,.07); border-color:rgba(100,180,255,.3); }
  .re-badge { font-size:18px; min-width:28px; text-align:center; }
  .re-field { display:flex; flex-direction:column; gap:3px; }
  .re-field label { font-size:10px; color:var(--muted); }
  .re-field input[type=number] { width:64px; background:var(--bg); border:1px solid var(--border); border-radius:5px; color:var(--text); padding:2px 5px; font-size:12px; }
  .re-field input[type=range] { width:80px; }
  .re-field select { background:var(--bg); border:1px solid var(--border); border-radius:5px; color:var(--text); padding:2px 4px; font-size:11px; }
  .re-row2 { grid-column:1/-1; display:flex; gap:8px; flex-wrap:wrap; align-items:center; }
  .re-del { background:none; border:none; color:var(--muted); cursor:pointer; font-size:16px; padding:0 4px; line-height:1; }
  .re-del:hover { color:var(--accent); }
  .gif-position-label { margin-top: 10px; }
  .gif-show-toggle-row { margin-top: 10px; }
  .giphy-key-input { margin-top: 10px; font-size: 11px; opacity: 0.6; }
  .crop-toggle-row { margin-top: 10px; }

  /* AI Highlights */
  .hl-actions { display: flex; gap: 6px; margin-top: 12px; }
  .hl-actions .btn { flex: 1; padding: 10px 8px; font-size: 12px; }
  .hl-divider { height: 1px; background: var(--border); margin: 14px 0; }
  .hl-claude-row { display: flex; gap: 6px; margin-bottom: 8px; }
  .hl-claude-row input { flex: 1; }
  .hl-claude-btn { padding: 8px 10px; font-size: 12px; }
  .hl-export-btn { margin-top: 12px; }
  .hl-stats { font-size: 11px; color: var(--accent2); margin-top: 8px; min-height: 16px; text-align: center; }

  /* Subtitle import zone */
  .sub-upload-zone {
    border: 1px dashed var(--border); border-radius: var(--radius);
    padding: 14px 12px; text-align: center; cursor: pointer;
    background: var(--surface2); transition: all 0.2s; font-size: 12px;
    color: var(--muted);
  }
  .sub-upload-zone:hover { border-color: var(--accent2); color: var(--accent2); }
  .sub-upload-zone input { display: none; }

  /* Edit mode */
  .segments-header .edit-toggle-btn {
    padding: 3px 10px; font-size: 11px; font-weight: 700; border-radius: 6px;
    border: 1px solid var(--border); background: var(--surface2); color: var(--muted);
    cursor: pointer; transition: all 0.15s;
  }
  .segments-header .edit-toggle-btn:hover { border-color: var(--accent2); color: var(--accent2); }
  .segments-header .edit-toggle-btn.active { background: var(--accent); border-color: var(--accent); color: white; }

  .seg-edit-time-row { display: flex; align-items: center; gap: 4px; margin-bottom: 6px; }
  .seg-edit-time-row input {
    width: 70px; background: var(--surface); border: 1px solid var(--border);
    color: var(--accent2); border-radius: 6px; padding: 3px 6px; font-size: 11px;
    font-family: monospace; text-align: center; outline: none;
  }
  .seg-edit-time-row input:focus { border-color: var(--accent2); }
  .seg-edit-time-row span { color: var(--muted); font-size: 11px; }
  .seg-edit-delete-btn {
    margin-left: auto; background: none; border: 1px solid var(--border);
    color: var(--muted); border-radius: 6px; padding: 2px 7px; font-size: 13px;
    cursor: pointer; transition: all 0.15s; flex-shrink: 0;
  }
  .seg-edit-delete-btn:hover { border-color: var(--accent); color: var(--accent); }
  textarea.seg-edit-text {
    width: 100%; background: var(--surface); border: 1px solid var(--border);
    color: var(--text); border-radius: 6px; padding: 6px 8px; font-size: 13px;
    resize: vertical; outline: none; font-family: inherit; min-height: 52px; line-height: 1.4;
  }
  textarea.seg-edit-text:focus { border-color: var(--accent2); }
  .seg-add-btn {
    display: block; width: 100%; margin-top: 4px; padding: 4px;
    background: none; border: 1px dashed var(--border); color: var(--muted);
    border-radius: 6px; font-size: 11px; cursor: pointer; transition: all 0.15s;
  }
  .seg-add-btn:hover { border-color: var(--accent2); color: var(--accent2); }

  /* Timeline */
  .timeline-bar {
    border-top: 1px solid var(--border); background: #060609;
    display: flex; flex-direction: column; flex-shrink: 0; height: 210px;
  }
  .timeline-header {
    display: flex; align-items: center; justify-content: space-between;
    padding: 5px 14px; border-bottom: 1px solid var(--border); height: 28px; flex-shrink: 0;
  }
  .timeline-zoom-controls { display: flex; align-items: center; gap: 5px; }
  .tl-btn {
    background: var(--surface2); border: 1px solid var(--border); color: var(--muted);
    border-radius: 5px; padding: 1px 8px; font-size: 14px; cursor: pointer;
    line-height: 1.5; transition: all 0.15s;
  }
  .tl-btn:hover { border-color: var(--accent2); color: var(--accent2); }
  #timeline-canvas { flex: 1; width: 100%; display: block; cursor: crosshair; }

  /* Segment score bar */
  .seg-score-row { display: flex; align-items: center; gap: 6px; margin-top: 6px; }
  .seg-score-bar-bg { flex: 1; height: 4px; background: var(--border); border-radius: 2px; overflow: hidden; cursor: pointer; }
  .seg-score-bar-fill { height: 100%; border-radius: 2px; transition: width 0.3s; }
  .seg-score-pct { font-size: 10px; color: var(--muted); width: 30px; text-align: right; flex-shrink: 0; }
  .seg-include-toggle { width: 14px; height: 14px; border-radius: 3px; border: 1.5px solid var(--muted); cursor: pointer; flex-shrink: 0; display: flex; align-items: center; justify-content: center; transition: all 0.15s; }
  .seg-include-toggle.included { background: #22c55e; border-color: #22c55e; }
  .seg-include-toggle.included::after { content: '✓'; font-size: 9px; color: white; }
  .segment-card.hl-excluded { opacity: 0.45; }
</style>
</head>
<body>

<header>
  <div>
    <div class="logo">SubTok</div>
    <div class="logo-sub">TikTok-style subtitles with local Whisper</div>
  </div>
</header>
<div id="libass-warning" style="display:none;background:#7c2d12;color:#fef3c7;padding:8px 20px;font-size:12px;border-bottom:1px solid #92400e;">
  ⚠ ffmpeg saknar libass — video-export använder drawtext (enklare stil, ingen karaoke).
  Kör <strong>Debian: sudo apt install ffmpeg libass-dev / Mac: brew install ffmpeg</strong> och starta om appen för full styling.
  <button onclick="document.getElementById('libass-warning').style.display='none'" style="float:right;background:none;border:none;color:inherit;cursor:pointer;font-size:14px;">×</button>
</div>

<div class="layout">
  <!-- Sidebar -->
  <div class="sidebar">

    <!-- Upload -->
    <div class="section">
      <div class="section-title">Video</div>
      <div class="upload-zone" id="upload-zone" onclick="document.getElementById('file-input').click()">
        <div class="upload-icon">🎬</div>
        <div class="upload-label">Drop video here</div>
        <div class="upload-sub">MP4, MOV, MKV, WebM</div>
        <input type="file" id="file-input" accept="video/*">
      </div>
      <div id="file-name" style="font-size:12px;color:var(--muted);margin-top:8px;text-align:center;"></div>
    </div>

    <!-- Import subtitles -->
    <div class="section">
      <div class="section-title">Import Subtitles</div>
      <div class="sub-upload-zone" onclick="document.getElementById('sub-file-input').click()">
        📄 Upload SRT or ASS file
        <input type="file" id="sub-file-input" accept=".srt,.ass,.ssa">
      </div>
    </div>

    <!-- Whisper settings -->
    <div class="section">
      <div class="section-title">Whisper</div>
      <label>Model</label>
      <select id="model-select">
        <option value="tiny">Tiny — fastest</option>
        <option value="base" selected>Base — balanced</option>
        <option value="small">Small — better</option>
        <option value="medium">Medium — accurate</option>
        <option value="large">Large — best</option>
      </select>
      <label>Language</label>
      <select id="lang-select">
        <option value="auto">Auto detect</option>
        <option value="sv">Swedish</option>
        <option value="en">English</option>
        <option value="de">German</option>
        <option value="fr">French</option>
        <option value="es">Spanish</option>
        <option value="pt">Portuguese</option>
        <option value="it">Italian</option>
        <option value="ja">Japanese</option>
        <option value="ko">Korean</option>
        <option value="zh">Chinese</option>
      </select>
      <div style="margin-top:14px;">
        <button class="btn btn-primary" id="transcribe-btn" disabled onclick="transcribe()">
          <span>🎙</span> Transcribe
        </button>
      </div>
      <div class="progress-bar" id="progress-bar" style="display:none">
        <div class="progress-fill pulsing" id="progress-fill" style="width:100%"></div>
      </div>
      <div class="status-text" id="status-text"></div>
    </div>

    <!-- Style presets -->
    <div class="section">
      <div class="section-title">Style Preset</div>
      <div class="presets">
        <div class="preset-chip active" onclick="applyPreset('tiktok')">TikTok</div>
        <div class="preset-chip" onclick="applyPreset('viral')">Viral White</div>
        <div class="preset-chip" onclick="applyPreset('neon')">Neon</div>
        <div class="preset-chip" onclick="applyPreset('minimal')">Minimal</div>
        <div class="preset-chip" onclick="applyPreset('fire')">Fire</div>
        <div class="preset-chip" onclick="applyPreset('shadow')">Shadow</div>
      </div>
    </div>

    <!-- Style settings -->
    <div class="section">
      <div class="section-title">Style</div>

      <label>Font</label>
      <select id="font-select" onchange="document.fonts.load(`900 52px '${this.value}'`).then(()=>updatePreview())">
        <optgroup label="── Hype / TikTok ──">
          <option value="Arial Black">Arial Black</option>
          <option value="Impact">Impact</option>
          <option value="Anton">Anton</option>
          <option value="Bebas Neue">Bebas Neue</option>
          <option value="Bangers">Bangers</option>
          <option value="Lilita One">Lilita One</option>
          <option value="Titan One">Titan One</option>
        </optgroup>
        <optgroup label="── Kondenserad ──">
          <option value="Oswald">Oswald</option>
          <option value="Teko">Teko</option>
          <option value="Montserrat">Montserrat</option>
          <option value="Russo One">Russo One</option>
        </optgroup>
        <optgroup label="── Lekfull / Rund ──">
          <option value="Fredoka One">Fredoka One</option>
          <option value="Righteous">Righteous</option>
          <option value="Boogaloo">Boogaloo</option>
          <option value="Arial Rounded MT Bold">Arial Rounded</option>
        </optgroup>
        <optgroup label="── Skript / Handstil ──">
          <option value="Pacifico">Pacifico</option>
          <option value="Lobster">Lobster</option>
          <option value="Permanent Marker">Permanent Marker</option>
          <option value="Cabin Sketch">Cabin Sketch</option>
        </optgroup>
        <optgroup label="── Elegant / Serif ──">
          <option value="Playfair Display">Playfair Display</option>
          <option value="Cinzel">Cinzel</option>
          <option value="Georgia">Georgia</option>
          <option value="Helvetica Neue">Helvetica Neue</option>
        </optgroup>
        <optgroup label="── Special ──">
          <option value="Press Start 2P">Press Start 2P</option>
        </optgroup>
      </select>

      <label>Font Size: <span id="fontsize-val">52</span>px</label>
      <input type="range" id="fontsize" min="20" max="120" value="52" oninput="document.getElementById('fontsize-val').textContent=this.value;updatePreview()">

      <label>Words per chunk</label>
      <select id="words-per-chunk" onchange="updatePreview()">
        <option value="1">1 word</option>
        <option value="2">2 words</option>
        <option value="3" selected>3 words</option>
        <option value="4">4 words</option>
        <option value="5">5 words</option>
      </select>

      <label>Text Color</label>
      <div class="color-row">
        <input type="color" id="text-color" value="#ffffff" onchange="updatePreview()">
        <span>Main text</span>
      </div>

      <label>Highlight Color</label>
      <div class="color-row">
        <input type="color" id="highlight-color" value="#FEE800" onchange="updatePreview()">
        <span>Active word</span>
      </div>

      <label>Outline Color</label>
      <div class="color-row">
        <input type="color" id="outline-color" value="#000000" onchange="updatePreview()">
        <span>Stroke</span>
      </div>

      <label>Outline Width: <span id="outline-val">4</span>px</label>
      <input type="range" id="outline-width" min="0" max="12" value="4" oninput="document.getElementById('outline-val').textContent=this.value;updatePreview()">

      <label>Vertical: <span id="pos-val">85</span>%</label>
      <input type="range" id="position" min="10" max="98" value="85" oninput="document.getElementById('pos-val').textContent=this.value;updatePreview()">

      <label>Horizontal: <span id="pos-x-val">50</span>%</label>
      <input type="range" id="position-x" min="5" max="95" value="50" oninput="document.getElementById('pos-x-val').textContent=this.value;updatePreview()">

      <div class="toggle-row">
        <span class="toggle-label">Uppercase</span>
        <div class="toggle on" id="uppercase-toggle" onclick="this.classList.toggle('on');updatePreview()"></div>
      </div>
      <div class="toggle-row">
        <span class="toggle-label">Word highlight</span>
        <div class="toggle on" id="highlight-toggle" onclick="this.classList.toggle('on');updatePreview()"></div>
      </div>
    </div>

    <!-- Emoji Reactions -->
    <div class="section">
      <div class="section-title">Emoji Reactions</div>
      <button type="button" class="btn btn-secondary emoji-autodetect-btn" onclick="autoDetectEmojis()">✨ Auto-detect</button>
      <label for="emoji-position">Position</label>
      <select id="emoji-position" title="Emoji position relative to subtitle" onchange="updatePreview()">
        <option value="above">Ovanför text</option>
        <option value="below" selected>Under text</option>
        <option value="right">Till höger om text</option>
      </select>
      <label for="emoji-effect">Effekt</label>
      <select id="emoji-effect" title="Emoji-animationseffekt" onchange="updatePreview()">
        <option value="none">Ingen</option>
        <option value="fadein">Tona fram</option>
        <option value="bounceup" selected>Studsa upp</option>
        <option value="zoomin">Zooma in</option>
        <option value="shake">Skaka</option>
      </select>
      <label for="emoji-size">Size: <span id="emoji-size-val">48</span>px</label>
      <input type="range" id="emoji-size" title="Emoji size in pixels" min="20" max="120" value="48" oninput="document.getElementById('emoji-size-val').textContent=this.value;updatePreview()">
      <div class="toggle-row emoji-toggle-row">
        <span class="toggle-label">Show emojis</span>
        <div class="toggle on" id="emoji-toggle" onclick="this.classList.toggle('on');updatePreview()"></div>
      </div>
    </div>

    <!-- GIF Reactions -->
    <div class="section">
      <div class="section-title">GIF Reactions</div>
      <label for="gif-assign-target" class="gif-assign-target">Assign to segment</label>
      <select id="gif-assign-target" title="Target segment for GIF assignment"></select>
      <div class="gif-search-row gif-search-section">
        <input type="text" id="gif-search" placeholder="Search GIFs…" title="Search GIFs" onkeydown="if(event.key==='Enter')searchGifs()">
        <button type="button" class="gif-search-btn" onclick="searchGifs()">🔍</button>
      </div>
      <div class="gif-results" id="gif-results"></div>
      <label>Effekt</label>
      <select id="gif-effect" title="GIF-animationseffekt" onchange="updateGifOverlay(document.getElementById('video-player').currentTime)">
        <option value="none">Ingen</option>
        <option value="fadein">Tona fram</option>
        <option value="slideup" selected>Glid upp</option>
        <option value="bounceup">Studsa upp</option>
        <option value="zoomin">Zooma in</option>
        <option value="spin">Snurra in</option>
        <option value="shake">Skaka</option>
      </select>
      <label>Position X: <span id="gif-pos-x-val">50</span>%</label>
      <input type="range" id="gif-pos-x" title="GIF horisontell position" min="5" max="95" value="50" oninput="document.getElementById('gif-pos-x-val').textContent=this.value;updateGifOverlay(document.getElementById('video-player').currentTime)">
      <label>Position Y: <span id="gif-pos-y-val">30</span>%</label>
      <input type="range" id="gif-pos-y" title="GIF vertikal position" min="5" max="95" value="30" oninput="document.getElementById('gif-pos-y-val').textContent=this.value;updateGifOverlay(document.getElementById('video-player').currentTime)">
      <label for="gif-size">Storlek: <span id="gif-size-val">50</span>%</label>
      <input type="range" id="gif-size" min="10" max="100" value="50" oninput="document.getElementById('gif-size-val').textContent=this.value;updateGifOverlay(document.getElementById('video-player').currentTime)">
      <label for="gif-opacity">Opacitet: <span id="gif-opacity-val">90</span>%</label>
      <input type="range" id="gif-opacity" min="10" max="100" value="90" oninput="document.getElementById('gif-opacity-val').textContent=this.value;updateGifOverlay(document.getElementById('video-player').currentTime)">
      <div class="toggle-row gif-show-toggle-row">
        <span class="toggle-label">Visa GIFs</span>
        <div class="toggle on" id="gif-toggle" onclick="this.classList.toggle('on');updateGifOverlay(document.getElementById('video-player').currentTime)"></div>
      </div>
      <input type="text" id="giphy-key" class="giphy-key-input" placeholder="Giphy API key (gratis på developers.giphy.com)" title="Din Giphy API-nyckel">
    </div>

    <!-- Crop to Format -->
    <div class="section">
      <div class="section-title">Crop to Format</div>
      <div class="crop-aspect-chips">
        <div class="crop-chip active" onclick="setCropAspect('9:16',this)">9:16 TikTok</div>
        <div class="crop-chip" onclick="setCropAspect('1:1',this)">1:1 Square</div>
        <div class="crop-chip" onclick="setCropAspect('4:5',this)">4:5 Feed</div>
        <div class="crop-chip" onclick="setCropAspect('16:9',this)">16:9 Wide</div>
      </div>
      <label for="crop-x">Horizontal: <span id="crop-x-val">50</span>%</label>
      <input type="range" id="crop-x" title="Horizontal crop position" min="0" max="100" value="50" oninput="document.getElementById('crop-x-val').textContent=this.value;updateCropOverlay()">
      <label for="crop-y">Vertical: <span id="crop-y-val">50</span>%</label>
      <input type="range" id="crop-y" title="Vertical crop position" min="0" max="100" value="50" oninput="document.getElementById('crop-y-val').textContent=this.value;updateCropOverlay()">
      <div class="toggle-row crop-toggle-row">
        <span class="toggle-label">Preview crop</span>
        <div class="toggle on" id="crop-preview-toggle" onclick="this.classList.toggle('on');toggleCropPreview()"></div>
      </div>
      <button type="button" class="btn btn-primary crop-export-btn" id="crop-export-btn" disabled onclick="exportCropped()">✂️ Export Cropped</button>
    </div>

    <!-- AI Highlights -->
    <div class="section">
      <div class="section-title">AI Highlights</div>
      <label for="hl-target">Target length: <span id="hl-target-val">50</span>%</label>
      <input type="range" id="hl-target" title="Target highlight duration as % of original" min="10" max="90" value="50" oninput="document.getElementById('hl-target-val').textContent=this.value">
      <div class="hl-actions">
        <button type="button" class="btn btn-secondary" id="hl-local-btn" disabled onclick="analyzeHighlights(false)">⚡ Analyze</button>
        <button type="button" class="btn btn-primary" id="hl-export-btn" disabled onclick="exportHighlights()">🎬 Export Reel</button>
      </div>
      <div class="hl-stats" id="hl-stats"></div>
      <div class="hl-divider"></div>
      <label for="hl-claude-key">Anthropic API Key (optional)</label>
      <div class="hl-claude-row">
        <input type="password" id="hl-claude-key" title="Anthropic API key for Claude analysis" placeholder="sk-ant-…">
        <button type="button" class="btn btn-secondary hl-claude-btn" id="hl-claude-btn" onclick="analyzeHighlights(true)">✨ Claude</button>
      </div>
    </div>

  </div>

  <!-- Main -->
  <div class="main">
    <div class="preview-area">

      <!-- Video -->
      <div class="video-panel" id="video-panel"
        ondrop="handleDrop(event)" ondragover="event.preventDefault();document.getElementById('upload-zone').classList.add('drag')"
        ondragleave="document.getElementById('upload-zone').classList.remove('drag')">
        <div class="video-wrapper" id="video-wrapper">
          <div class="video-placeholder" id="video-placeholder">
            <div class="ph-icon">🎞</div>
            <div>Upload a video to start</div>
          </div>
          <video id="video-player" style="display:none" controls></video>
          <canvas id="sub-canvas" style="display:none"></canvas>
          <div id="gif-overlay"></div>
          <div class="crop-overlay" id="crop-overlay">
            <div class="crop-window" id="crop-window">
              <span class="crop-label" id="crop-label">9:16</span>
            </div>
          </div>
        </div>
      </div>

      <!-- Segments / Reactions panel -->
      <div class="segments-panel">
        <div class="segments-header">
          <div class="panel-tabs">
            <button type="button" class="panel-tab active" id="tab-transcript" onclick="switchPanelTab('transcript')">Transcript <span id="lang-badge"></span></button>
            <button type="button" class="panel-tab" id="tab-reactions" onclick="switchPanelTab('reactions')" style="display:none">Emoji &amp; GIF</button>
          </div>
          <div style="display:flex;align-items:center;gap:8px;">
            <span class="seg-count" id="seg-count">0 segments</span>
            <button type="button" class="edit-toggle-btn" id="edit-mode-btn" onclick="toggleEditMode()">Edit</button>
          </div>
        </div>
        <div class="segments-list" id="segments-list">
          <div style="padding:40px 20px;text-align:center;color:var(--muted);font-size:13px;">
            Transcribe a video or import subtitles
          </div>
        </div>
        <div id="reaction-panel" style="display:none;flex:1;overflow-y:auto;padding:8px 10px;display:none;flex-direction:column;gap:6px">
          <div style="display:flex;gap:6px;padding-bottom:6px;border-bottom:1px solid var(--border);margin-bottom:4px">
            <button type="button" class="tl-btn" onclick="addEmojiItemManual()">+ Emoji</button>
            <button type="button" class="tl-btn" onclick="addGifItemManual()">+ GIF</button>
          </div>
          <div id="reaction-list"></div>
        </div>
      </div>
    </div>

    <!-- Timeline (shown in edit mode) -->
    <div class="timeline-bar" id="timeline-bar" style="display:none">
      <div class="timeline-header">
        <span style="font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:1px">Timeline</span>
        <div class="timeline-zoom-controls">
          <button type="button" class="tl-btn" onclick="zoomTimeline(-1)" title="Zoom out">−</button>
          <span id="tl-zoom-label" style="font-size:11px;color:var(--muted);min-width:30px;text-align:center">1×</span>
          <button type="button" class="tl-btn" onclick="zoomTimeline(1)" title="Zoom in">+</button>
          <button type="button" class="tl-btn" onclick="fitTimeline()" style="font-size:10px;padding:1px 8px" title="Fit all">Fit</button>
        </div>
      </div>
      <canvas id="timeline-canvas"></canvas>
    </div>

    <!-- Export bar -->
    <div class="export-bar" id="export-bar">
      <span style="font-size:12px;color:var(--muted);flex:1">Export</span>
      <button class="btn btn-secondary" id="export-srt-btn" disabled onclick="exportSRT()">↓ SRT</button>
      <button class="btn btn-secondary" id="export-ass-btn" disabled onclick="exportASS()">↓ ASS</button>
      <button class="btn btn-primary" id="export-video-btn" disabled onclick="exportVideo()">🔥 Burn to Video</button>
    </div>
  </div>
</div>

<script>
let videoFile = null;
let segments = [];
let currentTime = 0;
let animFrameId = null;
let segmentEmojis = {};
let emojiPickerTarget = null;
let segmentGifs = {};
let cropAspect = '9:16';
let highlightScores = [];
let highlightSelected = new Set();
let editMode = false;

// Timeline state
let waveformData = null;   // { peaks: Float32Array, duration, numPeaks }
let tlZoom = 100;          // px/sec
let tlOffset = 0;          // seconds at left edge
let tlDrag = null;         // active drag: { type, segIdx, startX, origStart, origEnd }
let tlHoverSeg = -1;
let tlRafId = null;

// Check ffmpeg/libass status on load
fetch('/api/status').then(r => r.json()).then(s => {
  if (!s.libass) document.getElementById('libass-warning').style.display = 'block';
}).catch(() => {});

// Apply each font to its own <option> so the dropdown previews them visually
document.fonts.ready.then(() => {
  document.querySelectorAll('#font-select option[value]').forEach(opt => {
    const f = opt.value;
    if (f) opt.style.fontFamily = `"${f}", sans-serif`;
  });
});

// ── File handling ──────────────────────────────────────────────
document.getElementById('file-input').addEventListener('change', e => {
  const f = e.target.files[0];
  if (f) loadVideo(f);
});

document.getElementById('sub-file-input').addEventListener('change', e => {
  const f = e.target.files[0];
  if (f) importSubtitles(f);
  e.target.value = '';
});

async function importSubtitles(f) {
  const zone = document.querySelector('.sub-upload-zone');
  zone.textContent = '⏳ Importing…';
  const fd = new FormData();
  fd.append('file', f);
  try {
    const res = await fetch('/import/subtitles', { method: 'POST', body: fd });
    if (!res.ok) throw new Error(await res.text());
    const data = await res.json();
    segments = data.segments;
    segmentEmojis = {};
    segmentGifs = {};
    highlightScores = [];
    highlightSelected = new Set();
    renderSegmentsList();
    refreshGifTargetDropdown();
    document.getElementById('hl-local-btn').disabled = false;
    enableExports();
    showLangBadge(data.language !== 'unknown' ? data.language : '');
    setStatus(`Imported ${segments.length} subtitles from ${f.name}`);
  } catch (err) {
    setStatus('Import error: ' + err.message);
    alert('Import failed: ' + err.message);
  }
  zone.innerHTML = '📄 Upload SRT or ASS file';
}

function handleDrop(e) {
  e.preventDefault();
  document.getElementById('upload-zone').classList.remove('drag');
  const f = e.dataTransfer.files[0];
  if (f && f.type.startsWith('video/')) loadVideo(f);
}

function loadVideo(f) {
  videoFile = f;
  document.getElementById('file-name').textContent = f.name;
  document.getElementById('transcribe-btn').disabled = false;
  const url = URL.createObjectURL(f);
  const vid = document.getElementById('video-player');
  vid.src = url;
  vid.style.display = 'block';
  document.getElementById('video-placeholder').style.display = 'none';
  document.getElementById('sub-canvas').style.display = 'block';
  vid.addEventListener('loadedmetadata', () => resizeCanvas());
  vid.addEventListener('timeupdate', () => {
    renderSubtitles(vid.currentTime);
    updateGifOverlay(vid.currentTime);
  });
  vid.addEventListener('play', () => { if (editMode) startTlRaf(); });
  vid.addEventListener('pause', () => stopTlRaf());
  vid.addEventListener('ended', () => stopTlRaf());
  window.addEventListener('resize', () => {
    resizeCanvas();
    updateCropOverlay();
    if (document.getElementById('timeline-bar').style.display !== 'none') {
      const bar = document.getElementById('timeline-bar');
      const canvas = document.getElementById('timeline-canvas');
      canvas.width = bar.clientWidth;
      canvas.height = bar.clientHeight - 28;
      drawTimeline();
    }
  });
  document.getElementById('crop-export-btn').disabled = false;
  generateWaveform(f);
}

function resizeCanvas() {
  const vid = document.getElementById('video-player');
  const canvas = document.getElementById('sub-canvas');
  canvas.width = vid.videoWidth || vid.offsetWidth;
  canvas.height = vid.videoHeight || vid.offsetHeight;
  canvas.style.width = vid.offsetWidth + 'px';
  canvas.style.height = vid.offsetHeight + 'px';
}

// ── Transcription ─────────────────────────────────────────────
async function transcribe() {
  if (!videoFile) return;
  const btn = document.getElementById('transcribe-btn');
  btn.disabled = true;
  document.getElementById('progress-bar').style.display = 'block';
  setStatus('Loading Whisper model & transcribing… (may take a minute)');

  const fd = new FormData();
  fd.append('file', videoFile);
  fd.append('model', document.getElementById('model-select').value);
  fd.append('language', document.getElementById('lang-select').value);

  try {
    const res = await fetch('/transcribe', { method: 'POST', body: fd });
    if (!res.ok) throw new Error(await res.text());
    const data = await res.json();
    segments = data.segments;
    segmentEmojis = {};
    segmentGifs = {};
    highlightScores = [];
    highlightSelected = new Set();
    renderSegmentsList();
    refreshGifTargetDropdown();
    document.getElementById('hl-local-btn').disabled = false;
    enableExports();
    setStatus(`Done! Detected language: ${data.language}`);
    showLangBadge(data.language);
    document.getElementById('progress-bar').style.display = 'none';
  } catch(err) {
    setStatus('Error: ' + err.message);
    document.getElementById('progress-bar').style.display = 'none';
  }
  btn.disabled = false;
}

function setStatus(msg) {
  document.getElementById('status-text').textContent = msg;
}

function showLangBadge(lang) {
  const el = document.getElementById('lang-badge');
  el.innerHTML = `<span class="lang-badge">${lang}</span>`;
}

// ── Segments list ─────────────────────────────────────────────
function renderSegmentsList() {
  const list = document.getElementById('segments-list');
  document.getElementById('seg-count').textContent = `${segments.length} segments`;
  list.innerHTML = '';

  if (!segments.length) {
    list.innerHTML = '<div style="padding:40px 20px;text-align:center;color:var(--muted);font-size:13px;">Transcribe a video or import subtitles</div>';
    return;
  }

  segments.forEach((seg, i) => {
    const card = document.createElement('div');
    card.className = 'segment-card';
    card.id = `seg-${i}`;

    if (editMode) {
      card.innerHTML = `
        <div class="seg-edit-time-row">
          <input type="text" value="${fmt(seg.start)}" title="Start time (m:ss.s)"
            onblur="updateSegmentTime(${i},'start',this.value)"
            onkeydown="if(event.key==='Enter')this.blur()">
          <span>→</span>
          <input type="text" value="${fmt(seg.end)}" title="End time (m:ss.s)"
            onblur="updateSegmentTime(${i},'end',this.value)"
            onkeydown="if(event.key==='Enter')this.blur()">
          <button type="button" class="seg-edit-delete-btn" onclick="deleteSegment(${i})" title="Delete segment">🗑</button>
        </div>
        <textarea class="seg-edit-text" oninput="updateSegmentText(${i},this.value)">${seg.text}</textarea>
        <button type="button" class="seg-add-btn" onclick="addSegmentAfter(${i})">+ Add segment after</button>`;
      list.appendChild(card);
      return;
    }

    const ts = `${fmt(seg.start)} → ${fmt(seg.end)}`;
    const wordsHtml = (seg.words || []).map(w =>
      `<span class="seg-word" data-start="${w.start}" data-end="${w.end}">${w.word.trim()}</span>`
    ).join('');
    const emojis = segmentEmojis[i] || [];
    const emojiHtml = emojis.map(e =>
      `<span class="seg-emoji-badge" onclick="event.stopPropagation();removeEmoji(${i},'${e}')" title="Click to remove">
        ${e}<span class="remove-emoji">×</span>
      </span>`
    ).join('');
    const gif = segmentGifs[i];
    const gifHtml = gif
      ? `<div class="seg-gif-badge"><img src="${gif.preview}" alt="GIF"><span style="font-size:11px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1">${gif.title||'GIF'}</span><button type="button" class="gif-remove" onclick="event.stopPropagation();removeSegmentGif(${i})">×</button></div>`
      : '';

    const score = highlightScores[i];
    const hasScores = highlightScores.length > 0;
    const included = highlightSelected.has(i);
    const barColor = included ? '#22c55e' : '#6b7280';
    const scorePct = hasScores ? Math.round(score * 100) : 0;
    const scoreHtml = hasScores ? `
      <div class="seg-score-row" onclick="event.stopPropagation()">
        <div class="seg-include-toggle ${included ? 'included' : ''}" onclick="toggleHighlightSegment(${i})" title="Toggle include in highlight reel"></div>
        <div class="seg-score-bar-bg" onclick="toggleHighlightSegment(${i})">
          <div class="seg-score-bar-fill" style="width:${scorePct}%;background:${barColor}"></div>
        </div>
        <span class="seg-score-pct">${scorePct}%</span>
      </div>` : '';

    card.innerHTML = `
      <div class="seg-time">${ts}</div>
      <div class="seg-text">${seg.text}</div>
      <div class="seg-words">${wordsHtml}</div>
      <div class="seg-emoji-row" id="emoji-row-${i}">
        ${emojiHtml}
        <button type="button" class="add-emoji-btn" onclick="event.stopPropagation();openEmojiPicker(${i},this)" title="Add emoji reaction">+ 😀</button>
      </div>
      <div id="gif-badge-${i}">${gifHtml}</div>
      ${scoreHtml}`;
    if (hasScores) card.classList.toggle('hl-excluded', !included);
    card.onclick = () => {
      document.getElementById('video-player').currentTime = seg.start;
    };
    list.appendChild(card);
  });
}

// ── Edit mode ─────────────────────────────────────────────────
function toggleEditMode() {
  editMode = !editMode;
  const btn = document.getElementById('edit-mode-btn');
  btn.classList.toggle('active', editMode);
  btn.textContent = editMode ? 'Done' : 'Edit';
  const bar = document.getElementById('timeline-bar');
  bar.style.display = editMode ? 'flex' : 'none';
  document.getElementById('tab-reactions').style.display = editMode ? '' : 'none';
  if (editMode) {
    requestAnimationFrame(() => initTimeline());
    const vid = document.getElementById('video-player');
    if (vid.style.display !== 'none' && !vid.paused) startTlRaf();
    renderReactionList();
  } else {
    stopTlRaf();
    switchPanelTab('transcript');
  }
  renderSegmentsList();
}

function switchPanelTab(tab) {
  const isTranscript = tab === 'transcript';
  document.getElementById('segments-list').style.display  = isTranscript ? '' : 'none';
  document.getElementById('reaction-panel').style.display = isTranscript ? 'none' : 'flex';
  document.getElementById('tab-transcript').classList.toggle('active', isTranscript);
  document.getElementById('tab-reactions').classList.toggle('active', !isTranscript);
  if (!isTranscript) renderReactionList();
}

function parseFmt(str) {
  const parts = str.trim().split(':');
  if (parts.length !== 2) return NaN;
  return parseInt(parts[0], 10) * 60 + parseFloat(parts[1]);
}

function distributeWords(text, start, end) {
  const words = text.trim().split(/\s+/).filter(Boolean);
  if (!words.length) return [];
  const dur = (end - start) / words.length;
  return words.map((w, i) => ({
    word: w,
    start: Math.round((start + i * dur) * 1000) / 1000,
    end: Math.round((start + (i + 1) * dur) * 1000) / 1000,
  }));
}

function updateSegmentText(i, text) {
  segments[i].text = text;
  segments[i].words = distributeWords(text, segments[i].start, segments[i].end);
  updatePreview();
  drawTimeline();
}

function updateSegmentTime(i, field, value) {
  const t = parseFmt(value);
  if (isNaN(t) || t < 0) return;
  segments[i][field] = Math.round(t * 1000) / 1000;
  segments[i].words = distributeWords(segments[i].text, segments[i].start, segments[i].end);
  // Refresh just the time input to show normalised value
  const card = document.getElementById(`seg-${i}`);
  if (card) {
    const inputs = card.querySelectorAll('.seg-edit-time-row input');
    if (field === 'start' && inputs[0]) inputs[0].value = fmt(segments[i].start);
    if (field === 'end' && inputs[1]) inputs[1].value = fmt(segments[i].end);
  }
  updatePreview();
  drawTimeline();
}

function deleteSegment(i) {
  segments.splice(i, 1);
  // Re-index ids
  segments.forEach((s, idx) => { s.id = idx; });
  // Shift emoji/gif maps
  const newEmojis = {}, newGifs = {};
  Object.entries(segmentEmojis).forEach(([k, v]) => { const n = parseInt(k); if (n < i) newEmojis[n] = v; else if (n > i) newEmojis[n-1] = v; });
  Object.entries(segmentGifs).forEach(([k, v]) => { const n = parseInt(k); if (n < i) newGifs[n] = v; else if (n > i) newGifs[n-1] = v; });
  segmentEmojis = newEmojis;
  segmentGifs = newGifs;
  highlightScores = [];
  highlightSelected = new Set();
  renderSegmentsList();
  refreshGifTargetDropdown();
  updatePreview();
}

function addSegmentAfter(i) {
  const prevEnd = segments[i].end;
  const nextStart = segments[i + 1]?.start ?? prevEnd + 3;
  const newSeg = {
    id: i + 1,
    start: Math.round(prevEnd * 1000) / 1000,
    end: Math.round(Math.min(prevEnd + 2, nextStart) * 1000) / 1000,
    text: '',
    words: [],
  };
  segments.splice(i + 1, 0, newSeg);
  segments.forEach((s, idx) => { s.id = idx; });
  // Shift emoji/gif maps for indices > i
  const newEmojis = {}, newGifs = {};
  Object.entries(segmentEmojis).forEach(([k, v]) => { const n = parseInt(k); newEmojis[n <= i ? n : n + 1] = v; });
  Object.entries(segmentGifs).forEach(([k, v]) => { const n = parseInt(k); newGifs[n <= i ? n : n + 1] = v; });
  segmentEmojis = newEmojis;
  segmentGifs = newGifs;
  highlightScores = [];
  highlightSelected = new Set();
  renderSegmentsList();
  refreshGifTargetDropdown();
  // Focus the new card's textarea
  setTimeout(() => {
    const card = document.getElementById(`seg-${i + 1}`);
    if (card) { card.querySelector('textarea')?.focus(); card.scrollIntoView({ block: 'nearest' }); }
  }, 50);
}

function fmt(s) {
  const m = Math.floor(s/60), sec = (s%60).toFixed(1).padStart(4,'0');
  return `${m}:${sec}`;
}

// ── Subtitle rendering on canvas ──────────────────────────────
function getStyle() {
  return {
    font: document.getElementById('font-select').value,
    fontSize: parseInt(document.getElementById('fontsize').value),
    textColor: document.getElementById('text-color').value,
    highlightColor: document.getElementById('highlight-color').value,
    outlineColor: document.getElementById('outline-color').value,
    outlineWidth: parseInt(document.getElementById('outline-width').value),
    position: parseInt(document.getElementById('position').value) / 100,
    positionX: parseInt(document.getElementById('position-x').value) / 100,
    uppercase: document.getElementById('uppercase-toggle').classList.contains('on'),
    highlightEnabled: document.getElementById('highlight-toggle').classList.contains('on'),
    wordsPerChunk: parseInt(document.getElementById('words-per-chunk').value),
  };
}

function renderSubtitles(time) {
  const canvas = document.getElementById('sub-canvas');
  const ctx = canvas.getContext('2d');
  const vid = document.getElementById('video-player');
  if (!canvas.width) resizeCanvas();
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  if (!segments.length) return;

  const s = getStyle();
  const wpc = s.wordsPerChunk;

  // Collect all word-chunks
  const chunks = [];
  for (const seg of segments) {
    const words = seg.words || [];
    if (!words.length) {
      chunks.push({ words: [{ word: seg.text, start: seg.start, end: seg.end }], start: seg.start, end: seg.end });
      continue;
    }
    for (let i = 0; i < words.length; i += wpc) {
      const chunk = words.slice(i, i + wpc);
      chunks.push({ words: chunk, start: chunk[0].start, end: chunk[chunk.length-1].end });
    }
  }

  const active = chunks.find(c => time >= c.start && time <= c.end + 0.05);
  if (!active) {
    updateActiveSegment(time);
    return;
  }

  const scaleFactor = canvas.width / (vid.videoWidth || canvas.width);
  const fontSize = Math.round(s.fontSize * scaleFactor * (canvas.width / 1080));
  ctx.font = `900 ${fontSize}px "${s.font}"`;
  ctx.textAlign = 'center';
  ctx.textBaseline = 'bottom';

  const y = canvas.height * s.position;
  const cx = canvas.width * s.positionX;

  if (s.highlightEnabled) {
    // Render word by word with highlight
    const wordTexts = active.words.map(w => s.uppercase ? w.word.toUpperCase() : w.word);
    const fullText = wordTexts.join(' ');
    const totalW = ctx.measureText(fullText).width;
    let x = cx - totalW / 2;

    active.words.forEach((w, idx) => {
      const txt = s.uppercase ? w.word.toUpperCase() : w.word;
      const isActive = time >= w.start && time <= w.end + 0.05;
      const wordW = ctx.measureText(txt).width;
      const spaceW = idx < active.words.length - 1 ? ctx.measureText(' ').width : 0;
      const wordCx = x + wordW / 2;

      ctx.lineWidth = s.outlineWidth * scaleFactor;
      ctx.strokeStyle = s.outlineColor;
      ctx.strokeText(txt, wordCx, y);
      ctx.fillStyle = isActive ? s.highlightColor : s.textColor;
      ctx.fillText(txt, wordCx, y);
      x += wordW + spaceW;
    });
  } else {
    const txt = s.uppercase
      ? active.words.map(w => w.word).join(' ').toUpperCase()
      : active.words.map(w => w.word).join(' ');
    ctx.lineWidth = s.outlineWidth * scaleFactor;
    ctx.strokeStyle = s.outlineColor;
    ctx.strokeText(txt, cx, y);
    ctx.fillStyle = s.textColor;
    ctx.fillText(txt, cx, y);
  }

  // Emoji overlay
  const emojiOn = document.getElementById('emoji-toggle').classList.contains('on');
  if (emojiOn) {
    const activeSeg = segments.findIndex(sg => time >= sg.start && time <= sg.end + 0.05);
    const emojis = activeSeg >= 0 ? (segmentEmojis[activeSeg] || []) : [];
    if (emojis.length) {
      const emojiPx   = parseInt(document.getElementById('emoji-size').value);
      const emojiSize = Math.round(emojiPx * scaleFactor * (canvas.width / 1080));
      const pos       = document.getElementById('emoji-position').value;
      const effect    = document.getElementById('emoji-effect').value;

      // Animation progress (first 0.4 s of segment)
      const elapsed = Math.max(0, time - (activeSeg >= 0 ? segments[activeSeg].start : time));
      const t = Math.min(1, elapsed / 0.4);
      let animDy = 0, animAlpha = 1, animScale = 1;
      if (effect === 'fadein')   { animAlpha = t; }
      else if (effect === 'bounceup') {
        animDy    = t < 1 ? (1 - (t < 0.6 ? t/0.6 : 1 + Math.sin((t-0.6)/0.4*Math.PI)*0.25)) * emojiSize * 1.5 : 0;
        animAlpha = Math.min(1, t * 2);
      }
      else if (effect === 'zoomin')  { animScale = 0.2 + 0.8 * t; animAlpha = t; }
      else if (effect === 'shake')   {
        const st = Math.min(1, elapsed / 0.5);
        animDy = st < 1 ? Math.sin(st * Math.PI * 5) * emojiSize * 0.3 * (1 - st) : 0;
      }

      ctx.font = `${emojiSize}px serif`;
      ctx.textBaseline = 'middle';
      ctx.textAlign = 'center';
      const totalW = emojis.length * (emojiSize * 1.2);
      let ex = cx - totalW / 2 + emojiSize * 0.6;
      let ey;
      if (pos === 'above') ey = y - fontSize - emojiSize * 0.6;
      else if (pos === 'below') ey = y + emojiSize * 0.8;
      else { ex = cx + ctx.measureText(active.words.map(w=>w.word).join(' ')).width / 2 + emojiSize; ey = y - fontSize / 2; }

      ctx.save();
      ctx.globalAlpha = animAlpha;
      emojis.forEach(e => {
        ctx.save();
        ctx.translate(ex, ey + animDy);
        ctx.scale(animScale, animScale);
        ctx.fillText(e, 0, 0);
        ctx.restore();
        ex += emojiSize * 1.2;
      });
      ctx.restore();
    }
  }

  // emojiItems track — render independently of segments
  const trackEmojis = emojiItems.filter(it => time >= it.start && time <= it.end);
  trackEmojis.forEach(item => {
    if (!item.emojis.length) return;
    const emojiPx   = parseInt(document.getElementById('emoji-size').value);
    const emojiSize2 = Math.round(emojiPx * scaleFactor * (canvas.width / 1080));
    const elapsed2 = Math.max(0, time - item.start);
    const t2 = Math.min(1, elapsed2 / 0.4);
    const effect2 = item.effect ?? document.getElementById('emoji-effect').value;
    let dy2 = 0, alpha2 = 1, sc2 = 1;
    if (effect2 === 'fadein')    alpha2 = t2;
    else if (effect2 === 'bounceup') { dy2 = t2 < 1 ? (1 - (t2 < 0.6 ? t2/0.6 : 1 + Math.sin((t2-0.6)/0.4*Math.PI)*0.25)) * emojiSize2 * 1.5 : 0; alpha2 = Math.min(1, t2*2); }
    else if (effect2 === 'zoomin')   { sc2 = 0.2 + 0.8*t2; alpha2 = t2; }
    else if (effect2 === 'shake')    { dy2 = t2 < 1 ? Math.sin(t2 * Math.PI * 5) * emojiSize2 * 0.3 * (1-t2) : 0; }
    // Use item's own position, not the subtitle position
    const ey2 = canvas.height * ((item.posY ?? 30) / 100);
    const cx2 = canvas.width  * ((item.posX ?? 50) / 100);
    const totalW2 = item.emojis.length * (emojiSize2 * 1.2);
    let ex2 = cx2 - totalW2 / 2 + emojiSize2 * 0.6;
    ctx.save();
    ctx.font = `${emojiSize2}px serif`;
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.globalAlpha = alpha2;
    item.emojis.forEach(e => {
      ctx.save(); ctx.translate(ex2, ey2 + dy2); ctx.scale(sc2, sc2); ctx.fillText(e, 0, 0); ctx.restore();
      ex2 += emojiSize2 * 1.2;
    });
    ctx.restore();
  });

  updateActiveSegment(time);
}

function updateActiveSegment(time) {
  const idx = segments.findIndex(s => time >= s.start && time <= s.end + 0.1);
  document.querySelectorAll('.segment-card').forEach((el, i) => {
    el.classList.toggle('active', i === idx);
  });
  if (idx >= 0) {
    const el = document.getElementById(`seg-${idx}`);
    if (el) el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });

    // highlight individual words
    document.querySelectorAll(`#seg-${idx} .seg-word`).forEach(w => {
      const ws = parseFloat(w.dataset.start);
      const we = parseFloat(w.dataset.end);
      w.classList.toggle('highlight', time >= ws && time <= we + 0.05);
    });
  }
}

function updatePreview() {
  const vid = document.getElementById('video-player');
  if (vid.style.display !== 'none') renderSubtitles(vid.currentTime);
}

// ── Presets ───────────────────────────────────────────────────
const PRESETS = {
  tiktok:  { font: 'Arial Black', fontSize: 52, textColor: '#ffffff', highlightColor: '#FEE800', outlineColor: '#000000', outlineWidth: 4, position: 85, uppercase: true },
  viral:   { font: 'Impact',      fontSize: 60, textColor: '#ffffff', highlightColor: '#ff3b5c', outlineColor: '#000000', outlineWidth: 5, position: 80, uppercase: true },
  neon:    { font: 'Arial Black', fontSize: 48, textColor: '#25f4ee', highlightColor: '#fe2c55', outlineColor: '#000000', outlineWidth: 3, position: 85, uppercase: false },
  minimal: { font: 'Helvetica Neue', fontSize: 44, textColor: '#ffffff', highlightColor: '#ffffff', outlineColor: '#000000', outlineWidth: 2, position: 90, uppercase: false },
  fire:    { font: 'Impact',      fontSize: 58, textColor: '#ff6600', highlightColor: '#ffcc00', outlineColor: '#000000', outlineWidth: 6, position: 82, uppercase: true },
  shadow:  { font: 'Arial Rounded MT Bold', fontSize: 50, textColor: '#ffffff', highlightColor: '#c0ff00', outlineColor: '#1a1a1a', outlineWidth: 5, position: 87, uppercase: false },
};

function applyPreset(name) {
  document.querySelectorAll('.preset-chip').forEach(c => c.classList.toggle('active', c.textContent.toLowerCase().replace(' ','') === name));
  const p = PRESETS[name];
  if (!p) return;
  document.getElementById('font-select').value = p.font;
  document.getElementById('fontsize').value = p.fontSize;
  document.getElementById('fontsize-val').textContent = p.fontSize;
  document.getElementById('text-color').value = p.textColor;
  document.getElementById('highlight-color').value = p.highlightColor;
  document.getElementById('outline-color').value = p.outlineColor;
  document.getElementById('outline-width').value = p.outlineWidth;
  document.getElementById('outline-val').textContent = p.outlineWidth;
  document.getElementById('position').value = Math.round(p.position);
  document.getElementById('pos-val').textContent = Math.round(p.position);
  if (p.uppercase) document.getElementById('uppercase-toggle').classList.add('on');
  else document.getElementById('uppercase-toggle').classList.remove('on');
  updatePreview();
}

// ── Export ────────────────────────────────────────────────────
function enableExports() {
  ['export-srt-btn','export-ass-btn','export-video-btn'].forEach(id => {
    document.getElementById(id).disabled = false;
  });
}

function getAssStyle() {
  const s = getStyle();
  const hexToAss = (hex, alpha = '00') => {
    const r = parseInt(hex.slice(1,3),16);
    const g = parseInt(hex.slice(3,5),16);
    const b = parseInt(hex.slice(5,7),16);
    return `&H${alpha}${b.toString(16).padStart(2,'0').toUpperCase()}${g.toString(16).padStart(2,'0').toUpperCase()}${r.toString(16).padStart(2,'0').toUpperCase()}`;
  };
  return {
    font: s.font,
    fontsize: s.fontSize,
    primary_color: hexToAss(s.textColor),
    highlight_color: hexToAss(s.highlightColor),
    outline_color: hexToAss(s.outlineColor),
    bold: true,
    outline: s.outlineWidth,
    shadow: 0,
    margin_v: Math.round((1 - s.position / 100) * 1920 * 0.15 + 40),
    pos_x_frac: s.positionX,
    pos_y_frac: s.position,
  };
}

async function exportSRT() {
  const res = await fetch('/export/srt', {
    method: 'POST',
    headers: {'Content-Type':'application/json'},
    body: JSON.stringify({ segments, words_per_chunk: document.getElementById('words-per-chunk').value })
  });
  downloadBlob(await res.blob(), 'subtitles.srt');
}

async function exportASS() {
  const res = await fetch('/export/ass', {
    method: 'POST',
    headers: {'Content-Type':'application/json'},
    body: JSON.stringify({ segments, style: getAssStyle(), words_per_chunk: document.getElementById('words-per-chunk').value })
  });
  downloadBlob(await res.blob(), 'subtitles.ass');
}

async function exportVideo() {
  if (!videoFile) return;
  const btn = document.getElementById('export-video-btn');
  btn.disabled = true;
  btn.textContent = '⏳ Burning…';
  const fd = new FormData();
  fd.append('file', videoFile);
  fd.append('segments_json', JSON.stringify(segments));
  fd.append('style_json', JSON.stringify(getAssStyle()));
  fd.append('words_per_chunk', document.getElementById('words-per-chunk').value);
  try {
    const res = await fetch('/export/video', { method: 'POST', body: fd });
    if (!res.ok) throw new Error(await res.text());
    downloadBlob(await res.blob(), 'output_with_subs.mp4');
  } catch(e) {
    alert('Export failed: ' + e.message);
  }
  btn.disabled = false;
  btn.textContent = '🔥 Burn to Video';
}

function downloadBlob(blob, name) {
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = name;
  a.click();
}

// ── AI Highlights ─────────────────────────────────────────────
async function analyzeHighlights(useClaude) {
  if (!segments.length) return;
  const btn = useClaude ? document.getElementById('hl-claude-btn') : document.getElementById('hl-local-btn');
  const prevText = btn.textContent;
  btn.disabled = true;
  btn.textContent = '⏳ Analyzing…';
  document.getElementById('hl-stats').textContent = '';

  const target_pct = parseInt(document.getElementById('hl-target').value) / 100;
  const api_key = useClaude ? document.getElementById('hl-claude-key').value.trim() : '';

  try {
    const res = await fetch('/analyze_highlights', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ segments, target_pct, api_key }),
    });
    if (!res.ok) throw new Error(await res.text());
    const data = await res.json();

    highlightScores = data.scores || [];
    highlightSelected = new Set(data.selected || []);

    if (data.warning) document.getElementById('hl-stats').textContent = '⚠️ ' + data.warning;

    renderSegmentsList();
    updateHlStats();
    document.getElementById('hl-export-btn').disabled = !videoFile || !highlightSelected.size;
  } catch (e) {
    document.getElementById('hl-stats').textContent = 'Error: ' + e.message;
  }
  btn.disabled = false;
  btn.textContent = prevText;
}

function toggleHighlightSegment(idx) {
  if (highlightSelected.has(idx)) highlightSelected.delete(idx);
  else highlightSelected.add(idx);
  // Update just this card's score row + class without full re-render
  const card = document.getElementById(`seg-${idx}`);
  if (!card) return;
  const included = highlightSelected.has(idx);
  card.classList.toggle('hl-excluded', !included);
  const toggle = card.querySelector('.seg-include-toggle');
  const fill = card.querySelector('.seg-score-bar-fill');
  if (toggle) toggle.classList.toggle('included', included);
  if (fill) fill.style.background = included ? '#22c55e' : '#6b7280';
  updateHlStats();
  document.getElementById('hl-export-btn').disabled = !videoFile || !highlightSelected.size;
}

function updateHlStats() {
  const sel = [...highlightSelected];
  if (!sel.length || !segments.length) { document.getElementById('hl-stats').textContent = ''; return; }
  const dur = sel.reduce((sum, i) => sum + (segments[i].end - segments[i].start), 0);
  const total = segments.reduce((sum, s) => sum + (s.end - s.start), 0);
  const pct = Math.round(dur / total * 100);
  document.getElementById('hl-stats').textContent =
    `${sel.length} segments · ${fmt(dur)} of ${fmt(total)} (${pct}%)`;
}

async function exportHighlights() {
  if (!videoFile || !highlightSelected.size) return;
  const btn = document.getElementById('hl-export-btn');
  btn.disabled = true;
  btn.textContent = '⏳ Cutting…';

  const ranges = [...highlightSelected].sort((a,b)=>a-b).map(i => [segments[i].start, segments[i].end]);
  const fd = new FormData();
  fd.append('file', videoFile);
  fd.append('ranges_json', JSON.stringify(ranges));
  fd.append('padding', '0.15');

  try {
    const res = await fetch('/export/highlights', { method: 'POST', body: fd });
    if (!res.ok) throw new Error(await res.text());
    downloadBlob(await res.blob(), 'highlights.mp4');
  } catch (e) {
    alert('Export failed: ' + e.message);
  }
  btn.disabled = false;
  btn.textContent = '🎬 Export Reel';
}

// ── GIF reactions ─────────────────────────────────────────────
function refreshGifTargetDropdown() {
  const sel = document.getElementById('gif-assign-target');
  sel.innerHTML = segments.map((s, i) =>
    `<option value="${i}">[${fmt(s.start)}] ${s.text.slice(0,40)}</option>`
  ).join('');
}

async function searchGifs() {
  const q = document.getElementById('gif-search').value.trim();
  if (!q) return;
  const key = document.getElementById('giphy-key').value.trim();
  const url = `/search_gifs?q=${encodeURIComponent(q)}${key ? '&api_key=' + encodeURIComponent(key) : ''}`;
  const res = await fetch(url);
  if (!res.ok) { alert('GIF search failed'); return; }
  const data = await res.json();
  const grid = document.getElementById('gif-results');
  if (data.hint) {
    grid.innerHTML = `<p style="grid-column:1/-1;color:var(--muted);font-size:12px;padding:8px 0">${data.hint}</p>`;
    grid._data = [];
    return;
  }
  if (!data.results.length) {
    grid.innerHTML = `<p style="grid-column:1/-1;color:var(--muted);font-size:12px;padding:8px 0">Inga resultat.</p>`;
    grid._data = [];
    return;
  }
  grid.innerHTML = data.results.map((g, i) =>
    `<div class="gif-thumb" data-idx="${i}" onclick="assignGif(${i})" title="${g.title}">
      <img src="${g.preview}" alt="${g.title}" loading="lazy">
    </div>`
  ).join('');
  grid._data = data.results;
}

function assignGif(thumbIdx) {
  const grid = document.getElementById('gif-results');
  const gif = grid._data[thumbIdx];
  if (!gif) return;
  selectedGif = gif;
  const segIdx = parseInt(document.getElementById('gif-assign-target').value);
  if (isNaN(segIdx)) return;
  segmentGifs[segIdx] = gif;
  document.querySelectorAll('.gif-thumb').forEach((el, i) => el.classList.toggle('selected', i === thumbIdx));
  refreshGifBadge(segIdx);
  updateGifOverlay(document.getElementById('video-player').currentTime);
  document.getElementById('crop-export-btn').disabled = false;
}

function removeSegmentGif(segIdx) {
  delete segmentGifs[segIdx];
  refreshGifBadge(segIdx);
  updateGifOverlay(document.getElementById('video-player').currentTime);
}

function refreshGifBadge(segIdx) {
  const el = document.getElementById(`gif-badge-${segIdx}`);
  if (!el) return;
  const gif = segmentGifs[segIdx];
  el.innerHTML = gif
    ? `<div class="seg-gif-badge"><img src="${gif.preview}" alt="GIF"><span style="font-size:11px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1">${gif.title||'GIF'}</span><button type="button" class="gif-remove" onclick="event.stopPropagation();removeSegmentGif(${segIdx})">×</button></div>`
    : '';
}

function updateGifOverlay(time) {
  const overlay = document.getElementById('gif-overlay');
  const vid = document.getElementById('video-player');
  if (!overlay || vid.style.display === 'none') return;

  const enabled = document.getElementById('gif-toggle').classList.contains('on');
  if (!enabled) { overlay.innerHTML = ''; return; }

  // Check gifItems track first (per-item settings), then fall back to segment-based gifs
  const trackGifItem = gifItems.find(it => time >= it.start && time <= it.end);
  const activeSeg    = segments.findIndex(s => time >= s.start && time <= s.end + 0.05);

  let gif, sizePct, opacity, posX, posY, effect;
  if (trackGifItem) {
    gif     = trackGifItem.gif;
    sizePct = (trackGifItem.size    ?? parseInt(document.getElementById('gif-size').value))    / 100;
    opacity = (trackGifItem.opacity ?? parseInt(document.getElementById('gif-opacity').value)) / 100;
    posX    = (trackGifItem.posX    ?? parseInt(document.getElementById('gif-pos-x').value))   / 100;
    posY    = (trackGifItem.posY    ?? parseInt(document.getElementById('gif-pos-y').value))   / 100;
    effect  = trackGifItem.effect   ?? document.getElementById('gif-effect').value;
  } else {
    gif     = activeSeg >= 0 ? segmentGifs[activeSeg] : null;
    sizePct = parseInt(document.getElementById('gif-size').value)    / 100;
    opacity = parseInt(document.getElementById('gif-opacity').value) / 100;
    posX    = parseInt(document.getElementById('gif-pos-x').value)   / 100;
    posY    = parseInt(document.getElementById('gif-pos-y').value)   / 100;
    effect  = document.getElementById('gif-effect').value;
  }

  if (!gif?.mp4) { overlay.innerHTML = ''; return; }

  const vw = vid.offsetWidth, vh = vid.offsetHeight;
  const gw   = Math.round(vw * sizePct);
  const left = Math.round(vw * posX - gw / 2);
  const top  = Math.round(vh * posY);

  // Reuse existing video element to prevent flicker — only recreate when gif changes
  let item = overlay.querySelector('.gif-overlay-item');
  let vidEl = item?.querySelector('video');
  if (!item || vidEl?.dataset.src !== gif.mp4) {
    overlay.innerHTML = '';
    item = document.createElement('div');
    item.className = 'gif-overlay-item' + (effect !== 'none' ? ` gif-anim-${effect}` : '');
    vidEl = document.createElement('video');
    vidEl.autoplay = true; vidEl.loop = true; vidEl.muted = true; vidEl.playsInline = true;
    vidEl.dataset.src = gif.mp4;
    vidEl.src = gif.mp4;
    item.appendChild(vidEl);
    overlay.appendChild(item);
  }
  item.style.cssText = `width:${gw}px;top:${top}px;left:${left}px;opacity:${opacity}`;
}

// ── Crop ──────────────────────────────────────────────────────
function setCropAspect(aspect, el) {
  cropAspect = aspect;
  document.querySelectorAll('.crop-chip').forEach(c => c.classList.remove('active'));
  el.classList.add('active');
  updateCropOverlay();
  document.getElementById('crop-export-btn').disabled = !videoFile;
}

function updateCropOverlay() {
  const vid = document.getElementById('video-player');
  const overlay = document.getElementById('crop-overlay');
  const win = document.getElementById('crop-window');
  const lbl = document.getElementById('crop-label');
  if (!vid || vid.style.display === 'none') return;

  lbl.textContent = cropAspect;
  const [aN, aD] = cropAspect.split(':').map(Number);
  const targetRatio = aN / aD;
  const srcRatio = vid.videoWidth / vid.videoHeight;
  const dw = vid.offsetWidth, dh = vid.offsetHeight;

  let cw, ch, cx, cy;
  const offX = parseInt(document.getElementById('crop-x').value) / 100;
  const offY = parseInt(document.getElementById('crop-y').value) / 100;

  if (srcRatio > targetRatio) {
    ch = dh;
    cw = Math.round(dh * targetRatio);
    cx = Math.round((dw - cw) * offX);
    cy = 0;
  } else {
    cw = dw;
    ch = Math.round(dw / targetRatio);
    cx = 0;
    cy = Math.round((dh - ch) * offY);
  }

  win.style.cssText = `left:${cx}px;top:${cy}px;width:${cw}px;height:${ch}px`;
}

function toggleCropPreview() {
  const overlay = document.getElementById('crop-overlay');
  const on = document.getElementById('crop-preview-toggle').classList.contains('on');
  overlay.style.display = on ? 'block' : 'none';
  if (on) updateCropOverlay();
}

async function exportCropped() {
  if (!videoFile) return;
  const btn = document.getElementById('crop-export-btn');
  btn.disabled = true;
  btn.textContent = '⏳ Cropping…';
  const fd = new FormData();
  fd.append('file', videoFile);
  fd.append('aspect', cropAspect);
  fd.append('offset_x', parseInt(document.getElementById('crop-x').value) / 100);
  fd.append('offset_y', parseInt(document.getElementById('crop-y').value) / 100);
  try {
    const res = await fetch('/crop', { method: 'POST', body: fd });
    if (!res.ok) throw new Error(await res.text());
    downloadBlob(await res.blob(), `cropped_${cropAspect.replace(':','x')}.mp4`);
  } catch(e) {
    alert('Crop failed: ' + e.message);
  }
  btn.disabled = false;
  btn.textContent = '✂️ Export Cropped';
}

// ── Emoji reactions ───────────────────────────────────────────
const EMOJI_CATEGORIES = {
  'Reactions': ['😂','🤣','😭','😱','🤯','😍','🥰','😎','🤔','😤','😡','🥺','😢','🤩','😏'],
  'Hype':      ['🔥','💯','⚡','💥','🎉','🎊','✨','💪','👏','🙌','🤙','👍','💃','🕺'],
  'Symbols':   ['❤️','🧡','💛','💚','💙','💜','🖤','🤍','💔','‼️','⁉️','🆘','✅','❌'],
  'Nature':    ['😈','👻','💀','🤖','👽','🎭','🎬','🎵','🎶','🌟','⭐','🌈','🌊','🍿'],
};

const EMOJI_KEYWORDS = [
  { words: ['haha','lol','funny','skratt','rolig','😂','😄'], emojis: ['😂','🤣'] },
  { words: ['wow','omg','crazy','galen','vansinnig','shock'], emojis: ['😱','🤯'] },
  { words: ['love','kärlek','älskar','hjärta','heart'],       emojis: ['❤️','😍'] },
  { words: ['fire','bäst','best','amazing','grym','🔥'],      emojis: ['🔥','💯'] },
  { words: ['sad','ledsen','sorglig','cry','gråter'],          emojis: ['😢','😭'] },
  { words: ['angry','arg','förbannad','mad','rage'],           emojis: ['😡','😤'] },
  { words: ['cool','nice','snygg','awesome','dope'],           emojis: ['😎','✨'] },
  { words: ['yes','ja','perfect','perfekt','exakt'],           emojis: ['✅','👍'] },
  { words: ['no','nej','wrong','fel','never','aldrig'],        emojis: ['❌','🙅'] },
  { words: ['party','fest','celebrate','fira','wins','vinner'],emojis: ['🎉','🥳'] },
  { words: ['music','musik','dance','dansa','beat','låt'],     emojis: ['🎵','💃'] },
  { words: ['food','mat','eat','äter','hungry','hungrig'],     emojis: ['🍕','😋'] },
  { words: ['run','spring','fast','snabb','speed'],            emojis: ['⚡','💨'] },
  { words: ['think','tänk','hmm','maybe','kanske'],            emojis: ['🤔','💭'] },
];

function autoDetectEmojis() {
  if (!segments.length) return;
  segments.forEach((seg, i) => {
    const text = seg.text.toLowerCase();
    const found = new Set(segmentEmojis[i] || []);
    for (const rule of EMOJI_KEYWORDS) {
      if (rule.words.some(w => text.includes(w))) {
        rule.emojis.forEach(e => found.add(e));
      }
    }
    if (found.size) segmentEmojis[i] = [...found];
  });
  renderSegmentsList();
  updatePreview();
}

function openEmojiPicker(segIdx, anchorEl) {
  closeEmojiPicker();
  emojiPickerTarget = segIdx;

  const picker = document.createElement('div');
  picker.className = 'emoji-picker';
  picker.id = 'emoji-picker';

  const current = segmentEmojis[segIdx] || [];
  let html = `<div class="emoji-picker-header">
    <span class="emoji-picker-title">Add reaction</span>
    <button type="button" class="emoji-picker-close" onclick="closeEmojiPicker()">×</button>
  </div>`;

  for (const [cat, emojis] of Object.entries(EMOJI_CATEGORIES)) {
    html += `<div class="emoji-category">${cat}</div><div class="emoji-grid">`;
    html += emojis.map(e =>
      `<span class="emoji-opt${current.includes(e) ? ' selected' : ''}" onclick="toggleEmoji(${segIdx},'${e}',this)" title="${e}">${e}</span>`
    ).join('');
    html += '</div>';
  }
  picker.innerHTML = html;
  document.body.appendChild(picker);

  const rect = anchorEl.getBoundingClientRect();
  picker.style.top = `${Math.min(rect.bottom + 6, window.innerHeight - picker.offsetHeight - 10)}px`;
  picker.style.left = `${Math.min(rect.left, window.innerWidth - 290)}px`;

  setTimeout(() => document.addEventListener('click', outsidePickerClick), 0);
}

function outsidePickerClick(e) {
  const picker = document.getElementById('emoji-picker');
  if (picker && !picker.contains(e.target)) closeEmojiPicker();
}

function closeEmojiPicker() {
  document.getElementById('emoji-picker')?.remove();
  document.removeEventListener('click', outsidePickerClick);
  emojiPickerTarget = null;
}

function openEmojiPickerForItem(itemId, clientX, clientY) {
  closeEmojiPicker();
  emojiPickerTarget = { type: 'item', id: itemId };
  const item = emojiItems.find(it => it.id === itemId);
  if (!item) return;

  const picker = document.createElement('div');
  picker.className = 'emoji-picker';
  picker.id = 'emoji-picker';

  let html = `<div class="emoji-picker-header">
    <span class="emoji-picker-title">Emoji på tidslinjen</span>
    <button type="button" class="emoji-picker-close" onclick="closeEmojiPicker()">×</button>
  </div>`;
  for (const [cat, emojis] of Object.entries(EMOJI_CATEGORIES)) {
    html += `<div class="emoji-category">${cat}</div><div class="emoji-grid">`;
    html += emojis.map(e =>
      `<span class="emoji-opt${item.emojis.includes(e) ? ' selected' : ''}" onclick="toggleEmojiItem(${itemId},'${e}',this)">${e}</span>`
    ).join('');
    html += '</div>';
  }
  // Delete button
  html += `<button type="button" style="margin-top:8px;width:100%;background:var(--accent);border:none;color:white;padding:5px;border-radius:6px;cursor:pointer;font-size:12px" onclick="removeEmojiItem(${itemId})">🗑 Ta bort</button>`;
  picker.innerHTML = html;
  document.body.appendChild(picker);

  picker.style.top  = `${Math.min(clientY, window.innerHeight - picker.offsetHeight - 10)}px`;
  picker.style.left = `${Math.min(clientX, window.innerWidth  - 290)}px`;
  setTimeout(() => document.addEventListener('click', outsidePickerClick), 0);
}

function toggleEmojiItem(itemId, emoji, el) {
  const item = emojiItems.find(it => it.id === itemId);
  if (!item) return;
  const idx = item.emojis.indexOf(emoji);
  if (idx >= 0) { item.emojis.splice(idx, 1); el.classList.remove('selected'); }
  else { item.emojis.push(emoji); el.classList.add('selected'); }
  drawTimeline();
  updatePreview();
}

function removeEmojiItem(itemId) {
  emojiItems = emojiItems.filter(it => it.id !== itemId);
  closeEmojiPicker();
  drawTimeline();
  renderReactionList();
  updatePreview();
}

function removeGifItem(itemId) {
  gifItems = gifItems.filter(it => it.id !== itemId);
  drawTimeline();
  renderReactionList();
  updateGifOverlay(document.getElementById('video-player').currentTime);
}

function _fmtSec(s) {
  const m = Math.floor(s / 60), sec = (s % 60).toFixed(1);
  return `${m}:${sec.padStart(4,'0')}`;
}
function _parseSec(str) {
  const [m, s] = str.split(':').map(Number);
  return (m || 0) * 60 + (s || 0);
}

function renderReactionList() {
  const el = document.getElementById('reaction-list');
  if (!el) return;
  const EFFECTS = ['none','fadein','slideup','bounceup','zoomin','spin','shake'];
  const EFFECT_LABELS = { none:'Ingen', fadein:'Tona fram', slideup:'Glid upp', bounceup:'Studsa upp', zoomin:'Zooma in', spin:'Snurra in', shake:'Skaka' };

  const rows = [
    ...emojiItems.map(item => ({ type:'emoji', item })),
    ...gifItems.map(item => ({ type:'gif', item })),
  ].sort((a, b) => a.item.start - b.item.start);

  if (!rows.length) { el.innerHTML = '<p style="font-size:12px;color:var(--muted);text-align:center;padding:8px">Inga emoji- eller GIF-block. Klicka i spåret eller använd + knapparna.</p>'; return; }

  el.innerHTML = rows.map(({ type, item }) => {
    const badge  = type === 'emoji' ? (item.emojis.slice(0,3).join('')||'😶') : '🎞';
    const effOpts = EFFECTS.map(e => `<option value="${e}"${(item.effect||'none')===e?' selected':''}>${EFFECT_LABELS[e]}</option>`).join('');
    const posX = item.posX ?? 50;
    const posY = item.posY ?? 30;
    const extra = type === 'gif'
      ? `<div class="re-field"><label>Storlek %</label><input type="number" min="10" max="100" value="${item.size??50}" oninput="updateReItem('${type}',${item.id},'size',+this.value)"></div>
         <div class="re-field"><label>Opacitet %</label><input type="number" min="10" max="100" value="${item.opacity??90}" oninput="updateReItem('${type}',${item.id},'opacity',+this.value)"></div>`
      : `<button type="button" class="tl-btn" style="font-size:11px" onclick="openEmojiPickerForItem(${item.id},event.clientX,event.clientY)">Välj emojis</button>`;
    return `<div class="re-item re-item-${type}">
      <span class="re-badge">${badge}</span>
      <div class="re-field">
        <label>Start</label>
        <input type="number" step="0.1" value="${item.start.toFixed(1)}" oninput="updateReItem('${type}',${item.id},'start',+this.value)" style="width:60px">
      </div>
      <div class="re-field">
        <label>Slut</label>
        <input type="number" step="0.1" value="${item.end.toFixed(1)}" oninput="updateReItem('${type}',${item.id},'end',+this.value)" style="width:60px">
      </div>
      <button type="button" class="re-del" onclick="removeReItem('${type}',${item.id})">×</button>
      <div class="re-row2">
        <div class="re-field"><label>Pos X %</label><input type="number" min="0" max="100" value="${posX}" oninput="updateReItem('${type}',${item.id},'posX',+this.value)" style="width:52px"></div>
        <div class="re-field"><label>Pos Y %</label><input type="number" min="0" max="100" value="${posY}" oninput="updateReItem('${type}',${item.id},'posY',+this.value)" style="width:52px"></div>
        <div class="re-field"><label>Effekt</label><select onchange="updateReItem('${type}',${item.id},'effect',this.value)">${effOpts}</select></div>
        ${extra}
      </div>
    </div>`;
  }).join('');
}

function updateReItem(type, id, field, value) {
  const arr = type === 'emoji' ? emojiItems : gifItems;
  const item = arr.find(it => it.id === id);
  if (!item) return;
  if (field === 'start') item.start = Math.max(0, value);
  else if (field === 'end') item.end = Math.max(item.start + 0.1, value);
  else item[field] = value;
  drawTimeline();
  updatePreview();
  updateGifOverlay(document.getElementById('video-player').currentTime);
}

function removeReItem(type, id) {
  if (type === 'emoji') { emojiItems = emojiItems.filter(it => it.id !== id); updatePreview(); }
  else { gifItems = gifItems.filter(it => it.id !== id); updateGifOverlay(document.getElementById('video-player').currentTime); }
  drawTimeline();
  renderReactionList();
}

function addEmojiItemManual() {
  const vid = document.getElementById('video-player');
  const t = vid.style.display !== 'none' ? vid.currentTime : 0;
  const item = { id: nextItemId++, start: t, end: t + 2, emojis: [], posX: 50, posY: 30, effect: 'bounceup' };
  emojiItems.push(item);
  drawTimeline();
  renderReactionList();
  openEmojiPickerForItem(item.id, window.innerWidth / 2, window.innerHeight / 2);
}

function addGifItemManual() {
  if (!selectedGif) { alert('Välj en GIF i sökresultaten först.'); return; }
  const vid = document.getElementById('video-player');
  const t = vid.style.display !== 'none' ? vid.currentTime : 0;
  gifItems.push({ id: nextItemId++, start: t, end: t + 3, gif: selectedGif, posX: 50, posY: 30, size: 50, opacity: 90, effect: 'slideup' });
  drawTimeline();
  renderReactionList();
}

function toggleEmoji(segIdx, emoji, el) {
  const arr = segmentEmojis[segIdx] || [];
  const idx = arr.indexOf(emoji);
  if (idx >= 0) { arr.splice(idx, 1); el.classList.remove('selected'); }
  else { arr.push(emoji); el.classList.add('selected'); }
  segmentEmojis[segIdx] = arr;
  refreshEmojiRow(segIdx);
  updatePreview();
}

function removeEmoji(segIdx, emoji) {
  const arr = segmentEmojis[segIdx] || [];
  segmentEmojis[segIdx] = arr.filter(e => e !== emoji);
  refreshEmojiRow(segIdx);
  updatePreview();
}

// ── Timeline ──────────────────────────────────────────────────
async function generateWaveform(file) {
  try {
    const ab = await file.arrayBuffer();
    const ac = new (window.AudioContext || window.webkitAudioContext)();
    const buf = await ac.decodeAudioData(ab);
    const data = buf.getChannelData(0);
    const dur = buf.duration;
    const numPeaks = Math.min(8192, Math.ceil(dur * 100));
    const blockSize = Math.floor(data.length / numPeaks);
    const peaks = new Float32Array(numPeaks);
    for (let i = 0; i < numPeaks; i++) {
      let max = 0;
      const base = i * blockSize;
      for (let j = 0; j < blockSize; j++) {
        const abs = Math.abs(data[base + j] || 0);
        if (abs > max) max = abs;
      }
      peaks[i] = max;
    }
    waveformData = { peaks, duration: dur, numPeaks };
    ac.close();
    if (document.getElementById('timeline-bar').style.display !== 'none') drawTimeline();
  } catch (e) {
    console.warn('Waveform failed:', e);
  }
}

function tlTotalDuration() {
  const vid = document.getElementById('video-player');
  if (vid.style.display !== 'none' && vid.duration > 0) return vid.duration;
  if (segments.length) return Math.max(...segments.map(s => s.end)) + 1;
  return 60;
}

let tlDragCanvasRect = null;
let emojiItems = [];   // [{id,start,end,emojis:[]}]
let gifItems   = [];   // [{id,start,end,gif:{}}]
let nextItemId = 0;
let selectedGif = null; // currently selected gif from search

function initTimeline() {
  const bar = document.getElementById('timeline-bar');
  const canvas = document.getElementById('timeline-canvas');
  canvas.width = bar.clientWidth;
  canvas.height = bar.clientHeight - 28;
  canvas.onmousedown = tlMouseDown;
  canvas.onmousemove = tlMouseMove;
  canvas.onmouseleave = tlMouseLeave;
  canvas.addEventListener('wheel', tlWheel, { passive: false });
  fitTimeline();
}

function fitTimeline() {
  const canvas = document.getElementById('timeline-canvas');
  if (!canvas) return;
  const W = canvas.width || 800;
  const dur = tlTotalDuration();
  tlZoom = dur > 0 ? Math.max(10, (W - 20) / dur) : 100;
  tlOffset = 0;
  updateTlZoomLabel();
  drawTimeline();
}

function zoomTimeline(dir) {
  const canvas = document.getElementById('timeline-canvas');
  const W = canvas.width;
  const centerT = tlOffset + W / 2 / tlZoom;
  tlZoom = Math.max(10, Math.min(3000, tlZoom * (dir > 0 ? 1.6 : 0.625)));
  tlOffset = Math.max(0, centerT - W / 2 / tlZoom);
  updateTlZoomLabel();
  drawTimeline();
}

function updateTlZoomLabel() {
  const z = tlZoom / 100;
  const lbl = document.getElementById('tl-zoom-label');
  if (lbl) lbl.textContent = z >= 10 ? `${Math.round(z)}×` : z >= 1 ? `${z.toFixed(1)}×` : `${z.toFixed(2)}×`;
}

function tlDims() {
  const canvas = document.getElementById('timeline-canvas');
  const W = canvas.width, H = canvas.height;
  const RULER = 20, WAVE = 54;
  const SUBS_Y = RULER + WAVE + 3;
  const SUBS_H = 36;
  const EMOJI_Y = SUBS_Y + SUBS_H + 2;
  const EMOJI_H = 26;
  const GIF_Y = EMOJI_Y + EMOJI_H + 2;
  const GIF_H = H - GIF_Y - 3;
  return { W, H, RULER, WAVE, SUBS_Y, SUBS_H, EMOJI_Y, EMOJI_H, GIF_Y, GIF_H };
}

function tlX(t) { return (t - tlOffset) * tlZoom; }
function tlT(x) { return tlOffset + x / tlZoom; }

function drawTimeline() {
  const canvas = document.getElementById('timeline-canvas');
  if (!canvas) return;
  const ctx = canvas.getContext('2d');
  const { W, H, RULER, WAVE, SUBS_Y, SUBS_H } = tlDims();
  if (!W || !H) return;

  ctx.clearRect(0, 0, W, H);
  ctx.fillStyle = '#060609';
  ctx.fillRect(0, 0, W, H);

  // ── Ruler ──
  ctx.fillStyle = '#10101a';
  ctx.fillRect(0, 0, W, RULER);
  const tick = tlNiceTick(tlZoom);
  const t0 = Math.floor(tlOffset / tick) * tick;
  ctx.font = '9px monospace';
  ctx.textBaseline = 'middle';
  for (let t = t0; t < tlOffset + W / tlZoom + tick; t += tick) {
    const x = Math.round(tlX(t));
    if (x < -30 || x > W + 10) continue;
    ctx.fillStyle = '#333';
    ctx.fillRect(x, RULER - 5, 1, 5);
    ctx.fillStyle = '#666';
    ctx.textAlign = 'left';
    ctx.fillText(tlFmtTime(t), x + 2, RULER / 2);
  }
  ctx.fillStyle = '#222';
  ctx.fillRect(0, RULER, W, 1);

  // ── Waveform ──
  ctx.fillStyle = '#0c0c16';
  ctx.fillRect(0, RULER, W, WAVE);
  if (waveformData) {
    const { peaks, duration, numPeaks } = waveformData;
    const yCtr = RULER + WAVE / 2;
    ctx.fillStyle = '#1e6e6e';
    ctx.fillRect(0, yCtr, W, 1);
    for (let px = 0; px < W; px++) {
      const t = tlT(px);
      if (t < 0 || t > duration) continue;
      const idx = Math.min(numPeaks - 1, Math.floor((t / duration) * numPeaks));
      const amp = Math.round(peaks[idx] * (WAVE / 2 - 2));
      if (amp < 1) continue;
      ctx.fillStyle = '#25f4ee60';
      ctx.fillRect(px, yCtr - amp, 1, amp * 2);
    }
  }

  // ── Subtitle blocks ──
  ctx.fillStyle = '#111118';
  ctx.fillRect(0, SUBS_Y - 3, W, 3);
  ctx.fillStyle = '#0a0a12';
  ctx.fillRect(0, SUBS_Y, W, SUBS_H);

  segments.forEach((seg, i) => {
    const x1 = tlX(seg.start), x2 = tlX(seg.end);
    if (x2 < -20 || x1 > W + 20) return;
    const bw = x2 - x1;
    if (bw < 1) return;

    const isDragging = tlDrag?.segIdx === i;
    const isHovered = tlHoverSeg === i;
    const isDragStart = isDragging && tlDrag.type === 'resize-start';
    const isDragEnd   = isDragging && tlDrag.type === 'resize-end';
    const isDragMove  = isDragging && tlDrag.type === 'move';

    const y1 = SUBS_Y + 3, y2 = SUBS_Y + SUBS_H - 3, midY = SUBS_Y + SUBS_H / 2;
    const vx1 = Math.max(0, x1), vx2 = Math.min(W, x2);

    // Subtle background
    ctx.fillStyle = isDragging ? 'rgba(254,44,85,.14)' : isHovered ? 'rgba(254,44,85,.09)' : 'rgba(254,44,85,.05)';
    ctx.fillRect(vx1, y1, vx2 - vx1, y2 - y1);

    // Top + bottom lines (only visible portion)
    ctx.strokeStyle = isDragMove ? '#25f4ee' : isHovered ? '#ff607a' : '#fe2c55';
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(vx1, y1); ctx.lineTo(vx2, y1);
    ctx.moveTo(vx1, y2); ctx.lineTo(vx2, y2);
    ctx.stroke();

    // Left edge + arrow
    if (x1 >= -1 && x1 <= W + 1) {
      ctx.strokeStyle = isDragStart ? '#25f4ee' : isHovered ? '#ff607a' : '#fe2c55';
      ctx.lineWidth = isDragStart ? 2.5 : 1.5;
      ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x1, y2); ctx.stroke();
      if (bw > 22) {
        ctx.fillStyle = isDragStart ? '#25f4ee' : '#fe2c55';
        ctx.font = '10px sans-serif';
        ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
        ctx.fillText('◀', x1 + 3, midY);
      }
    }

    // Right edge + arrow
    if (x2 >= -1 && x2 <= W + 1) {
      ctx.strokeStyle = isDragEnd ? '#25f4ee' : isHovered ? '#ff607a' : '#fe2c55';
      ctx.lineWidth = isDragEnd ? 2.5 : 1.5;
      ctx.beginPath(); ctx.moveTo(x2, y1); ctx.lineTo(x2, y2); ctx.stroke();
      if (bw > 22) {
        ctx.fillStyle = isDragEnd ? '#25f4ee' : '#fe2c55';
        ctx.font = '10px sans-serif';
        ctx.textAlign = 'right'; ctx.textBaseline = 'middle';
        ctx.fillText('▶', x2 - 3, midY);
      }
    }

    // Text label
    if (bw > 42) {
      ctx.save();
      const tx = Math.max(0, x1) + 16, tw = Math.min(W, x2) - tx - 16;
      if (tw > 8) {
        ctx.beginPath(); ctx.rect(tx, y1, tw, y2 - y1); ctx.clip();
        ctx.fillStyle = isDragging ? 'rgba(37,244,238,.9)' : 'rgba(255,255,255,.75)';
        ctx.font = '10px Inter, sans-serif';
        ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
        ctx.fillText(seg.text, tx + tw / 2, midY);
      }
      ctx.restore();
    }
  });

  // ── Emoji track ──
  const { EMOJI_Y, EMOJI_H, GIF_Y, GIF_H } = tlDims();
  ctx.fillStyle = '#08080f';
  ctx.fillRect(0, EMOJI_Y, W, EMOJI_H);
  ctx.fillStyle = '#1a1a28';
  ctx.font = '8px monospace';
  ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
  ctx.fillText('EMOJI', 2, EMOJI_Y + EMOJI_H / 2);

  emojiItems.forEach((item, i) => {
    const ex1 = tlX(item.start), ex2 = tlX(item.end);
    if (ex2 < 0 || ex1 > W) return;
    const ey1 = EMOJI_Y + 2, ey2 = EMOJI_Y + EMOJI_H - 2;
    const isActive = tlDrag?.track === 'emoji' && tlDrag.segIdx === i;
    ctx.fillStyle = isActive ? 'rgba(37,244,238,.25)' : 'rgba(254,220,0,.15)';
    ctx.fillRect(Math.max(0, ex1), ey1, Math.min(W, ex2) - Math.max(0, ex1), ey2 - ey1);
    ctx.strokeStyle = isActive ? '#25f4ee' : '#fee800';
    ctx.lineWidth = 1.5;
    ctx.beginPath();
    ctx.moveTo(Math.max(0, ex1), ey1); ctx.lineTo(Math.min(W, ex2), ey1);
    ctx.moveTo(Math.max(0, ex1), ey2); ctx.lineTo(Math.min(W, ex2), ey2);
    ctx.stroke();
    if (ex1 >= 0 && ex1 <= W) { ctx.beginPath(); ctx.moveTo(ex1, ey1); ctx.lineTo(ex1, ey2); ctx.stroke(); }
    if (ex2 >= 0 && ex2 <= W) { ctx.beginPath(); ctx.moveTo(ex2, ey1); ctx.lineTo(ex2, ey2); ctx.stroke(); }
    const label = item.emojis.slice(0, 4).join(' ') || '+';
    const bw = ex2 - ex1;
    if (bw > 16) {
      ctx.save();
      ctx.beginPath(); ctx.rect(Math.max(0, ex1) + 2, ey1, Math.min(W, ex2) - Math.max(0, ex1) - 4, ey2 - ey1); ctx.clip();
      ctx.font = `${Math.min(14, EMOJI_H - 6)}px serif`;
      ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
      ctx.fillText(label, ex1 + bw / 2, EMOJI_Y + EMOJI_H / 2);
      ctx.restore();
    }
  });

  // ── GIF track ──
  ctx.fillStyle = '#08080f';
  ctx.fillRect(0, GIF_Y, W, GIF_H);
  ctx.fillStyle = '#1a1a28';
  ctx.font = '8px monospace';
  ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
  ctx.fillText('GIF', 2, GIF_Y + GIF_H / 2);

  gifItems.forEach((item, i) => {
    const gx1 = tlX(item.start), gx2 = tlX(item.end);
    if (gx2 < 0 || gx1 > W) return;
    const gy1 = GIF_Y + 2, gy2 = GIF_Y + GIF_H - 2;
    const isActive = tlDrag?.track === 'gif' && tlDrag.segIdx === i;
    ctx.fillStyle = isActive ? 'rgba(37,244,238,.25)' : 'rgba(100,180,255,.13)';
    ctx.fillRect(Math.max(0, gx1), gy1, Math.min(W, gx2) - Math.max(0, gx1), gy2 - gy1);
    ctx.strokeStyle = isActive ? '#25f4ee' : '#64b4ff';
    ctx.lineWidth = 1.5;
    ctx.beginPath();
    ctx.moveTo(Math.max(0, gx1), gy1); ctx.lineTo(Math.min(W, gx2), gy1);
    ctx.moveTo(Math.max(0, gx1), gy2); ctx.lineTo(Math.min(W, gx2), gy2);
    ctx.stroke();
    if (gx1 >= 0 && gx1 <= W) { ctx.beginPath(); ctx.moveTo(gx1, gy1); ctx.lineTo(gx1, gy2); ctx.stroke(); }
    if (gx2 >= 0 && gx2 <= W) { ctx.beginPath(); ctx.moveTo(gx2, gy1); ctx.lineTo(gx2, gy2); ctx.stroke(); }
    const bw = gx2 - gx1;
    if (bw > 16 && item.gif?.title) {
      ctx.save();
      ctx.beginPath(); ctx.rect(Math.max(0, gx1) + 2, gy1, Math.min(W, gx2) - Math.max(0, gx1) - 4, gy2 - gy1); ctx.clip();
      ctx.fillStyle = isActive ? '#25f4ee' : '#64b4ffcc';
      ctx.font = '9px Inter, sans-serif';
      ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
      ctx.fillText(item.gif.title, gx1 + bw / 2, GIF_Y + GIF_H / 2);
      ctx.restore();
    }
  });

  // ── Playhead ──
  const vid = document.getElementById('video-player');
  const ph = vid.style.display !== 'none' ? vid.currentTime : 0;
  const phX = Math.round(tlX(ph));
  if (phX >= 0 && phX <= W) {
    ctx.strokeStyle = '#fe2c55';
    ctx.lineWidth = 1.5;
    ctx.beginPath(); ctx.moveTo(phX, 0); ctx.lineTo(phX, H); ctx.stroke();
    ctx.fillStyle = '#fe2c55';
    ctx.beginPath(); ctx.moveTo(phX - 5, 0); ctx.lineTo(phX + 5, 0); ctx.lineTo(phX, 8); ctx.closePath(); ctx.fill();
  }
}

function tlRoundRect(ctx, x, y, w, h, r) {
  r = Math.min(r, w / 2, h / 2);
  ctx.beginPath();
  ctx.moveTo(x + r, y);
  ctx.arcTo(x + w, y, x + w, y + h, r);
  ctx.arcTo(x + w, y + h, x, y + h, r);
  ctx.arcTo(x, y + h, x, y, r);
  ctx.arcTo(x, y, x + w, y, r);
  ctx.closePath();
}

function tlNiceTick(pxPerSec) {
  const sec = 80 / pxPerSec;
  for (const n of [0.1, 0.25, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600]) {
    if (n >= sec) return n;
  }
  return 600;
}

function tlFmtTime(s) {
  if (s < 0) return '';
  return `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, '0')}`;
}

function tlGetHit(x, track, items) {
  const HW = 10;
  for (let i = items.length - 1; i >= 0; i--) {
    const x1 = tlX(items[i].start), x2 = tlX(items[i].end);
    const hw = Math.min(HW, Math.max(4, (x2 - x1) / 4));
    if (x < x1 - 2 || x > x2 + 2) continue;
    if (x <= x1 + hw) return { track, type: 'resize-start', segIdx: i };
    if (x >= x2 - hw) return { track, type: 'resize-end',   segIdx: i };
    return { track, type: 'move', segIdx: i };
  }
  return null;
}

// Sort segments by start to find neighbours for clamping
function tlNeighbours(segIdx) {
  const sorted = segments
    .map((s, i) => ({ start: s.start, end: s.end, i }))
    .sort((a, b) => a.start - b.start);
  const pos = sorted.findIndex(s => s.i === segIdx);
  return {
    prevEnd:   pos > 0                  ? sorted[pos - 1].end   : 0,
    nextStart: pos < sorted.length - 1  ? sorted[pos + 1].start : Infinity,
  };
}

function tlApplyDrag(clientX) {
  if (!tlDrag) return;
  const x = clientX - tlDragCanvasRect.left;

  if (tlDrag.type === 'pan') {
    tlOffset = Math.max(0, tlDrag.origOffset - (x - tlDrag.startX) / tlZoom);
    drawTimeline();
    return;
  }

  const dt = (x - tlDrag.startX) / tlZoom;
  const MIN = 0.05;
  let item;

  if (tlDrag.track === 'sub') {
    item = segments[tlDrag.segIdx];
    const { prevEnd, nextStart } = tlNeighbours(tlDrag.segIdx);
    if (tlDrag.type === 'move') {
      const dur = tlDrag.origEnd - tlDrag.origStart;
      const ns = Math.max(prevEnd, Math.min(nextStart - dur, tlDrag.origStart + dt));
      item.start = Math.round(ns * 1000) / 1000;
      item.end   = Math.round((ns + dur) * 1000) / 1000;
    } else if (tlDrag.type === 'resize-start') {
      item.start = Math.round(Math.max(prevEnd, Math.min(tlDrag.origEnd - MIN, tlDrag.origStart + dt)) * 1000) / 1000;
    } else {
      item.end = Math.round(Math.min(nextStart, Math.max(tlDrag.origStart + MIN, tlDrag.origEnd + dt)) * 1000) / 1000;
    }
  } else {
    const arr = tlDrag.track === 'emoji' ? emojiItems : gifItems;
    item = arr[tlDrag.segIdx];
    if (!item) return;
    if (tlDrag.type === 'move') {
      const dur = tlDrag.origEnd - tlDrag.origStart;
      const ns = Math.max(0, tlDrag.origStart + dt);
      item.start = Math.round(ns * 1000) / 1000;
      item.end   = Math.round((ns + dur) * 1000) / 1000;
    } else if (tlDrag.type === 'resize-start') {
      item.start = Math.round(Math.max(0, Math.min(tlDrag.origEnd - MIN, tlDrag.origStart + dt)) * 1000) / 1000;
    } else {
      item.end = Math.round(Math.max(tlDrag.origStart + MIN, tlDrag.origEnd + dt) * 1000) / 1000;
    }
  }

  drawTimeline();
}

function tlDocMove(e) { tlApplyDrag(e.clientX); }

function tlDocUp() {
  document.removeEventListener('mousemove', tlDocMove);
  document.removeEventListener('mouseup', tlDocUp);
  if (tlDrag && tlDrag.type !== 'pan' && tlDrag.track === 'sub') {
    const seg = segments[tlDrag.segIdx];
    seg.words = distributeWords(seg.text, seg.start, seg.end);
    updateTimeInputs(tlDrag.segIdx);
    renderSegmentsList();
    updatePreview();
  }
  tlDrag = null;
  const canvas = document.getElementById('timeline-canvas');
  if (canvas) canvas.style.cursor = 'crosshair';
  drawTimeline();
}

function tlMouseDown(e) {
  const x = e.offsetX, y = e.offsetY;
  const dims = tlDims();
  const { SUBS_Y, SUBS_H, EMOJI_Y, EMOJI_H, GIF_Y, GIF_H } = dims;
  tlDragCanvasRect = e.currentTarget.getBoundingClientRect();
  document.addEventListener('mousemove', tlDocMove);
  document.addEventListener('mouseup', tlDocUp);
  e.preventDefault();

  // Ruler or waveform → seek
  if (y < SUBS_Y) {
    const vid = document.getElementById('video-player');
    if (vid.style.display !== 'none') vid.currentTime = Math.max(0, tlT(x));
    tlDrag = { type: 'pan', startX: x, origOffset: tlOffset };
    e.currentTarget.style.cursor = 'grabbing';
    return;
  }

  if (y >= EMOJI_Y && y <= EMOJI_Y + EMOJI_H) {
    const hit = tlGetHit(x, 'emoji', emojiItems);
    if (hit) {
      tlDrag = { ...hit, startX: x, origStart: emojiItems[hit.segIdx].start, origEnd: emojiItems[hit.segIdx].end };
      e.currentTarget.style.cursor = hit.type === 'move' ? 'grabbing' : 'ew-resize';
    } else {
      // Add new emoji item at click position
      const t = Math.max(0, tlT(x));
      const item = { id: nextItemId++, start: t, end: t + 2, emojis: [], posX: 50, posY: 30, effect: 'bounceup' };
      emojiItems.push(item);
      drawTimeline();
      renderReactionList();
      openEmojiPickerForItem(item.id, e.clientX, e.clientY);
      tlDrag = { type: 'pan', startX: x, origOffset: tlOffset };
      e.currentTarget.style.cursor = 'grabbing';
    }
    return;
  }

  if (y >= GIF_Y && y <= GIF_Y + GIF_H) {
    const hit = tlGetHit(x, 'gif', gifItems);
    if (hit) {
      tlDrag = { ...hit, startX: x, origStart: gifItems[hit.segIdx].start, origEnd: gifItems[hit.segIdx].end };
      e.currentTarget.style.cursor = hit.type === 'move' ? 'grabbing' : 'ew-resize';
    } else {
      if (!selectedGif) { alert('Välj en GIF i sökresultaten först.'); document.removeEventListener('mousemove', tlDocMove); document.removeEventListener('mouseup', tlDocUp); return; }
      const t = Math.max(0, tlT(x));
      gifItems.push({ id: nextItemId++, start: t, end: t + 3, gif: selectedGif, posX: 50, posY: 30, size: 50, opacity: 90, effect: 'slideup' });
      drawTimeline();
      renderReactionList();
      tlDrag = { type: 'pan', startX: x, origOffset: tlOffset };
      e.currentTarget.style.cursor = 'grabbing';
    }
    return;
  }

  const inSubs = y >= SUBS_Y && y <= SUBS_Y + SUBS_H;
  const hit = inSubs ? tlGetHit(x, 'sub', segments) : null;
  if (hit) {
    tlDrag = { ...hit, startX: x, origStart: segments[hit.segIdx].start, origEnd: segments[hit.segIdx].end };
    e.currentTarget.style.cursor = hit.type === 'move' ? 'grabbing' : 'ew-resize';
  } else {
    tlDrag = { type: 'pan', startX: x, origOffset: tlOffset };
    e.currentTarget.style.cursor = 'grabbing';
  }
}

function tlMouseMove(e) {
  if (tlDrag) return;
  const x = e.offsetX, y = e.offsetY;
  const { SUBS_Y, SUBS_H, EMOJI_Y, EMOJI_H, GIF_Y, GIF_H } = tlDims();
  if (y >= SUBS_Y && y <= SUBS_Y + SUBS_H) {
    const hit = tlGetHit(x, 'sub', segments);
    tlHoverSeg = hit ? hit.segIdx : -1;
    e.currentTarget.style.cursor = !hit ? 'grab' : hit.type === 'move' ? 'grab' : 'ew-resize';
  } else if (y >= EMOJI_Y && y <= EMOJI_Y + EMOJI_H) {
    tlHoverSeg = -1;
    const hit = tlGetHit(x, 'emoji', emojiItems);
    e.currentTarget.style.cursor = hit ? (hit.type === 'move' ? 'grab' : 'ew-resize') : 'cell';
  } else if (y >= GIF_Y && y <= GIF_Y + GIF_H) {
    tlHoverSeg = -1;
    const hit = tlGetHit(x, 'gif', gifItems);
    e.currentTarget.style.cursor = hit ? (hit.type === 'move' ? 'grab' : 'ew-resize') : 'cell';
  } else {
    tlHoverSeg = -1;
    e.currentTarget.style.cursor = 'grab';
  }
  drawTimeline();
}

function tlMouseLeave() {
  if (tlDrag) return; // drag continues outside canvas via document listener
  tlHoverSeg = -1;
  drawTimeline();
}

function tlWheel(e) {
  e.preventDefault();
  if (e.ctrlKey || e.metaKey) {
    const curT = tlT(e.offsetX);
    tlZoom = Math.max(10, Math.min(3000, tlZoom * (e.deltaY < 0 ? 1.12 : 0.89)));
    tlOffset = Math.max(0, curT - e.offsetX / tlZoom);
    updateTlZoomLabel();
  } else {
    tlOffset = Math.max(0, tlOffset + e.deltaY / tlZoom);
  }
  drawTimeline();
}

function startTlRaf() {
  if (tlRafId) return;
  const loop = () => { drawTimeline(); tlRafId = requestAnimationFrame(loop); };
  tlRafId = requestAnimationFrame(loop);
}
function stopTlRaf() {
  if (tlRafId) { cancelAnimationFrame(tlRafId); tlRafId = null; }
  drawTimeline();
}

// ── Emoji reactions ───────────────────────────────────────────
function refreshEmojiRow(segIdx) {
  const row = document.getElementById(`emoji-row-${segIdx}`);
  if (!row) return;
  const emojis = segmentEmojis[segIdx] || [];
  const badges = emojis.map(e =>
    `<span class="seg-emoji-badge" onclick="event.stopPropagation();removeEmoji(${segIdx},'${e}')" title="Click to remove">
      ${e}<span class="remove-emoji">×</span>
    </span>`
  ).join('');
  row.innerHTML = badges +
    `<button type="button" class="add-emoji-btn" onclick="event.stopPropagation();openEmojiPicker(${segIdx},this)" title="Add emoji reaction">+ 😀</button>`;
}
</script>
</body>
</html>