JakgritB Claude Sonnet 4.6 commited on
Commit
fb5014d
·
1 Parent(s): 8e8e9d6

feat(frontend): full UI redesign + fix editor navigation bug

Browse files

Design:
- Dark-first design system with indigo primary, amber score badges
- fadeInUp animation on cards, shimmer on progress bar
- Hover effects: clip cards lift + scale, buttons brighten/shrink on active
- Explicit transitions (150-200ms) on all interactive elements
- Sidebar layout (320px fixed) + scrollable main column
- Editor page: sticky topbar + 60/40 video/controls split

Bug fix:
- Replace editorClip-derived routing with explicit view state
(view: 'dashboard' | 'editor') — eliminates race condition that
prevented editor from opening
- Remove problematic useEffect that reset editorClipId

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (2) hide show
  1. frontend/src/App.jsx +630 -277
  2. frontend/src/styles.css +1064 -581
frontend/src/App.jsx CHANGED
@@ -901,6 +901,9 @@ const translations = {
901
  },
902
  };
903
 
 
 
 
904
  function App() {
905
  const [profile, setProfile] = useState(defaultProfile);
906
  const [sourceMode, setSourceMode] = useState("youtube");
@@ -910,7 +913,11 @@ function App() {
910
  const [health, setHealth] = useState(null);
911
  const [error, setError] = useState("");
912
  const [isSubmitting, setIsSubmitting] = useState(false);
 
 
 
913
  const [editorClipId, setEditorClipId] = useState(null);
 
914
  const [captionStyles, setCaptionStyles] = useState(() => {
915
  try {
916
  return JSON.parse(localStorage.getItem("elevenclip.captionStyles") || "{}");
@@ -918,7 +925,9 @@ function App() {
918
  return {};
919
  }
920
  });
921
- const [language, setLanguage] = useState(() => localStorage.getItem("elevenclip.language") || "en");
 
 
922
  const [theme, setTheme] = useState(() => {
923
  const saved = localStorage.getItem("elevenclip.theme");
924
  if (saved) return saved;
@@ -939,11 +948,26 @@ function App() {
939
  () => (job?.clips || []).filter((clip) => !clip.deleted),
940
  [job?.clips]
941
  );
942
- const editorClip = activeClips.find((clip) => clip.id === editorClipId);
 
 
 
 
 
 
943
  const editorCaptionStyle = editorClip
944
  ? { ...defaultCaptionStyle, ...(captionStyles[editorClip.id] || {}) }
945
  : defaultCaptionStyle;
946
 
 
 
 
 
 
 
 
 
 
947
  useEffect(() => {
948
  document.documentElement.dataset.theme = theme;
949
  localStorage.setItem("elevenclip.theme", theme);
@@ -970,14 +994,11 @@ function App() {
970
  return () => window.clearInterval(timer);
971
  }, [job]);
972
 
973
- useEffect(() => {
974
- if (editorClipId && !editorClip) setEditorClipId(null);
975
- }, [editorClip, editorClipId]);
976
-
977
  async function submitJob(event) {
978
  event.preventDefault();
979
  setError("");
980
  setIsSubmitting(true);
 
981
  setEditorClipId(null);
982
  try {
983
  if (sourceMode === "youtube") {
@@ -1060,70 +1081,129 @@ function App() {
1060
  t={t}
1061
  />
1062
 
1063
- {editorClip ? (
1064
  <ClipEditorPage
1065
  clip={editorClip}
1066
  job={job}
1067
  t={t}
1068
- onBack={() => setEditorClipId(null)}
1069
  onPatch={patchClip}
1070
- onDelete={(clip) => patchClip(clip.id, { deleted: true })}
 
 
 
1071
  onApprove={(clip) => patchClip(clip.id, { approved: !clip.approved })}
1072
  onRegenerate={regenerateClip}
1073
  captionStyle={editorCaptionStyle}
1074
  onCaptionStyleChange={(patch) => updateCaptionStyle(editorClip.id, patch)}
1075
  />
1076
  ) : (
1077
- <div className="workspace-grid">
1078
- <section className="panel input-panel">
1079
- <ProfileForm
1080
- t={t}
1081
- profile={profile}
1082
- setProfile={setProfile}
1083
- setProfileValue={setProfileValue}
1084
- sourceMode={sourceMode}
1085
- setSourceMode={setSourceMode}
1086
- youtubeUrl={youtubeUrl}
1087
- setYoutubeUrl={setYoutubeUrl}
1088
- file={file}
1089
- setFile={setFile}
1090
- error={error}
1091
- isSubmitting={isSubmitting}
1092
- submitJob={submitJob}
1093
- />
1094
- </section>
1095
-
1096
- <section className="center-column">
1097
- <ProgressPanel job={job} t={t} />
1098
- <TranscriptPanel job={job} t={t} />
1099
- </section>
1100
-
1101
- <section className="results-column">
1102
- <ClipsPanel
1103
- clips={activeClips}
1104
- t={t}
1105
- onOpenEditor={(clip) => setEditorClipId(clip.id)}
1106
- onPatch={patchClip}
1107
- onDelete={(clip) => patchClip(clip.id, { deleted: true })}
1108
- onApprove={(clip) => patchClip(clip.id, { approved: !clip.approved })}
1109
- onRegenerate={regenerateClip}
1110
- />
1111
- </section>
1112
- </div>
1113
  )}
1114
  </main>
1115
  );
1116
  }
1117
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1118
  function AppHeader({ job, health, language, setLanguage, theme, setTheme, t }) {
1119
  const status = job?.status || "idle";
1120
  const modeLabel = health ? (health.demo_mode ? t("demoMode") : t("productionMode")) : "API";
1121
  const modeClass = health ? (health.demo_mode ? "demo" : "prod") : "";
 
1122
  return (
1123
  <header className="app-header">
1124
  <div className="brand-block">
1125
  <div className="brand-mark">
1126
- <Scissors size={22} />
1127
  </div>
1128
  <div>
1129
  <h1>ElevenClip.AI</h1>
@@ -1135,7 +1215,7 @@ function AppHeader({ job, health, language, setLanguage, theme, setTheme, t }) {
1135
  <span className={`mode-pill ${modeClass}`}>{modeLabel}</span>
1136
  <StatusPill status={status} t={t} />
1137
  <label className="toolbar-select" title={t("language")}>
1138
- <Languages size={16} />
1139
  <select value={language} onChange={(event) => setLanguage(event.target.value)}>
1140
  {LANGUAGES.map((item) => (
1141
  <option key={item.code} value={item.code}>
@@ -1150,13 +1230,23 @@ function AppHeader({ job, health, language, setLanguage, theme, setTheme, t }) {
1150
  title={t("theme")}
1151
  onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
1152
  >
1153
- {theme === "dark" ? <Sun size={17} /> : <Moon size={17} />}
1154
  </button>
1155
  </div>
1156
  </header>
1157
  );
1158
  }
1159
 
 
 
 
 
 
 
 
 
 
 
1160
  function ProfileForm({
1161
  t,
1162
  profile,
@@ -1179,7 +1269,9 @@ function ProfileForm({
1179
  <h2>{t("channelProfile")}</h2>
1180
  <p>{t("channelProfileText")}</p>
1181
  </div>
1182
- <SlidersHorizontal size={18} />
 
 
1183
  </div>
1184
 
1185
  <SelectField
@@ -1255,7 +1347,9 @@ function ProfileForm({
1255
  <div>
1256
  <h2>{t("videoInput")}</h2>
1257
  </div>
1258
- <Film size={18} />
 
 
1259
  </div>
1260
 
1261
  <div className="segmented">
@@ -1264,7 +1358,7 @@ function ProfileForm({
1264
  className={sourceMode === "youtube" ? "active" : ""}
1265
  onClick={() => setSourceMode("youtube")}
1266
  >
1267
- <LinkIcon size={16} />
1268
  {t("youtube")}
1269
  </button>
1270
  <button
@@ -1272,7 +1366,7 @@ function ProfileForm({
1272
  className={sourceMode === "upload" ? "active" : ""}
1273
  onClick={() => setSourceMode("upload")}
1274
  >
1275
- <Upload size={16} />
1276
  {t("upload")}
1277
  </button>
1278
  </div>
@@ -1298,22 +1392,23 @@ function ProfileForm({
1298
  )}
1299
 
1300
  {error && <div className="error-box">{error}</div>}
 
1301
  <button
1302
  className="primary-button"
 
1303
  disabled={isSubmitting || (sourceMode === "youtube" ? !youtubeUrl : !file)}
1304
  type="submit"
1305
  >
1306
- {isSubmitting ? <Loader2 className="spin" size={18} /> : <Wand2 size={18} />}
1307
  {t("startPipeline")}
1308
  </button>
1309
  </form>
1310
  );
1311
  }
1312
 
1313
- function StatusPill({ status, t }) {
1314
- return <span className={`status-pill ${status}`}>{t(status)}</span>;
1315
- }
1316
-
1317
  function ProgressPanel({ job, t }) {
1318
  const progress = Math.round((job?.progress || 0) * 100);
1319
  const steps = [
@@ -1342,15 +1437,17 @@ function ProgressPanel({ job, t }) {
1342
  </div>
1343
  <strong className="progress-percent">{progress}%</strong>
1344
  </div>
 
1345
  <div className="progress-track">
1346
  <div className="progress-bar" style={{ width: `${progress}%` }} />
1347
  </div>
 
1348
  <div className="step-list" aria-label={t("currentStep")}>
1349
  {steps.map(([id, label], index) => (
1350
  <div
1351
  key={id}
1352
- className={`step-item ${index < stepIndex ? "done" : ""} ${
1353
- index === stepIndex ? "active" : ""
1354
  }`}
1355
  >
1356
  <span>{index + 1}</span>
@@ -1358,7 +1455,10 @@ function ProgressPanel({ job, t }) {
1358
  </div>
1359
  ))}
1360
  </div>
1361
- <p className="helper-text">{t("progressNote")}</p>
 
 
 
1362
 
1363
  {job?.timings && Object.keys(job.timings).length > 0 && (
1364
  <div className="timing-grid">
@@ -1370,11 +1470,15 @@ function ProgressPanel({ job, t }) {
1370
  ))}
1371
  </div>
1372
  )}
1373
- {job?.error && <div className="error-box">{job.error}</div>}
 
1374
  </section>
1375
  );
1376
  }
1377
 
 
 
 
1378
  function TranscriptPanel({ job, t }) {
1379
  return (
1380
  <section className="panel transcript-panel">
@@ -1383,14 +1487,16 @@ function TranscriptPanel({ job, t }) {
1383
  <h2>{t("transcript")}</h2>
1384
  {!job?.transcript?.length && <p>{t("transcriptEmpty")}</p>}
1385
  </div>
1386
- <Captions size={18} />
 
 
1387
  </div>
1388
  {job?.transcript?.length > 0 && (
1389
  <div className="transcript-list">
1390
  {job.transcript.map((segment) => (
1391
  <div key={segment.id} className="transcript-row">
1392
  <span>
1393
- {formatTime(segment.start_seconds)} - {formatTime(segment.end_seconds)}
1394
  </span>
1395
  <p>{segment.text}</p>
1396
  </div>
@@ -1401,6 +1507,9 @@ function TranscriptPanel({ job, t }) {
1401
  );
1402
  }
1403
 
 
 
 
1404
  function ClipsPanel({ clips, t, onOpenEditor, onPatch, onDelete, onApprove, onRegenerate }) {
1405
  return (
1406
  <section className="panel clips-panel">
@@ -1411,12 +1520,14 @@ function ClipsPanel({ clips, t, onOpenEditor, onPatch, onDelete, onApprove, onRe
1411
  {clips.length} {t("readyClips")}
1412
  </p>
1413
  </div>
1414
- <PanelRightOpen size={18} />
 
 
1415
  </div>
1416
 
1417
  {clips.length === 0 ? (
1418
  <div className="empty-state">
1419
- <Film size={34} />
1420
  <h3>{t("noClips")}</h3>
1421
  <p>{t("noClipsText")}</p>
1422
  </div>
@@ -1440,55 +1551,104 @@ function ClipsPanel({ clips, t, onOpenEditor, onPatch, onDelete, onApprove, onRe
1440
  );
1441
  }
1442
 
 
 
 
1443
  function ClipCard({ clip, t, onOpenEditor, onPatch, onDelete, onApprove, onRegenerate }) {
1444
  const duration = Math.max(1, clip.end_seconds - clip.start_seconds);
 
1445
  return (
1446
  <article className="clip-card">
1447
  <div className="clip-video">
1448
- {clip.video_url ? <video controls src={`${API_BASE}${clip.video_url}`} /> : <Film size={34} />}
 
 
 
 
1449
  </div>
 
1450
  <div className="clip-body">
1451
- <div className="clip-title-row">
1452
- <div>
1453
- <h3>{clip.title}</h3>
1454
- <p>{clip.reason}</p>
 
1455
  </div>
1456
- <span className="score">
1457
- <Gauge size={14} />
1458
  {Math.round(clip.score)}
1459
  </span>
1460
  </div>
1461
 
1462
- <div className="metric-row">
 
1463
  <span>
1464
- <Clock3 size={14} />
1465
  {duration.toFixed(1)}s
1466
  </span>
1467
  <span>
1468
- {formatTime(clip.start_seconds)} - {formatTime(clip.end_seconds)}
1469
  </span>
1470
  </div>
1471
 
1472
- <p className="subtitle-snippet">{clip.subtitle_text}</p>
 
 
 
1473
 
 
1474
  <div className="clip-actions">
1475
- <button className="action-primary" type="button" title={t("openEditor")} onClick={() => onOpenEditor(clip)}>
1476
- <PanelRightOpen size={16} />
 
 
 
 
 
 
 
1477
  {t("openEditor")}
1478
  </button>
1479
- <button className="action-approve" type="button" title={t("approve")} onClick={() => onApprove(clip)}>
1480
- <Check size={16} />
1481
- {clip.approved ? t("approved") : t("approve")}
 
 
 
 
 
 
1482
  </button>
1483
- <button type="button" title={t("regenerate")} onClick={() => onRegenerate(clip)}>
1484
- <RefreshCcw size={16} />
 
 
 
 
 
 
 
1485
  </button>
1486
- <button type="button" title={t("delete")} onClick={() => onDelete(clip)}>
1487
- <Trash2 size={16} />
 
 
 
 
 
 
 
1488
  </button>
 
 
1489
  {clip.download_url && (
1490
- <a className="download-button" href={`${API_BASE}${clip.download_url}`} title={t("download")}>
1491
- <Download size={16} />
 
 
 
 
 
1492
  </a>
1493
  )}
1494
  </div>
@@ -1497,6 +1657,9 @@ function ClipCard({ clip, t, onOpenEditor, onPatch, onDelete, onApprove, onRegen
1497
  );
1498
  }
1499
 
 
 
 
1500
  function ClipEditorPage({
1501
  clip,
1502
  job,
@@ -1514,13 +1677,18 @@ function ClipEditorPage({
1514
  const metadataModel = clip.metadata?.model || "unknown";
1515
  const sourceKind = job?.source?.kind || "video";
1516
  const activeCue = cues[0]?.text || clip.subtitle_text || clip.title;
 
1517
  const timelineDuration = Math.max(
1518
  clip.end_seconds,
1519
  ...(job?.transcript || []).map((segment) => Number(segment.end_seconds || 0)),
1520
  1
1521
  );
1522
  const rangeLeft = clamp((clip.start_seconds / timelineDuration) * 100, 0, 100);
1523
- const rangeWidth = clamp(((clip.end_seconds - clip.start_seconds) / timelineDuration) * 100, 1, 100);
 
 
 
 
1524
 
1525
  function patchCue(index, text) {
1526
  const next = cues.map((cue, cueIndex) => (cueIndex === index ? { ...cue, text } : cue));
@@ -1546,108 +1714,114 @@ function ClipEditorPage({
1546
  }
1547
 
1548
  function setClipLength(seconds) {
1549
- onPatch(clip.id, { end_seconds: roundTime(clamp(clip.start_seconds + seconds, clip.start_seconds + 1, timelineDuration)) });
 
 
 
 
1550
  }
1551
 
1552
  return (
1553
  <div className="editor-shell">
 
1554
  <div className="editor-topbar">
1555
  <button className="ghost-button" type="button" onClick={onBack}>
1556
- <ArrowLeft size={17} />
1557
  {t("backToDashboard")}
1558
  </button>
1559
- <div>
1560
- <h2>{t("editor")}</h2>
1561
- <p>{t("editorText")}</p>
 
 
 
 
1562
  </div>
1563
- </div>
1564
 
1565
- <div className="editor-grid">
1566
- <aside className="tool-rail" aria-label={t("editorTools")}>
1567
- <button type="button" className="active" title={t("toolSelect")}>
1568
- <PanelRightOpen size={18} />
1569
- <span>{t("toolSelect")}</span>
1570
- </button>
1571
- <button type="button" title={t("toolTrim")}>
1572
- <Scissors size={18} />
1573
- <span>{t("toolTrim")}</span>
1574
- </button>
1575
- <button type="button" title={t("toolCaptions")}>
1576
- <Captions size={18} />
1577
- <span>{t("toolCaptions")}</span>
1578
- </button>
1579
- <button type="button" title={t("toolStyle")}>
1580
- <SlidersHorizontal size={18} />
1581
- <span>{t("toolStyle")}</span>
1582
- </button>
1583
- <button type="button" title={t("toolExport")}>
1584
- <Download size={18} />
1585
- <span>{t("toolExport")}</span>
1586
  </button>
1587
- </aside>
1588
-
1589
- <section className="panel editor-main">
1590
- <div className="panel-heading compact">
1591
- <div>
1592
- <h2>{t("preview")}</h2>
1593
- <p>
1594
- {formatTime(clip.start_seconds)} - {formatTime(clip.end_seconds)}
1595
- </p>
1596
- </div>
1597
- <Film size={18} />
1598
- </div>
 
 
 
 
 
 
1599
  <div className="editor-preview">
1600
- {clip.video_url ? <video controls src={`${API_BASE}${clip.video_url}`} /> : <Film size={44} />}
 
 
 
 
1601
  <CaptionPreview text={activeCue} settings={captionStyle} />
1602
  </div>
1603
 
1604
- <div className="range-editor">
 
1605
  <div className="panel-heading compact">
1606
  <div>
1607
  <h2>{t("clipRange")}</h2>
1608
  <p>
1609
- {formatTime(clip.start_seconds)} - {formatTime(clip.end_seconds)}
 
1610
  </p>
1611
  </div>
1612
- <Scissors size={18} />
 
 
1613
  </div>
 
 
1614
  <div className="editor-toolbox">
1615
- <span>{t("editorTools")}</span>
1616
- <button type="button" onClick={() => updateStart(clip.start_seconds - 0.5)}>
1617
- {t("trimStartBack")}
1618
- </button>
1619
- <button type="button" onClick={() => updateStart(clip.start_seconds + 0.5)}>
1620
- {t("trimStartForward")}
1621
- </button>
1622
- <button type="button" onClick={() => updateEnd(clip.end_seconds - 0.5)}>
1623
- {t("trimEndBack")}
1624
- </button>
1625
- <button type="button" onClick={() => updateEnd(clip.end_seconds + 0.5)}>
1626
- {t("trimEndForward")}
1627
- </button>
1628
- <button type="button" onClick={() => moveClip(-1)}>
1629
- {t("moveClipLeft")}
1630
- </button>
1631
- <button type="button" onClick={() => moveClip(1)}>
1632
- {t("moveClipRight")}
1633
- </button>
1634
- <button type="button" onClick={() => setClipLength(30)}>
1635
- {t("setClipLength30")}
1636
- </button>
1637
- <button type="button" onClick={() => setClipLength(60)}>
1638
- {t("setClipLength60")}
1639
- </button>
1640
- <button type="button" onClick={() => setClipLength(90)}>
1641
- {t("setClipLength90")}
1642
- </button>
1643
  </div>
 
 
1644
  <div className="timeline-visual">
1645
  <div className="timeline-fill" />
1646
- <div className="timeline-window" style={{ left: `${rangeLeft}%`, width: `${rangeWidth}%` }} />
 
 
 
1647
  {Array.from({ length: 9 }).map((_, index) => (
1648
  <span key={index} style={{ left: `${index * 12.5}%` }} />
1649
  ))}
1650
  </div>
 
 
1651
  <div className="range-sliders">
1652
  <label>
1653
  <span>{t("rangeStart")}</span>
@@ -1672,6 +1846,8 @@ function ClipEditorPage({
1672
  />
1673
  </label>
1674
  </div>
 
 
1675
  <div className="timeline">
1676
  <NumberField
1677
  label={t("start")}
@@ -1687,21 +1863,25 @@ function ClipEditorPage({
1687
  </div>
1688
  </div>
1689
 
 
1690
  <TimelineTracks clip={clip} cues={cues} duration={timelineDuration} t={t} />
1691
 
1692
- <div className="subtitle-editor">
 
1693
  <div className="panel-heading compact">
1694
  <div>
1695
  <h2>{t("subtitleCues")}</h2>
1696
  <p>{t("subtitleCueHelp")}</p>
1697
  </div>
1698
- <Captions size={18} />
 
 
1699
  </div>
1700
  <div className="cue-list">
1701
  {cues.map((cue, index) => (
1702
  <div className="cue-row" key={`${cue.start_seconds}-${index}`}>
1703
  <span>
1704
- {formatTime(cue.start_seconds)} - {formatTime(cue.end_seconds)}
1705
  </span>
1706
  <textarea
1707
  defaultValue={cue.text}
@@ -1714,72 +1894,181 @@ function ClipEditorPage({
1714
  ))}
1715
  </div>
1716
  </div>
1717
- </section>
1718
 
1719
- <aside className="panel inspector-panel">
1720
- <div className="panel-heading compact">
1721
- <div>
1722
- <h2>{t("inspector")}</h2>
1723
- </div>
1724
- <Sparkles size={18} />
1725
- </div>
1726
- <dl className="inspector-list">
1727
- <div>
1728
- <dt>{t("title")}</dt>
1729
- <dd>{clip.title}</dd>
1730
- </div>
1731
- <div>
1732
- <dt>{t("reason")}</dt>
1733
- <dd>{clip.reason}</dd>
1734
- </div>
1735
- <div>
1736
- <dt>{t("score")}</dt>
1737
- <dd>{Math.round(clip.score)}</dd>
1738
- </div>
1739
- <div>
1740
- <dt>{t("status")}</dt>
1741
- <dd>{clip.approved ? t("approved") : t("notApproved")}</dd>
1742
- </div>
1743
- <div>
1744
- <dt>{t("model")}</dt>
1745
- <dd>{metadataModel}</dd>
1746
  </div>
1747
- <div>
1748
- <dt>{t("source")}</dt>
1749
- <dd>{t(`source_${sourceKind}`)}</dd>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1750
  </div>
1751
- </dl>
1752
-
1753
- <div className="inspector-actions">
1754
- <button type="button" onClick={() => onApprove(clip)}>
1755
- <Check size={16} />
1756
- {clip.approved ? t("approved") : t("approve")}
1757
- </button>
1758
- <button type="button" onClick={() => onRegenerate(clip)}>
1759
- <RefreshCcw size={16} />
1760
- {t("regenerate")}
1761
- </button>
1762
- {clip.download_url && (
1763
- <a href={`${API_BASE}${clip.download_url}`}>
1764
- <Download size={16} />
1765
- {t("download")}
1766
- </a>
1767
- )}
1768
- <button className="danger" type="button" onClick={() => onDelete(clip)}>
1769
- <Trash2 size={16} />
1770
- {t("delete")}
1771
- </button>
1772
  </div>
1773
 
1774
- <CaptionStylePanel t={t} settings={captionStyle} onChange={onCaptionStyleChange} />
 
 
 
 
 
 
 
 
 
 
 
1775
 
1776
- <TranscriptMini transcript={job?.transcript || []} clip={clip} t={t} />
1777
- </aside>
 
 
 
1778
  </div>
1779
  </div>
1780
  );
1781
  }
1782
 
 
 
 
1783
  function CaptionPreview({ text, settings }) {
1784
  const words = text.split(/\s+/).filter(Boolean);
1785
  const litCount = Math.max(1, Math.ceil(words.length * 0.45));
@@ -1806,24 +2095,33 @@ function CaptionPreview({ text, settings }) {
1806
  );
1807
  }
1808
 
 
 
 
1809
  function TimelineTracks({ clip, cues, duration, t }) {
1810
  const clipLeft = clamp((clip.start_seconds / duration) * 100, 0, 100);
1811
  const clipWidth = clamp(((clip.end_seconds - clip.start_seconds) / duration) * 100, 4, 100);
1812
  const subtitleItems = cues.slice(0, 8);
 
1813
  return (
1814
- <div className="timeline-workbench">
1815
  <div className="panel-heading compact">
1816
  <div>
1817
  <h2>{t("timelineTracks")}</h2>
1818
  <p>
1819
- {formatTime(clip.start_seconds)} - {formatTime(clip.end_seconds)}
1820
  </p>
1821
  </div>
1822
- <Film size={18} />
 
 
1823
  </div>
1824
  <div className="track-stack">
1825
  <TrackRow label={t("videoTrack")}>
1826
- <div className="track-clip video" style={{ left: `${clipLeft}%`, width: `${clipWidth}%` }}>
 
 
 
1827
  {clip.title}
1828
  </div>
1829
  </TrackRow>
@@ -1833,8 +2131,16 @@ function TimelineTracks({ clip, cues, duration, t }) {
1833
  className="track-clip subtitle"
1834
  key={`${cue.start_seconds}-${index}`}
1835
  style={{
1836
- left: `${clamp(((clip.start_seconds + cue.start_seconds) / duration) * 100, 0, 100)}%`,
1837
- width: `${clamp(((cue.end_seconds - cue.start_seconds) / duration) * 100, 3, 45)}%`,
 
 
 
 
 
 
 
 
1838
  }}
1839
  >
1840
  {cue.text}
@@ -1862,16 +2168,12 @@ function TrackRow({ label, children }) {
1862
  );
1863
  }
1864
 
 
 
 
1865
  function CaptionStylePanel({ t, settings, onChange }) {
1866
  return (
1867
- <section className="caption-style-panel">
1868
- <div className="panel-heading compact">
1869
- <div>
1870
- <h2>{t("captionStyle")}</h2>
1871
- </div>
1872
- <Captions size={18} />
1873
- </div>
1874
-
1875
  <div className="preset-row">
1876
  {Object.entries(captionPresets).map(([key, preset]) => (
1877
  <button type="button" key={key} onClick={() => onChange(preset)}>
@@ -1902,7 +2204,11 @@ function CaptionStylePanel({ t, settings, onChange }) {
1902
  </div>
1903
 
1904
  <div className="color-grid">
1905
- <ColorField label={t("fillColor")} value={settings.fillColor} onChange={(fillColor) => onChange({ fillColor })} />
 
 
 
 
1906
  <ColorField
1907
  label={t("strokeColor")}
1908
  value={settings.strokeColor}
@@ -1910,7 +2216,13 @@ function CaptionStylePanel({ t, settings, onChange }) {
1910
  />
1911
  </div>
1912
 
1913
- <RangeControl label={t("fontSize")} value={settings.fontSize} min={24} max={64} onChange={(fontSize) => onChange({ fontSize })} />
 
 
 
 
 
 
1914
  <RangeControl
1915
  label={t("strokeWidth")}
1916
  value={settings.strokeWidth}
@@ -1925,49 +2237,31 @@ function CaptionStylePanel({ t, settings, onChange }) {
1925
  max={38}
1926
  onChange={(position) => onChange({ position })}
1927
  />
1928
- </section>
1929
- );
1930
- }
1931
-
1932
- function ColorField({ label, value, onChange }) {
1933
- return (
1934
- <label className="color-field">
1935
- <span>{label}</span>
1936
- <input type="color" value={value} onChange={(event) => onChange(event.target.value)} />
1937
- </label>
1938
- );
1939
- }
1940
-
1941
- function RangeControl({ label, value, min, max, onChange }) {
1942
- return (
1943
- <label className="range-control">
1944
- <span>
1945
- {label}
1946
- <strong>{value}</strong>
1947
- </span>
1948
- <input
1949
- type="range"
1950
- min={min}
1951
- max={max}
1952
- step="1"
1953
- value={value}
1954
- onChange={(event) => onChange(Number(event.target.value))}
1955
- />
1956
- </label>
1957
  );
1958
  }
1959
 
 
 
 
1960
  function TranscriptMini({ transcript, clip, t }) {
1961
  const rows = transcript.filter(
1962
- (segment) => segment.end_seconds >= clip.start_seconds && segment.start_seconds <= clip.end_seconds
 
1963
  );
 
1964
  return (
1965
  <div className="mini-transcript">
1966
  <h3>{t("transcript")}</h3>
 
 
 
 
 
1967
  {rows.map((segment) => (
1968
  <div key={segment.id}>
1969
  <span>
1970
- {formatTime(segment.start_seconds)} - {formatTime(segment.end_seconds)}
1971
  </span>
1972
  <p>{segment.text}</p>
1973
  </div>
@@ -1976,6 +2270,9 @@ function TranscriptMini({ transcript, clip, t }) {
1976
  );
1977
  }
1978
 
 
 
 
1979
  function TextField({ label, value, onChange, placeholder, helper }) {
1980
  return (
1981
  <label className="field-block">
@@ -2010,7 +2307,11 @@ function SelectField({ label, value, onChange, options, helper }) {
2010
  return (
2011
  <label className="field-block">
2012
  <span className="field-label">{label}</span>
2013
- <select className="text-input" value={value} onChange={(event) => onChange(event.target.value)}>
 
 
 
 
2014
  {options.map((option) => (
2015
  <option key={option.value} value={option.value}>
2016
  {option.label}
@@ -2025,18 +2326,62 @@ function SelectField({ label, value, onChange, options, helper }) {
2025
  function NumberField({ label, value, onChange }) {
2026
  return (
2027
  <label>
2028
- <span>{label}</span>
 
 
2029
  <input
2030
  type="number"
2031
  min="0"
2032
  step="0.5"
2033
  value={value}
2034
  onChange={(event) => onChange(event.target.value)}
 
 
 
 
 
 
 
 
 
 
 
2035
  />
2036
  </label>
2037
  );
2038
  }
2039
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2040
  async function fetchJson(path, options) {
2041
  const response = await fetch(`${API_BASE}${path}`, options);
2042
  if (!response.ok) {
@@ -2053,14 +2398,16 @@ async function fetchJson(path, options) {
2053
  }
2054
 
2055
  function getSubtitleCues(clip, duration, settings = defaultCaptionStyle) {
2056
- return splitSubtitleText(clip.subtitle_text || "", settings.cueDensity).map((text, index, all) => {
2057
- const cueDuration = duration / Math.max(all.length, 1);
2058
- return {
2059
- start_seconds: roundTime(index * cueDuration),
2060
- end_seconds: roundTime((index + 1) * cueDuration),
2061
- text,
2062
- };
2063
- });
 
 
2064
  }
2065
 
2066
  function splitSubtitleText(text, density = "short") {
@@ -2085,8 +2432,14 @@ function splitSubtitleText(text, density = "short") {
2085
  let current = [];
2086
  words.forEach((word) => {
2087
  const candidate = [...current, word].join(" ");
2088
- const punctuationBreak = current.length > 0 && /[,.!?;:]$/.test(current[current.length - 1]);
2089
- if (current.length > 0 && (candidate.length > limit.chars || current.length >= limit.words || punctuationBreak)) {
 
 
 
 
 
 
2090
  chunks.push(current.join(" "));
2091
  current = [word];
2092
  } else {
 
901
  },
902
  };
903
 
904
+ // ============================================================
905
+ // App root
906
+ // ============================================================
907
  function App() {
908
  const [profile, setProfile] = useState(defaultProfile);
909
  const [sourceMode, setSourceMode] = useState("youtube");
 
913
  const [health, setHealth] = useState(null);
914
  const [error, setError] = useState("");
915
  const [isSubmitting, setIsSubmitting] = useState(false);
916
+
917
+ // Explicit view state — fixes the editor navigation bug
918
+ const [view, setView] = useState("dashboard"); // 'dashboard' | 'editor'
919
  const [editorClipId, setEditorClipId] = useState(null);
920
+
921
  const [captionStyles, setCaptionStyles] = useState(() => {
922
  try {
923
  return JSON.parse(localStorage.getItem("elevenclip.captionStyles") || "{}");
 
925
  return {};
926
  }
927
  });
928
+ const [language, setLanguage] = useState(
929
+ () => localStorage.getItem("elevenclip.language") || "en"
930
+ );
931
  const [theme, setTheme] = useState(() => {
932
  const saved = localStorage.getItem("elevenclip.theme");
933
  if (saved) return saved;
 
948
  () => (job?.clips || []).filter((clip) => !clip.deleted),
949
  [job?.clips]
950
  );
951
+
952
+ // Derive the editor clip from view state — no useEffect reset needed
953
+ const editorClip =
954
+ view === "editor" && editorClipId
955
+ ? activeClips.find((clip) => clip.id === editorClipId) || null
956
+ : null;
957
+
958
  const editorCaptionStyle = editorClip
959
  ? { ...defaultCaptionStyle, ...(captionStyles[editorClip.id] || {}) }
960
  : defaultCaptionStyle;
961
 
962
+ // Navigation helpers
963
+ function openEditor(clip) {
964
+ setEditorClipId(clip.id);
965
+ setView("editor");
966
+ }
967
+ function closeEditor() {
968
+ setView("dashboard");
969
+ }
970
+
971
  useEffect(() => {
972
  document.documentElement.dataset.theme = theme;
973
  localStorage.setItem("elevenclip.theme", theme);
 
994
  return () => window.clearInterval(timer);
995
  }, [job]);
996
 
 
 
 
 
997
  async function submitJob(event) {
998
  event.preventDefault();
999
  setError("");
1000
  setIsSubmitting(true);
1001
+ setView("dashboard");
1002
  setEditorClipId(null);
1003
  try {
1004
  if (sourceMode === "youtube") {
 
1081
  t={t}
1082
  />
1083
 
1084
+ {view === "editor" && editorClipId && editorClip ? (
1085
  <ClipEditorPage
1086
  clip={editorClip}
1087
  job={job}
1088
  t={t}
1089
+ onBack={closeEditor}
1090
  onPatch={patchClip}
1091
+ onDelete={(clip) => {
1092
+ patchClip(clip.id, { deleted: true });
1093
+ closeEditor();
1094
+ }}
1095
  onApprove={(clip) => patchClip(clip.id, { approved: !clip.approved })}
1096
  onRegenerate={regenerateClip}
1097
  captionStyle={editorCaptionStyle}
1098
  onCaptionStyleChange={(patch) => updateCaptionStyle(editorClip.id, patch)}
1099
  />
1100
  ) : (
1101
+ <Dashboard
1102
+ profile={profile}
1103
+ setProfile={setProfile}
1104
+ setProfileValue={setProfileValue}
1105
+ sourceMode={sourceMode}
1106
+ setSourceMode={setSourceMode}
1107
+ youtubeUrl={youtubeUrl}
1108
+ setYoutubeUrl={setYoutubeUrl}
1109
+ file={file}
1110
+ setFile={setFile}
1111
+ error={error}
1112
+ isSubmitting={isSubmitting}
1113
+ submitJob={submitJob}
1114
+ job={job}
1115
+ activeClips={activeClips}
1116
+ t={t}
1117
+ onOpenEditor={openEditor}
1118
+ onPatch={patchClip}
1119
+ onDelete={(clip) => patchClip(clip.id, { deleted: true })}
1120
+ onApprove={(clip) => patchClip(clip.id, { approved: !clip.approved })}
1121
+ onRegenerate={regenerateClip}
1122
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1123
  )}
1124
  </main>
1125
  );
1126
  }
1127
 
1128
+ // ============================================================
1129
+ // Dashboard layout
1130
+ // ============================================================
1131
+ function Dashboard({
1132
+ profile,
1133
+ setProfile,
1134
+ setProfileValue,
1135
+ sourceMode,
1136
+ setSourceMode,
1137
+ youtubeUrl,
1138
+ setYoutubeUrl,
1139
+ file,
1140
+ setFile,
1141
+ error,
1142
+ isSubmitting,
1143
+ submitJob,
1144
+ job,
1145
+ activeClips,
1146
+ t,
1147
+ onOpenEditor,
1148
+ onPatch,
1149
+ onDelete,
1150
+ onApprove,
1151
+ onRegenerate,
1152
+ }) {
1153
+ return (
1154
+ <div className="workspace-grid">
1155
+ {/* Sidebar — profile form */}
1156
+ <div className="sidebar-column">
1157
+ <section className="panel input-panel">
1158
+ <ProfileForm
1159
+ t={t}
1160
+ profile={profile}
1161
+ setProfile={setProfile}
1162
+ setProfileValue={setProfileValue}
1163
+ sourceMode={sourceMode}
1164
+ setSourceMode={setSourceMode}
1165
+ youtubeUrl={youtubeUrl}
1166
+ setYoutubeUrl={setYoutubeUrl}
1167
+ file={file}
1168
+ setFile={setFile}
1169
+ error={error}
1170
+ isSubmitting={isSubmitting}
1171
+ submitJob={submitJob}
1172
+ />
1173
+ </section>
1174
+ </div>
1175
+
1176
+ {/* Main content column */}
1177
+ <div className="main-column">
1178
+ <ProgressPanel job={job} t={t} />
1179
+ <TranscriptPanel job={job} t={t} />
1180
+ <ClipsPanel
1181
+ clips={activeClips}
1182
+ t={t}
1183
+ onOpenEditor={onOpenEditor}
1184
+ onPatch={onPatch}
1185
+ onDelete={onDelete}
1186
+ onApprove={onApprove}
1187
+ onRegenerate={onRegenerate}
1188
+ />
1189
+ </div>
1190
+ </div>
1191
+ );
1192
+ }
1193
+
1194
+ // ============================================================
1195
+ // App Header
1196
+ // ============================================================
1197
  function AppHeader({ job, health, language, setLanguage, theme, setTheme, t }) {
1198
  const status = job?.status || "idle";
1199
  const modeLabel = health ? (health.demo_mode ? t("demoMode") : t("productionMode")) : "API";
1200
  const modeClass = health ? (health.demo_mode ? "demo" : "prod") : "";
1201
+
1202
  return (
1203
  <header className="app-header">
1204
  <div className="brand-block">
1205
  <div className="brand-mark">
1206
+ <Scissors size={20} />
1207
  </div>
1208
  <div>
1209
  <h1>ElevenClip.AI</h1>
 
1215
  <span className={`mode-pill ${modeClass}`}>{modeLabel}</span>
1216
  <StatusPill status={status} t={t} />
1217
  <label className="toolbar-select" title={t("language")}>
1218
+ <Languages size={14} />
1219
  <select value={language} onChange={(event) => setLanguage(event.target.value)}>
1220
  {LANGUAGES.map((item) => (
1221
  <option key={item.code} value={item.code}>
 
1230
  title={t("theme")}
1231
  onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
1232
  >
1233
+ {theme === "dark" ? <Sun size={16} /> : <Moon size={16} />}
1234
  </button>
1235
  </div>
1236
  </header>
1237
  );
1238
  }
1239
 
1240
+ // ============================================================
1241
+ // Status pill
1242
+ // ============================================================
1243
+ function StatusPill({ status, t }) {
1244
+ return <span className={`status-pill ${status}`}>{t(status)}</span>;
1245
+ }
1246
+
1247
+ // ============================================================
1248
+ // Profile form (sidebar)
1249
+ // ============================================================
1250
  function ProfileForm({
1251
  t,
1252
  profile,
 
1269
  <h2>{t("channelProfile")}</h2>
1270
  <p>{t("channelProfileText")}</p>
1271
  </div>
1272
+ <div className="panel-heading-icon">
1273
+ <SlidersHorizontal size={16} />
1274
+ </div>
1275
  </div>
1276
 
1277
  <SelectField
 
1347
  <div>
1348
  <h2>{t("videoInput")}</h2>
1349
  </div>
1350
+ <div className="panel-heading-icon">
1351
+ <Film size={16} />
1352
+ </div>
1353
  </div>
1354
 
1355
  <div className="segmented">
 
1358
  className={sourceMode === "youtube" ? "active" : ""}
1359
  onClick={() => setSourceMode("youtube")}
1360
  >
1361
+ <LinkIcon size={14} />
1362
  {t("youtube")}
1363
  </button>
1364
  <button
 
1366
  className={sourceMode === "upload" ? "active" : ""}
1367
  onClick={() => setSourceMode("upload")}
1368
  >
1369
+ <Upload size={14} />
1370
  {t("upload")}
1371
  </button>
1372
  </div>
 
1392
  )}
1393
 
1394
  {error && <div className="error-box">{error}</div>}
1395
+
1396
  <button
1397
  className="primary-button"
1398
+ style={{ width: "100%" }}
1399
  disabled={isSubmitting || (sourceMode === "youtube" ? !youtubeUrl : !file)}
1400
  type="submit"
1401
  >
1402
+ {isSubmitting ? <Loader2 className="spin" size={16} /> : <Wand2 size={16} />}
1403
  {t("startPipeline")}
1404
  </button>
1405
  </form>
1406
  );
1407
  }
1408
 
1409
+ // ============================================================
1410
+ // Progress panel
1411
+ // ============================================================
 
1412
  function ProgressPanel({ job, t }) {
1413
  const progress = Math.round((job?.progress || 0) * 100);
1414
  const steps = [
 
1437
  </div>
1438
  <strong className="progress-percent">{progress}%</strong>
1439
  </div>
1440
+
1441
  <div className="progress-track">
1442
  <div className="progress-bar" style={{ width: `${progress}%` }} />
1443
  </div>
1444
+
1445
  <div className="step-list" aria-label={t("currentStep")}>
1446
  {steps.map(([id, label], index) => (
1447
  <div
1448
  key={id}
1449
+ className={`step-item${index < stepIndex ? " done" : ""}${
1450
+ index === stepIndex ? " active" : ""
1451
  }`}
1452
  >
1453
  <span>{index + 1}</span>
 
1455
  </div>
1456
  ))}
1457
  </div>
1458
+
1459
+ <p className="helper-text" style={{ marginTop: 12 }}>
1460
+ {t("progressNote")}
1461
+ </p>
1462
 
1463
  {job?.timings && Object.keys(job.timings).length > 0 && (
1464
  <div className="timing-grid">
 
1470
  ))}
1471
  </div>
1472
  )}
1473
+
1474
+ {job?.error && <div className="error-box" style={{ marginTop: 12 }}>{job.error}</div>}
1475
  </section>
1476
  );
1477
  }
1478
 
1479
+ // ============================================================
1480
+ // Transcript panel
1481
+ // ============================================================
1482
  function TranscriptPanel({ job, t }) {
1483
  return (
1484
  <section className="panel transcript-panel">
 
1487
  <h2>{t("transcript")}</h2>
1488
  {!job?.transcript?.length && <p>{t("transcriptEmpty")}</p>}
1489
  </div>
1490
+ <div className="panel-heading-icon">
1491
+ <Captions size={16} />
1492
+ </div>
1493
  </div>
1494
  {job?.transcript?.length > 0 && (
1495
  <div className="transcript-list">
1496
  {job.transcript.map((segment) => (
1497
  <div key={segment.id} className="transcript-row">
1498
  <span>
1499
+ {formatTime(segment.start_seconds)} {formatTime(segment.end_seconds)}
1500
  </span>
1501
  <p>{segment.text}</p>
1502
  </div>
 
1507
  );
1508
  }
1509
 
1510
+ // ============================================================
1511
+ // Clips panel
1512
+ // ============================================================
1513
  function ClipsPanel({ clips, t, onOpenEditor, onPatch, onDelete, onApprove, onRegenerate }) {
1514
  return (
1515
  <section className="panel clips-panel">
 
1520
  {clips.length} {t("readyClips")}
1521
  </p>
1522
  </div>
1523
+ <div className="panel-heading-icon">
1524
+ <Film size={16} />
1525
+ </div>
1526
  </div>
1527
 
1528
  {clips.length === 0 ? (
1529
  <div className="empty-state">
1530
+ <Film size={32} />
1531
  <h3>{t("noClips")}</h3>
1532
  <p>{t("noClipsText")}</p>
1533
  </div>
 
1551
  );
1552
  }
1553
 
1554
+ // ============================================================
1555
+ // Clip card
1556
+ // ============================================================
1557
  function ClipCard({ clip, t, onOpenEditor, onPatch, onDelete, onApprove, onRegenerate }) {
1558
  const duration = Math.max(1, clip.end_seconds - clip.start_seconds);
1559
+
1560
  return (
1561
  <article className="clip-card">
1562
  <div className="clip-video">
1563
+ {clip.video_url ? (
1564
+ <video controls src={`${API_BASE}${clip.video_url}`} />
1565
+ ) : (
1566
+ <Film size={28} />
1567
+ )}
1568
  </div>
1569
+
1570
  <div className="clip-body">
1571
+ {/* Title + score */}
1572
+ <div className="clip-meta">
1573
+ <div style={{ minWidth: 0 }}>
1574
+ <h3 className="clip-title">{clip.title}</h3>
1575
+ <p className="clip-reason">{clip.reason}</p>
1576
  </div>
1577
+ <span className="score-badge">
1578
+ <Gauge size={12} />
1579
  {Math.round(clip.score)}
1580
  </span>
1581
  </div>
1582
 
1583
+ {/* Duration row */}
1584
+ <div className="clip-duration-row">
1585
  <span>
1586
+ <Clock3 size={12} />
1587
  {duration.toFixed(1)}s
1588
  </span>
1589
  <span>
1590
+ {formatTime(clip.start_seconds)} {formatTime(clip.end_seconds)}
1591
  </span>
1592
  </div>
1593
 
1594
+ {/* Subtitle snippet */}
1595
+ {clip.subtitle_text && (
1596
+ <p className="subtitle-snippet">{clip.subtitle_text}</p>
1597
+ )}
1598
 
1599
+ {/* Action row */}
1600
  <div className="clip-actions">
1601
+ {/* Edit primary style, full label */}
1602
+ <button
1603
+ className="btn btn-primary"
1604
+ type="button"
1605
+ title={t("openEditor")}
1606
+ onClick={() => onOpenEditor(clip)}
1607
+ style={{ justifyContent: "center" }}
1608
+ >
1609
+ <PanelRightOpen size={14} />
1610
  {t("openEditor")}
1611
  </button>
1612
+
1613
+ {/* Approve toggle */}
1614
+ <button
1615
+ className={`btn btn-icon ${clip.approved ? "btn-success" : ""}`}
1616
+ type="button"
1617
+ title={clip.approved ? t("approved") : t("approve")}
1618
+ onClick={() => onApprove(clip)}
1619
+ >
1620
+ <Check size={14} />
1621
  </button>
1622
+
1623
+ {/* Regenerate */}
1624
+ <button
1625
+ className="btn btn-icon"
1626
+ type="button"
1627
+ title={t("regenerate")}
1628
+ onClick={() => onRegenerate(clip)}
1629
+ >
1630
+ <RefreshCcw size={14} />
1631
  </button>
1632
+
1633
+ {/* Delete */}
1634
+ <button
1635
+ className="btn btn-icon btn-danger"
1636
+ type="button"
1637
+ title={t("delete")}
1638
+ onClick={() => onDelete(clip)}
1639
+ >
1640
+ <Trash2 size={14} />
1641
  </button>
1642
+
1643
+ {/* Download */}
1644
  {clip.download_url && (
1645
+ <a
1646
+ className="btn btn-icon"
1647
+ href={`${API_BASE}${clip.download_url}`}
1648
+ title={t("download")}
1649
+ style={{ borderColor: "var(--primary-dim)", background: "var(--primary-glow)", color: "var(--primary)" }}
1650
+ >
1651
+ <Download size={14} />
1652
  </a>
1653
  )}
1654
  </div>
 
1657
  );
1658
  }
1659
 
1660
+ // ============================================================
1661
+ // Clip editor page — full page replacement
1662
+ // ============================================================
1663
  function ClipEditorPage({
1664
  clip,
1665
  job,
 
1677
  const metadataModel = clip.metadata?.model || "unknown";
1678
  const sourceKind = job?.source?.kind || "video";
1679
  const activeCue = cues[0]?.text || clip.subtitle_text || clip.title;
1680
+
1681
  const timelineDuration = Math.max(
1682
  clip.end_seconds,
1683
  ...(job?.transcript || []).map((segment) => Number(segment.end_seconds || 0)),
1684
  1
1685
  );
1686
  const rangeLeft = clamp((clip.start_seconds / timelineDuration) * 100, 0, 100);
1687
+ const rangeWidth = clamp(
1688
+ ((clip.end_seconds - clip.start_seconds) / timelineDuration) * 100,
1689
+ 1,
1690
+ 100
1691
+ );
1692
 
1693
  function patchCue(index, text) {
1694
  const next = cues.map((cue, cueIndex) => (cueIndex === index ? { ...cue, text } : cue));
 
1714
  }
1715
 
1716
  function setClipLength(seconds) {
1717
+ onPatch(clip.id, {
1718
+ end_seconds: roundTime(
1719
+ clamp(clip.start_seconds + seconds, clip.start_seconds + 1, timelineDuration)
1720
+ ),
1721
+ });
1722
  }
1723
 
1724
  return (
1725
  <div className="editor-shell">
1726
+ {/* Sticky top bar */}
1727
  <div className="editor-topbar">
1728
  <button className="ghost-button" type="button" onClick={onBack}>
1729
+ <ArrowLeft size={16} />
1730
  {t("backToDashboard")}
1731
  </button>
1732
+
1733
+ <div className="editor-topbar-info">
1734
+ <h2>{clip.title}</h2>
1735
+ <p>
1736
+ {t("editorText")} &nbsp;&middot;&nbsp; {formatTime(clip.start_seconds)} –{" "}
1737
+ {formatTime(clip.end_seconds)}
1738
+ </p>
1739
  </div>
 
1740
 
1741
+ <div className="editor-topbar-actions">
1742
+ <button
1743
+ className={`btn ${clip.approved ? "btn-success" : ""}`}
1744
+ type="button"
1745
+ onClick={() => onApprove(clip)}
1746
+ >
1747
+ <Check size={14} />
1748
+ {clip.approved ? t("approved") : t("approve")}
 
 
 
 
 
 
 
 
 
 
 
 
 
1749
  </button>
1750
+ {clip.download_url && (
1751
+ <a
1752
+ className="btn btn-primary"
1753
+ href={`${API_BASE}${clip.download_url}`}
1754
+ title={t("download")}
1755
+ >
1756
+ <Download size={14} />
1757
+ {t("download")}
1758
+ </a>
1759
+ )}
1760
+ </div>
1761
+ </div>
1762
+
1763
+ {/* Two-column body */}
1764
+ <div className="editor-body">
1765
+ {/* Left: video + controls */}
1766
+ <div className="editor-left">
1767
+ {/* Video preview */}
1768
  <div className="editor-preview">
1769
+ {clip.video_url ? (
1770
+ <video controls src={`${API_BASE}${clip.video_url}`} />
1771
+ ) : (
1772
+ <Film size={40} />
1773
+ )}
1774
  <CaptionPreview text={activeCue} settings={captionStyle} />
1775
  </div>
1776
 
1777
+ {/* Clip range section */}
1778
+ <div className="section-panel">
1779
  <div className="panel-heading compact">
1780
  <div>
1781
  <h2>{t("clipRange")}</h2>
1782
  <p>
1783
+ {formatTime(clip.start_seconds)} {formatTime(clip.end_seconds)} &nbsp;·&nbsp;{" "}
1784
+ {duration.toFixed(1)}s
1785
  </p>
1786
  </div>
1787
+ <div className="panel-heading-icon">
1788
+ <Scissors size={14} />
1789
+ </div>
1790
  </div>
1791
+
1792
+ {/* Quick-trim toolbox */}
1793
  <div className="editor-toolbox">
1794
+ <span className="editor-toolbox-label">{t("editorTools")}</span>
1795
+ {[
1796
+ [t("trimStartBack"), () => updateStart(clip.start_seconds - 0.5)],
1797
+ [t("trimStartForward"), () => updateStart(clip.start_seconds + 0.5)],
1798
+ [t("trimEndBack"), () => updateEnd(clip.end_seconds - 0.5)],
1799
+ [t("trimEndForward"), () => updateEnd(clip.end_seconds + 0.5)],
1800
+ [t("moveClipLeft"), () => moveClip(-1)],
1801
+ [t("moveClipRight"), () => moveClip(1)],
1802
+ [t("setClipLength30"), () => setClipLength(30)],
1803
+ [t("setClipLength60"), () => setClipLength(60)],
1804
+ [t("setClipLength90"), () => setClipLength(90)],
1805
+ ].map(([label, handler]) => (
1806
+ <button key={label} className="btn" type="button" onClick={handler}>
1807
+ {label}
1808
+ </button>
1809
+ ))}
 
 
 
 
 
 
 
 
 
 
 
 
1810
  </div>
1811
+
1812
+ {/* Timeline visual */}
1813
  <div className="timeline-visual">
1814
  <div className="timeline-fill" />
1815
+ <div
1816
+ className="timeline-window"
1817
+ style={{ left: `${rangeLeft}%`, width: `${rangeWidth}%` }}
1818
+ />
1819
  {Array.from({ length: 9 }).map((_, index) => (
1820
  <span key={index} style={{ left: `${index * 12.5}%` }} />
1821
  ))}
1822
  </div>
1823
+
1824
+ {/* Range sliders */}
1825
  <div className="range-sliders">
1826
  <label>
1827
  <span>{t("rangeStart")}</span>
 
1846
  />
1847
  </label>
1848
  </div>
1849
+
1850
+ {/* Numeric inputs */}
1851
  <div className="timeline">
1852
  <NumberField
1853
  label={t("start")}
 
1863
  </div>
1864
  </div>
1865
 
1866
+ {/* Timeline tracks */}
1867
  <TimelineTracks clip={clip} cues={cues} duration={timelineDuration} t={t} />
1868
 
1869
+ {/* Subtitle cue editor */}
1870
+ <div className="section-panel">
1871
  <div className="panel-heading compact">
1872
  <div>
1873
  <h2>{t("subtitleCues")}</h2>
1874
  <p>{t("subtitleCueHelp")}</p>
1875
  </div>
1876
+ <div className="panel-heading-icon">
1877
+ <Captions size={14} />
1878
+ </div>
1879
  </div>
1880
  <div className="cue-list">
1881
  {cues.map((cue, index) => (
1882
  <div className="cue-row" key={`${cue.start_seconds}-${index}`}>
1883
  <span>
1884
+ {formatTime(cue.start_seconds)} {formatTime(cue.end_seconds)}
1885
  </span>
1886
  <textarea
1887
  defaultValue={cue.text}
 
1894
  ))}
1895
  </div>
1896
  </div>
1897
+ </div>
1898
 
1899
+ {/* Right: inspector + caption style + mini transcript */}
1900
+ <div className="editor-right">
1901
+ {/* Inspector */}
1902
+ <div className="section-panel">
1903
+ <div className="panel-heading compact">
1904
+ <div>
1905
+ <h2>{t("inspector")}</h2>
1906
+ </div>
1907
+ <div className="panel-heading-icon">
1908
+ <Sparkles size={14} />
1909
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1910
  </div>
1911
+
1912
+ <dl className="inspector-list">
1913
+ <div>
1914
+ <dt>{t("title")}</dt>
1915
+ <dd>{clip.title}</dd>
1916
+ </div>
1917
+ <div>
1918
+ <dt>{t("reason")}</dt>
1919
+ <dd>{clip.reason}</dd>
1920
+ </div>
1921
+ <div>
1922
+ <dt>{t("score")}</dt>
1923
+ <dd>{Math.round(clip.score)}</dd>
1924
+ </div>
1925
+ <div>
1926
+ <dt>{t("status")}</dt>
1927
+ <dd>{clip.approved ? t("approved") : t("notApproved")}</dd>
1928
+ </div>
1929
+ <div>
1930
+ <dt>{t("model")}</dt>
1931
+ <dd>{metadataModel}</dd>
1932
+ </div>
1933
+ <div>
1934
+ <dt>{t("source")}</dt>
1935
+ <dd>{t(`source_${sourceKind}`)}</dd>
1936
+ </div>
1937
+ </dl>
1938
+
1939
+ <div className="inspector-actions" style={{ marginTop: 12 }}>
1940
+ <button
1941
+ className={`btn-primary ${clip.approved ? "btn-success" : "btn-primary"}`}
1942
+ style={{
1943
+ display: "inline-flex",
1944
+ alignItems: "center",
1945
+ justifyContent: "center",
1946
+ gap: 7,
1947
+ minHeight: 38,
1948
+ padding: "0 14px",
1949
+ border: "1px solid",
1950
+ borderRadius: "var(--radius-sm)",
1951
+ fontSize: "0.84rem",
1952
+ fontWeight: 600,
1953
+ cursor: "pointer",
1954
+ transition: "all 150ms ease",
1955
+ borderColor: clip.approved
1956
+ ? "rgba(52,211,153,0.35)"
1957
+ : "var(--primary-dim)",
1958
+ background: clip.approved ? "var(--success-soft)" : "var(--primary-glow)",
1959
+ color: clip.approved ? "var(--success)" : "var(--primary)",
1960
+ }}
1961
+ type="button"
1962
+ onClick={() => onApprove(clip)}
1963
+ >
1964
+ <Check size={14} />
1965
+ {clip.approved ? t("approved") : t("approve")}
1966
+ </button>
1967
+
1968
+ <button
1969
+ className="inspector-actions"
1970
+ style={{
1971
+ display: "inline-flex",
1972
+ alignItems: "center",
1973
+ justifyContent: "center",
1974
+ gap: 7,
1975
+ minHeight: 38,
1976
+ padding: "0 14px",
1977
+ border: "1px solid var(--border)",
1978
+ borderRadius: "var(--radius-sm)",
1979
+ background: "var(--surface2)",
1980
+ color: "var(--text-muted)",
1981
+ fontSize: "0.84rem",
1982
+ fontWeight: 600,
1983
+ cursor: "pointer",
1984
+ transition: "all 150ms ease",
1985
+ width: "100%",
1986
+ }}
1987
+ type="button"
1988
+ onClick={() => onRegenerate(clip)}
1989
+ >
1990
+ <RefreshCcw size={14} />
1991
+ {t("regenerate")}
1992
+ </button>
1993
+
1994
+ {clip.download_url && (
1995
+ <a
1996
+ style={{
1997
+ display: "inline-flex",
1998
+ alignItems: "center",
1999
+ justifyContent: "center",
2000
+ gap: 7,
2001
+ minHeight: 38,
2002
+ padding: "0 14px",
2003
+ border: "1px solid var(--primary-dim)",
2004
+ borderRadius: "var(--radius-sm)",
2005
+ background: "var(--primary-glow)",
2006
+ color: "var(--primary)",
2007
+ fontSize: "0.84rem",
2008
+ fontWeight: 600,
2009
+ textDecoration: "none",
2010
+ transition: "all 150ms ease",
2011
+ }}
2012
+ href={`${API_BASE}${clip.download_url}`}
2013
+ >
2014
+ <Download size={14} />
2015
+ {t("download")}
2016
+ </a>
2017
+ )}
2018
+
2019
+ <button
2020
+ style={{
2021
+ display: "inline-flex",
2022
+ alignItems: "center",
2023
+ justifyContent: "center",
2024
+ gap: 7,
2025
+ minHeight: 38,
2026
+ padding: "0 14px",
2027
+ border: "1px solid rgba(248,113,113,0.3)",
2028
+ borderRadius: "var(--radius-sm)",
2029
+ background: "var(--danger-soft)",
2030
+ color: "var(--danger)",
2031
+ fontSize: "0.84rem",
2032
+ fontWeight: 600,
2033
+ cursor: "pointer",
2034
+ transition: "all 150ms ease",
2035
+ width: "100%",
2036
+ }}
2037
+ type="button"
2038
+ onClick={() => onDelete(clip)}
2039
+ >
2040
+ <Trash2 size={14} />
2041
+ {t("delete")}
2042
+ </button>
2043
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2044
  </div>
2045
 
2046
+ {/* Caption style panel */}
2047
+ <div className="section-panel">
2048
+ <div className="panel-heading compact">
2049
+ <div>
2050
+ <h2>{t("captionStyle")}</h2>
2051
+ </div>
2052
+ <div className="panel-heading-icon">
2053
+ <Captions size={14} />
2054
+ </div>
2055
+ </div>
2056
+ <CaptionStylePanel t={t} settings={captionStyle} onChange={onCaptionStyleChange} />
2057
+ </div>
2058
 
2059
+ {/* Mini transcript */}
2060
+ <div className="section-panel">
2061
+ <TranscriptMini transcript={job?.transcript || []} clip={clip} t={t} />
2062
+ </div>
2063
+ </div>
2064
  </div>
2065
  </div>
2066
  );
2067
  }
2068
 
2069
+ // ============================================================
2070
+ // Caption preview (overlay on video)
2071
+ // ============================================================
2072
  function CaptionPreview({ text, settings }) {
2073
  const words = text.split(/\s+/).filter(Boolean);
2074
  const litCount = Math.max(1, Math.ceil(words.length * 0.45));
 
2095
  );
2096
  }
2097
 
2098
+ // ============================================================
2099
+ // Timeline tracks
2100
+ // ============================================================
2101
  function TimelineTracks({ clip, cues, duration, t }) {
2102
  const clipLeft = clamp((clip.start_seconds / duration) * 100, 0, 100);
2103
  const clipWidth = clamp(((clip.end_seconds - clip.start_seconds) / duration) * 100, 4, 100);
2104
  const subtitleItems = cues.slice(0, 8);
2105
+
2106
  return (
2107
+ <div className="section-panel">
2108
  <div className="panel-heading compact">
2109
  <div>
2110
  <h2>{t("timelineTracks")}</h2>
2111
  <p>
2112
+ {formatTime(clip.start_seconds)} {formatTime(clip.end_seconds)}
2113
  </p>
2114
  </div>
2115
+ <div className="panel-heading-icon">
2116
+ <Film size={14} />
2117
+ </div>
2118
  </div>
2119
  <div className="track-stack">
2120
  <TrackRow label={t("videoTrack")}>
2121
+ <div
2122
+ className="track-clip video"
2123
+ style={{ left: `${clipLeft}%`, width: `${clipWidth}%` }}
2124
+ >
2125
  {clip.title}
2126
  </div>
2127
  </TrackRow>
 
2131
  className="track-clip subtitle"
2132
  key={`${cue.start_seconds}-${index}`}
2133
  style={{
2134
+ left: `${clamp(
2135
+ ((clip.start_seconds + cue.start_seconds) / duration) * 100,
2136
+ 0,
2137
+ 100
2138
+ )}%`,
2139
+ width: `${clamp(
2140
+ ((cue.end_seconds - cue.start_seconds) / duration) * 100,
2141
+ 3,
2142
+ 45
2143
+ )}%`,
2144
  }}
2145
  >
2146
  {cue.text}
 
2168
  );
2169
  }
2170
 
2171
+ // ============================================================
2172
+ // Caption style panel
2173
+ // ============================================================
2174
  function CaptionStylePanel({ t, settings, onChange }) {
2175
  return (
2176
+ <div className="caption-style-panel">
 
 
 
 
 
 
 
2177
  <div className="preset-row">
2178
  {Object.entries(captionPresets).map(([key, preset]) => (
2179
  <button type="button" key={key} onClick={() => onChange(preset)}>
 
2204
  </div>
2205
 
2206
  <div className="color-grid">
2207
+ <ColorField
2208
+ label={t("fillColor")}
2209
+ value={settings.fillColor}
2210
+ onChange={(fillColor) => onChange({ fillColor })}
2211
+ />
2212
  <ColorField
2213
  label={t("strokeColor")}
2214
  value={settings.strokeColor}
 
2216
  />
2217
  </div>
2218
 
2219
+ <RangeControl
2220
+ label={t("fontSize")}
2221
+ value={settings.fontSize}
2222
+ min={24}
2223
+ max={64}
2224
+ onChange={(fontSize) => onChange({ fontSize })}
2225
+ />
2226
  <RangeControl
2227
  label={t("strokeWidth")}
2228
  value={settings.strokeWidth}
 
2237
  max={38}
2238
  onChange={(position) => onChange({ position })}
2239
  />
2240
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2241
  );
2242
  }
2243
 
2244
+ // ============================================================
2245
+ // Mini transcript (editor right column)
2246
+ // ============================================================
2247
  function TranscriptMini({ transcript, clip, t }) {
2248
  const rows = transcript.filter(
2249
+ (segment) =>
2250
+ segment.end_seconds >= clip.start_seconds && segment.start_seconds <= clip.end_seconds
2251
  );
2252
+
2253
  return (
2254
  <div className="mini-transcript">
2255
  <h3>{t("transcript")}</h3>
2256
+ {rows.length === 0 && (
2257
+ <p style={{ margin: 0, fontSize: "0.8rem", color: "var(--text-soft)" }}>
2258
+ {t("transcriptEmpty")}
2259
+ </p>
2260
+ )}
2261
  {rows.map((segment) => (
2262
  <div key={segment.id}>
2263
  <span>
2264
+ {formatTime(segment.start_seconds)} {formatTime(segment.end_seconds)}
2265
  </span>
2266
  <p>{segment.text}</p>
2267
  </div>
 
2270
  );
2271
  }
2272
 
2273
+ // ============================================================
2274
+ // Reusable form fields
2275
+ // ============================================================
2276
  function TextField({ label, value, onChange, placeholder, helper }) {
2277
  return (
2278
  <label className="field-block">
 
2307
  return (
2308
  <label className="field-block">
2309
  <span className="field-label">{label}</span>
2310
+ <select
2311
+ className="text-input"
2312
+ value={value}
2313
+ onChange={(event) => onChange(event.target.value)}
2314
+ >
2315
  {options.map((option) => (
2316
  <option key={option.value} value={option.value}>
2317
  {option.label}
 
2326
  function NumberField({ label, value, onChange }) {
2327
  return (
2328
  <label>
2329
+ <span style={{ display: "block", fontSize: "0.74rem", fontWeight: 700, color: "var(--text-muted)", marginBottom: 5 }}>
2330
+ {label}
2331
+ </span>
2332
  <input
2333
  type="number"
2334
  min="0"
2335
  step="0.5"
2336
  value={value}
2337
  onChange={(event) => onChange(event.target.value)}
2338
+ style={{
2339
+ width: "100%",
2340
+ minHeight: 36,
2341
+ padding: "7px 10px",
2342
+ border: "1px solid var(--border)",
2343
+ borderRadius: "var(--radius-sm)",
2344
+ background: "var(--surface)",
2345
+ color: "var(--text)",
2346
+ outline: "none",
2347
+ fontVariantNumeric: "tabular-nums",
2348
+ }}
2349
  />
2350
  </label>
2351
  );
2352
  }
2353
 
2354
+ function ColorField({ label, value, onChange }) {
2355
+ return (
2356
+ <label className="color-field">
2357
+ <span>{label}</span>
2358
+ <input type="color" value={value} onChange={(event) => onChange(event.target.value)} />
2359
+ </label>
2360
+ );
2361
+ }
2362
+
2363
+ function RangeControl({ label, value, min, max, onChange }) {
2364
+ return (
2365
+ <label className="range-control">
2366
+ <span>
2367
+ {label}
2368
+ <strong>{value}</strong>
2369
+ </span>
2370
+ <input
2371
+ type="range"
2372
+ min={min}
2373
+ max={max}
2374
+ step="1"
2375
+ value={value}
2376
+ onChange={(event) => onChange(Number(event.target.value))}
2377
+ />
2378
+ </label>
2379
+ );
2380
+ }
2381
+
2382
+ // ============================================================
2383
+ // Utility functions — unchanged
2384
+ // ============================================================
2385
  async function fetchJson(path, options) {
2386
  const response = await fetch(`${API_BASE}${path}`, options);
2387
  if (!response.ok) {
 
2398
  }
2399
 
2400
  function getSubtitleCues(clip, duration, settings = defaultCaptionStyle) {
2401
+ return splitSubtitleText(clip.subtitle_text || "", settings.cueDensity).map(
2402
+ (text, index, all) => {
2403
+ const cueDuration = duration / Math.max(all.length, 1);
2404
+ return {
2405
+ start_seconds: roundTime(index * cueDuration),
2406
+ end_seconds: roundTime((index + 1) * cueDuration),
2407
+ text,
2408
+ };
2409
+ }
2410
+ );
2411
  }
2412
 
2413
  function splitSubtitleText(text, density = "short") {
 
2432
  let current = [];
2433
  words.forEach((word) => {
2434
  const candidate = [...current, word].join(" ");
2435
+ const punctuationBreak =
2436
+ current.length > 0 && /[,.!?;:]$/.test(current[current.length - 1]);
2437
+ if (
2438
+ current.length > 0 &&
2439
+ (candidate.length > limit.chars ||
2440
+ current.length >= limit.words ||
2441
+ punctuationBreak)
2442
+ ) {
2443
  chunks.push(current.join(" "));
2444
  current = [word];
2445
  } else {
frontend/src/styles.css CHANGED
@@ -2,51 +2,92 @@
2
  @tailwind components;
3
  @tailwind utilities;
4
 
 
 
 
 
 
5
  :root {
6
- color-scheme: light;
7
- font-family: Inter, ui-sans-serif, system-ui, sans-serif;
8
- --bg: #f6f7f9;
9
- --surface: #ffffff;
10
- --surface-muted: #eef2f6;
11
- --surface-strong: #e2e8f0;
12
- --text: #111827;
13
- --text-muted: #64748b;
14
- --text-soft: #94a3b8;
15
- --border: #d9e2ec;
16
- --border-strong: #b7c4d4;
17
- --primary: #0f766e;
18
- --primary-strong: #115e59;
19
- --primary-soft: #ccfbf1;
20
- --accent: #d97706;
21
- --accent-soft: #fef3c7;
22
- --danger: #be123c;
23
- --danger-soft: #ffe4e6;
24
- --shadow: 0 18px 40px rgb(15 23 42 / 0.08);
25
- --radius: 8px;
26
- }
27
-
28
- :root[data-theme="dark"] {
29
  color-scheme: dark;
30
- --bg: #09111f;
31
- --surface: #111827;
32
- --surface-muted: #172033;
33
- --surface-strong: #243044;
34
- --text: #f8fafc;
35
- --text-muted: #a8b3c7;
36
- --text-soft: #748198;
37
- --border: #28364d;
38
- --border-strong: #3b4d67;
39
- --primary: #2dd4bf;
40
- --primary-strong: #5eead4;
41
- --primary-soft: #133b3b;
42
- --accent: #fbbf24;
43
- --accent-soft: #3d2e12;
44
- --danger: #fb7185;
45
- --danger-soft: #3d1723;
46
- --shadow: 0 20px 44px rgb(0 0 0 / 0.26);
47
- }
48
-
49
- * {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  box-sizing: border-box;
51
  }
52
 
@@ -54,6 +95,8 @@ body {
54
  margin: 0;
55
  background: var(--bg);
56
  color: var(--text);
 
 
57
  }
58
 
59
  button,
@@ -70,227 +113,306 @@ select {
70
 
71
  button:disabled {
72
  cursor: not-allowed;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  }
74
 
 
 
 
75
  .app-shell {
76
  min-height: 100vh;
77
- background:
78
- linear-gradient(180deg, color-mix(in srgb, var(--surface-muted) 60%, transparent), transparent 360px),
79
- var(--bg);
80
  color: var(--text);
81
  }
82
 
 
 
 
83
  .app-header {
84
  position: sticky;
85
  top: 0;
86
- z-index: 20;
87
  display: flex;
88
- min-height: 76px;
89
  align-items: center;
90
  justify-content: space-between;
91
  gap: 20px;
 
92
  border-bottom: 1px solid var(--border);
93
- background: color-mix(in srgb, var(--surface) 92%, transparent);
94
- padding: 14px clamp(16px, 4vw, 44px);
95
- backdrop-filter: blur(18px);
96
- }
97
-
98
- .brand-block,
99
- .header-actions,
100
- .clip-actions,
101
- .metric-row,
102
- .editor-topbar,
103
- .panel-heading,
104
- .toolbar-select,
105
- .mode-pill,
106
- .status-pill,
107
- .icon-button,
108
- .ghost-button,
109
- .primary-button,
110
- .inspector-actions a,
111
- .inspector-actions button {
112
- display: flex;
113
- align-items: center;
114
  }
115
 
116
  .brand-block {
117
- min-width: 0;
 
118
  gap: 12px;
 
119
  }
120
 
121
  .brand-mark {
122
  display: grid;
123
- width: 44px;
124
- height: 44px;
125
- flex: 0 0 auto;
126
  place-items: center;
127
- border-radius: var(--radius);
128
- background: #020617;
129
- color: white;
 
 
 
 
 
130
  }
131
 
132
- .brand-block h1,
133
- .panel-heading h2,
134
- .editor-topbar h2,
135
- .mini-transcript h3 {
136
- margin: 0;
137
- color: var(--text);
138
- letter-spacing: 0;
139
  }
140
 
141
  .brand-block h1 {
142
- font-size: 1.2rem;
 
143
  font-weight: 800;
 
 
144
  }
145
 
146
- .brand-block p,
147
- .panel-heading p,
148
- .helper-text,
149
- .file-name {
150
  margin: 2px 0 0;
 
151
  color: var(--text-muted);
152
- font-size: 0.86rem;
153
- line-height: 1.5;
 
 
 
154
  }
155
 
156
  .header-actions {
 
 
157
  flex-wrap: wrap;
158
  justify-content: flex-end;
159
  gap: 8px;
160
  }
161
 
 
 
 
162
  .mode-pill,
163
  .status-pill {
164
- min-height: 34px;
165
- border-radius: 999px;
166
- border: 1px solid var(--border);
167
  padding: 0 10px;
 
 
 
168
  color: var(--text-muted);
169
- font-size: 0.76rem;
170
- font-weight: 800;
 
171
  text-transform: uppercase;
 
172
  }
173
 
174
  .mode-pill.prod,
175
  .status-pill.completed {
176
- border-color: color-mix(in srgb, var(--primary) 50%, var(--border));
177
- background: var(--primary-soft);
178
- color: var(--primary-strong);
179
  }
180
 
181
  .mode-pill.demo,
182
  .status-pill.running,
183
  .status-pill.queued {
184
- border-color: color-mix(in srgb, #0ea5e9 45%, var(--border));
185
- background: color-mix(in srgb, #e0f2fe 70%, var(--surface));
186
- color: #0369a1;
187
- }
188
-
189
- :root[data-theme="dark"] .mode-pill.demo,
190
- :root[data-theme="dark"] .status-pill.running,
191
- :root[data-theme="dark"] .status-pill.queued {
192
- background: #102a3d;
193
- color: #7dd3fc;
194
  }
195
 
196
  .status-pill.failed {
197
- border-color: color-mix(in srgb, var(--danger) 50%, var(--border));
198
  background: var(--danger-soft);
199
  color: var(--danger);
200
  }
201
 
202
- .toolbar-select,
203
- .icon-button,
204
- .ghost-button,
205
- .clip-actions button,
206
- .download-button,
207
- .inspector-actions a,
208
- .inspector-actions button {
209
- min-height: 36px;
210
- justify-content: center;
211
- gap: 8px;
212
  border: 1px solid var(--border);
213
- border-radius: 7px;
214
  background: var(--surface);
215
- color: var(--text);
216
- text-decoration: none;
 
217
  }
218
 
219
- .toolbar-select {
220
- padding: 0 8px;
221
- background: var(--surface);
 
222
  }
223
 
224
  .toolbar-select select {
225
- min-width: 52px;
226
  border: 0;
227
- background: var(--surface);
228
- color: var(--text);
229
- -webkit-text-fill-color: var(--text);
230
  outline: none;
 
 
231
  }
232
 
233
  .toolbar-select select option,
234
  .text-input option {
 
 
 
 
 
 
235
  background: #ffffff;
236
  color: #111827;
237
  }
238
 
239
- :root[data-theme="dark"] .toolbar-select select option,
240
- :root[data-theme="dark"] .text-input option {
241
- background: #111827;
242
- color: #f8fafc;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  }
244
 
245
- .icon-button {
246
- width: 36px;
247
- padding: 0;
248
  }
249
 
 
 
 
250
  .workspace-grid {
251
  display: grid;
252
- grid-template-columns: minmax(320px, 420px) minmax(0, 1fr);
253
- gap: 14px;
 
254
  align-items: start;
255
- padding: 18px clamp(16px, 3vw, 40px) 36px;
256
  }
257
 
258
- .center-column,
259
- .results-column,
260
- .form-stack {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
  display: grid;
262
- align-content: start;
263
  gap: 16px;
 
264
  }
265
 
 
 
 
266
  .panel {
267
  border: 1px solid var(--border);
268
- border-radius: var(--radius);
269
- background: color-mix(in srgb, var(--surface) 96%, transparent);
270
- box-shadow: var(--shadow);
 
271
  }
272
 
273
- .input-panel,
274
- .progress-panel,
275
- .transcript-panel,
276
- .clips-panel,
277
- .editor-main,
278
- .inspector-panel {
279
  padding: 18px;
280
  }
281
 
282
- .input-panel {
283
- position: static;
284
- overflow: visible;
285
- }
286
-
287
- .results-column {
288
- grid-column: 1 / -1;
289
  }
290
 
291
  .panel-heading {
 
 
292
  justify-content: space-between;
293
  gap: 12px;
 
294
  }
295
 
296
  .panel-heading.compact {
@@ -300,20 +422,44 @@ button:disabled {
300
  .panel-heading h2,
301
  .editor-topbar h2,
302
  .mini-transcript h3 {
303
- font-size: 0.92rem;
304
- font-weight: 850;
 
 
305
  text-transform: uppercase;
 
306
  }
307
 
308
- .divider {
309
- height: 1px;
310
- background: var(--border);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
  }
312
 
313
  .form-grid-two {
314
  display: grid;
315
- grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
316
- gap: 12px;
317
  }
318
 
319
  .field-block {
@@ -322,257 +468,468 @@ button:disabled {
322
  }
323
 
324
  .field-label {
325
- color: var(--text);
326
- font-size: 0.82rem;
327
- font-weight: 750;
 
328
  }
329
 
330
  .text-input,
331
- .file-input,
332
  textarea,
333
- .timeline input {
334
  width: 100%;
335
- border: 1px solid var(--border-strong);
336
- border-radius: 7px;
337
- background: var(--surface);
338
  color: var(--text);
339
  outline: none;
 
340
  }
341
 
342
  .text-input,
343
  .file-input {
344
- min-height: 42px;
345
- padding: 9px 11px;
 
346
  }
347
 
348
  textarea {
349
  resize: vertical;
350
- padding: 10px 11px;
351
- line-height: 1.5;
 
 
 
 
 
 
 
352
  }
353
 
354
  .text-input:focus,
355
- .file-input:focus,
356
  textarea:focus,
357
- .timeline input:focus {
358
  border-color: var(--primary);
359
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 18%, transparent);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
  }
361
 
 
 
 
362
  .segmented {
363
  display: grid;
364
  grid-template-columns: 1fr 1fr;
365
- gap: 4px;
366
- border-radius: var(--radius);
367
- background: var(--surface-muted);
368
- padding: 4px;
 
369
  }
370
 
371
  .segmented button {
372
  display: flex;
373
- min-height: 40px;
374
  align-items: center;
375
  justify-content: center;
376
- gap: 8px;
377
- border: 0;
378
- border-radius: 6px;
 
379
  background: transparent;
380
  color: var(--text-muted);
 
 
 
 
 
 
 
 
381
  }
382
 
383
  .segmented button.active {
 
384
  background: var(--surface);
385
  color: var(--text);
386
- box-shadow: 0 1px 2px rgb(15 23 42 / 0.10);
387
  }
388
 
 
 
 
389
  .primary-button {
390
- min-height: 44px;
 
391
  justify-content: center;
392
- gap: 10px;
393
- border: 1px solid var(--primary-strong);
394
- border-radius: 7px;
395
- background: var(--primary-strong);
396
- color: white;
397
- font-weight: 850;
 
 
 
 
 
 
 
 
 
 
 
398
  }
399
 
400
- :root[data-theme="dark"] .primary-button {
401
- color: #042f2e;
 
402
  }
403
 
404
  .primary-button:disabled {
405
- opacity: 0.55;
406
  }
407
 
408
- .progress-percent {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
409
  color: var(--text);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
410
  font-size: 2rem;
 
411
  font-variant-numeric: tabular-nums;
 
 
412
  }
413
 
414
  .progress-track {
415
- height: 12px;
416
  overflow: hidden;
417
  border-radius: 999px;
418
- background: var(--surface-strong);
419
- margin-top: 16px;
 
420
  }
421
 
422
  .progress-bar {
423
  height: 100%;
424
  border-radius: 999px;
425
- background: linear-gradient(90deg, var(--primary), var(--accent));
426
- transition: width 260ms ease;
 
 
427
  }
428
 
429
  .step-list {
430
  display: grid;
431
  grid-template-columns: repeat(6, minmax(0, 1fr));
432
- gap: 8px;
433
- margin-top: 16px;
434
  }
435
 
436
  .step-item {
437
  display: grid;
438
- gap: 6px;
439
- min-width: 0;
440
  color: var(--text-soft);
441
- font-size: 0.75rem;
442
- font-weight: 750;
443
  }
444
 
445
  .step-item span {
446
  display: grid;
447
- width: 28px;
448
- height: 28px;
449
  place-items: center;
450
  border: 1px solid var(--border);
451
- border-radius: 999px;
452
- background: var(--surface);
453
  font-variant-numeric: tabular-nums;
 
 
454
  }
455
 
456
  .step-item p {
457
  margin: 0;
458
  overflow-wrap: anywhere;
 
 
 
 
 
 
 
 
 
 
 
459
  }
460
 
461
- .step-item.done,
462
  .step-item.active {
463
- color: var(--text);
464
  }
465
 
466
- .step-item.done span,
467
  .step-item.active span {
468
  border-color: var(--primary);
469
- background: var(--primary-soft);
470
- color: var(--primary-strong);
471
  }
472
 
473
  .timing-grid {
474
  display: grid;
475
- grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
476
- gap: 10px;
477
- margin-top: 16px;
478
  }
479
 
480
  .timing-grid div {
 
481
  border: 1px solid var(--border);
482
- border-radius: 7px;
483
- background: var(--surface-muted);
484
- padding: 10px;
485
  }
486
 
487
- .timing-grid span,
488
- .inspector-list dt,
489
- .cue-row span,
490
- .transcript-row span,
491
- .mini-transcript span {
492
  color: var(--text-muted);
493
- font-size: 0.76rem;
494
- font-weight: 750;
495
  }
496
 
497
  .timing-grid strong {
498
  display: block;
499
  margin-top: 4px;
500
- color: var(--text);
501
- font-size: 1.02rem;
502
  font-variant-numeric: tabular-nums;
 
503
  }
504
 
 
 
 
505
  .transcript-list {
506
  display: grid;
507
- max-height: 420px;
508
- overflow: auto;
509
- gap: 8px;
510
- padding-right: 6px;
 
 
511
  }
512
 
513
  .transcript-row {
514
  display: grid;
515
- grid-template-columns: 112px 1fr;
516
- gap: 12px;
517
- border-bottom: 1px solid var(--border);
518
  padding-bottom: 8px;
 
519
  }
520
 
521
- .transcript-row p,
522
- .mini-transcript p {
 
 
 
 
 
 
 
523
  margin: 0;
 
524
  color: var(--text);
525
- font-size: 0.9rem;
526
  line-height: 1.55;
527
  }
528
 
 
 
 
529
  .empty-state {
530
  display: grid;
531
- min-height: 280px;
532
  place-items: center;
533
  align-content: center;
534
  gap: 10px;
535
  border: 1px dashed var(--border-strong);
536
  border-radius: var(--radius);
537
- background: var(--surface-muted);
538
  color: var(--text-muted);
539
  text-align: center;
 
540
  }
541
 
542
  .empty-state h3 {
543
  margin: 0;
544
- color: var(--text);
545
  font-size: 1rem;
 
 
546
  }
547
 
548
  .empty-state p {
549
- max-width: 270px;
550
  margin: 0;
551
- line-height: 1.5;
 
552
  }
553
 
 
 
 
554
  .clip-grid {
555
  display: grid;
556
- grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
557
  gap: 14px;
558
  align-items: start;
559
  }
560
 
561
  .clip-card {
 
562
  overflow: hidden;
563
  border: 1px solid var(--border);
564
- border-radius: var(--radius);
565
- background: var(--surface-muted);
566
- min-width: 0;
 
 
 
 
 
 
 
 
 
567
  }
568
 
569
  .clip-video {
570
  display: grid;
571
  aspect-ratio: 9 / 16;
572
- max-height: 300px;
573
  place-items: center;
574
- background: #050b16;
575
- color: white;
 
576
  }
577
 
578
  .clip-video video {
@@ -583,209 +940,205 @@ textarea:focus,
583
 
584
  .clip-body {
585
  display: grid;
586
- gap: 12px;
587
- padding: 13px;
588
  }
589
 
590
- .clip-title-row {
 
591
  display: flex;
592
  align-items: flex-start;
593
  justify-content: space-between;
594
  gap: 10px;
595
  }
596
 
597
- .clip-title-row h3 {
598
  margin: 0;
 
 
599
  color: var(--text);
600
- font-size: 0.96rem;
601
- font-weight: 850;
602
  line-height: 1.35;
603
  }
604
 
605
- .clip-title-row p {
606
- margin: 5px 0 0;
 
607
  color: var(--text-muted);
608
- font-size: 0.82rem;
609
  line-height: 1.45;
 
 
 
 
610
  }
611
 
612
- .score {
613
  display: inline-flex;
 
 
614
  min-width: 52px;
615
- min-height: 34px;
616
  flex: 0 0 auto;
617
- align-items: center;
618
- justify-content: center;
619
- gap: 5px;
620
- border-radius: 7px;
621
  background: var(--accent-soft);
 
622
  color: var(--accent);
623
- font-weight: 900;
 
 
624
  }
625
 
626
- .metric-row {
 
 
627
  justify-content: space-between;
628
  gap: 8px;
 
 
629
  color: var(--text-muted);
630
- font-size: 0.8rem;
631
- font-weight: 750;
632
  }
633
 
634
- .metric-row span {
635
  display: inline-flex;
636
  align-items: center;
637
- gap: 5px;
638
- }
639
-
640
- .clip-actions {
641
- display: grid;
642
- grid-template-columns: minmax(0, 1fr) auto auto auto auto;
643
- gap: 8px;
644
- }
645
-
646
- .clip-actions button,
647
- .download-button {
648
- min-width: 38px;
649
- padding: 0 10px;
650
- white-space: nowrap;
651
- }
652
-
653
- .clip-actions button:not(.action-primary):not(.action-approve),
654
- .download-button {
655
- width: 38px;
656
- padding: 0;
657
- }
658
-
659
- .clip-actions .action-approve {
660
- min-width: 92px;
661
  }
662
 
663
  .subtitle-snippet {
664
- display: -webkit-box;
665
- min-height: 54px;
666
  margin: 0;
667
- overflow: hidden;
668
- -webkit-box-orient: vertical;
669
- -webkit-line-clamp: 3;
670
  border: 1px solid var(--border);
671
- border-radius: 7px;
672
- background: var(--surface);
673
- color: var(--text);
674
- font-size: 0.84rem;
675
- line-height: 1.45;
676
- padding: 10px;
677
- }
678
-
679
- .download-button {
680
- margin-left: auto;
681
- border-color: var(--primary);
682
- background: var(--primary);
683
- color: white;
684
  }
685
 
686
- :root[data-theme="dark"] .download-button {
687
- color: #042f2e;
 
 
 
 
688
  }
689
 
 
 
 
690
  .editor-shell {
691
- display: grid;
692
- gap: 14px;
693
- padding: 18px clamp(16px, 3vw, 40px) 32px;
 
694
  }
695
 
696
  .editor-topbar {
697
- justify-content: space-between;
698
- gap: 18px;
 
 
 
 
 
 
 
699
  }
700
 
701
- .editor-topbar > div {
702
  flex: 1;
 
703
  }
704
 
705
- .editor-topbar p {
706
- margin: 4px 0 0;
707
- color: var(--text-muted);
708
- }
709
-
710
- .ghost-button {
711
- padding: 0 12px;
 
712
  }
713
 
714
- .editor-grid {
715
- display: grid;
716
- grid-template-columns: 74px minmax(0, 1fr) minmax(330px, 420px);
717
- gap: 14px;
718
- align-items: start;
719
  }
720
 
721
- .tool-rail {
722
- position: sticky;
723
- top: 96px;
724
- display: grid;
725
  gap: 8px;
726
- border: 1px solid var(--border);
727
- border-radius: var(--radius);
728
- background: color-mix(in srgb, var(--surface) 96%, transparent);
729
- padding: 8px;
730
- box-shadow: var(--shadow);
731
  }
732
 
733
- .tool-rail button {
734
  display: grid;
735
- min-height: 58px;
736
- place-items: center;
737
- gap: 4px;
738
- border: 1px solid transparent;
739
- border-radius: 7px;
740
- background: transparent;
741
- color: var(--text-muted);
742
- font-size: 0.68rem;
743
- font-weight: 800;
744
  }
745
 
746
- .tool-rail button.active,
747
- .tool-rail button:hover {
748
- border-color: color-mix(in srgb, var(--primary) 45%, var(--border));
749
- background: var(--primary-soft);
750
- color: var(--primary-strong);
 
751
  }
752
 
753
- .editor-main,
754
- .inspector-panel {
755
  display: grid;
756
- gap: 16px;
 
 
 
 
 
 
 
757
  }
758
 
 
 
 
759
  .editor-preview {
760
- display: grid;
761
  position: relative;
762
- min-height: 520px;
763
- max-height: 68vh;
764
- overflow: hidden;
765
  place-items: center;
766
- border-radius: var(--radius);
767
- background: #050b16;
768
- color: white;
 
 
 
 
769
  }
770
 
771
  .editor-preview video {
772
  width: 100%;
773
  height: 100%;
774
- max-height: 68vh;
775
  object-fit: contain;
776
  }
777
 
778
  .caption-preview {
779
  position: absolute;
780
  left: 50%;
781
- width: min(82%, 760px);
782
  transform: translateX(-50%);
783
- color: white;
784
  font-weight: 900;
785
  letter-spacing: 0;
786
- line-height: 1.08;
787
  text-align: center;
788
- text-transform: none;
789
  pointer-events: none;
790
  }
791
 
@@ -801,137 +1154,101 @@ textarea:focus,
801
  animation: caption-bounce 760ms ease-in-out infinite alternate;
802
  }
803
 
804
- @keyframes caption-pop {
805
- from {
806
- transform: translateX(-50%) scale(0.98);
807
- }
808
- to {
809
- transform: translateX(-50%) scale(1.04);
810
- }
 
 
 
 
 
811
  }
812
 
813
- @keyframes caption-bounce {
814
- from {
815
- transform: translateX(-50%) translateY(0);
816
- }
817
- to {
818
- transform: translateX(-50%) translateY(-8px);
819
- }
820
  }
821
 
822
- .range-editor,
823
- .subtitle-editor,
824
- .timeline-workbench,
825
- .caption-style-panel {
826
- display: grid;
827
- gap: 12px;
828
- border: 1px solid var(--border);
829
- border-radius: var(--radius);
830
- background: var(--surface-muted);
831
- padding: 14px;
832
  }
833
 
834
  .timeline-visual {
835
  position: relative;
836
- height: 54px;
837
  overflow: hidden;
838
- border-radius: 7px;
839
- background: var(--surface);
840
  border: 1px solid var(--border);
 
 
841
  }
842
 
843
  .timeline-fill {
844
  position: absolute;
845
- inset: 21px 14px;
846
  border-radius: 999px;
847
- background: var(--surface-strong);
848
  }
849
 
850
  .timeline-window {
851
  position: absolute;
852
- top: 14px;
853
  height: 26px;
854
  border: 2px solid var(--primary);
855
  border-radius: 999px;
856
- background: color-mix(in srgb, var(--primary) 24%, transparent);
857
- box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--primary) 35%, transparent);
 
858
  }
859
 
860
  .timeline-window::before,
861
  .timeline-window::after {
 
862
  position: absolute;
863
- top: 4px;
864
  width: 2px;
865
- height: 14px;
866
  border-radius: 999px;
867
- background: var(--primary-strong);
868
- content: "";
869
- }
870
-
871
- .timeline-window::before {
872
- left: 8px;
873
  }
874
 
875
- .timeline-window::after {
876
- right: 8px;
877
- }
878
 
879
  .timeline-visual span {
880
  position: absolute;
881
- top: 18px;
882
  width: 1px;
883
- height: 18px;
884
- background: var(--border-strong);
885
- }
886
-
887
- .timeline {
888
- display: grid;
889
- grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 82px;
890
- align-items: end;
891
- gap: 10px;
892
- }
893
-
894
- .editor-toolbox {
895
- display: flex;
896
- flex-wrap: wrap;
897
- gap: 8px;
898
- align-items: center;
899
- }
900
-
901
- .editor-toolbox span {
902
- margin-right: 2px;
903
- color: var(--text);
904
- font-size: 0.82rem;
905
- font-weight: 850;
906
- }
907
-
908
- .editor-toolbox button {
909
- min-height: 34px;
910
- border: 1px solid var(--border);
911
- border-radius: 7px;
912
- background: var(--surface);
913
- color: var(--text);
914
- padding: 0 10px;
915
- font-size: 0.82rem;
916
- font-weight: 750;
917
- }
918
-
919
- .editor-toolbox button:hover {
920
- border-color: var(--primary);
921
  }
922
 
923
  .range-sliders {
924
  display: grid;
925
- grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
926
  gap: 12px;
927
  }
928
 
929
  .range-sliders label {
930
  display: grid;
931
- gap: 6px;
 
 
932
  color: var(--text-muted);
933
- font-size: 0.76rem;
934
- font-weight: 800;
935
  }
936
 
937
  .range-sliders input[type="range"] {
@@ -939,29 +1256,55 @@ textarea:focus,
939
  accent-color: var(--primary);
940
  }
941
 
 
 
 
 
 
 
 
942
  .timeline label {
943
  display: grid;
944
  gap: 5px;
 
 
945
  color: var(--text-muted);
946
- font-size: 0.76rem;
947
- font-weight: 800;
948
  }
949
 
950
  .timeline input {
951
- min-height: 38px;
952
- padding: 7px 9px;
 
 
 
 
 
 
 
 
 
 
 
 
 
953
  }
954
 
955
  .timeline strong {
956
- min-height: 38px;
957
- border-radius: 7px;
958
- background: var(--surface-strong);
959
- padding: 9px 8px;
960
- color: var(--text);
961
- text-align: center;
 
 
962
  font-variant-numeric: tabular-nums;
 
963
  }
964
 
 
 
 
965
  .track-stack {
966
  display: grid;
967
  gap: 8px;
@@ -969,99 +1312,133 @@ textarea:focus,
969
 
970
  .track-row {
971
  display: grid;
972
- grid-template-columns: 92px minmax(0, 1fr);
973
  gap: 10px;
974
  align-items: center;
975
  }
976
 
977
  .track-row > span {
 
 
978
  color: var(--text-muted);
979
- font-size: 0.75rem;
980
- font-weight: 850;
981
  }
982
 
983
  .track-lane {
984
  position: relative;
985
- min-height: 42px;
986
  overflow: hidden;
987
  border: 1px solid var(--border);
988
- border-radius: 7px;
989
  background:
990
  repeating-linear-gradient(
991
  90deg,
992
  transparent 0,
993
  transparent 11.8%,
994
- color-mix(in srgb, var(--border) 70%, transparent) 12%,
995
  transparent 12.2%
996
  ),
997
- var(--surface);
998
  }
999
 
1000
  .track-clip {
1001
  position: absolute;
1002
- top: 7px;
1003
- bottom: 7px;
1004
  display: flex;
1005
- min-width: 54px;
1006
  align-items: center;
1007
  overflow: hidden;
1008
- border-radius: 6px;
1009
- color: #042f2e;
1010
- font-size: 0.72rem;
1011
- font-weight: 850;
1012
- padding: 0 8px;
1013
- text-overflow: ellipsis;
1014
  white-space: nowrap;
 
1015
  }
1016
 
1017
  .track-clip.video {
1018
- background: linear-gradient(90deg, #5eead4, #22c55e);
 
1019
  }
1020
 
1021
  .track-clip.subtitle {
1022
- background: #fde68a;
1023
- color: #422006;
 
1024
  }
1025
 
1026
  .waveform {
1027
  position: absolute;
1028
- inset: 8px 10px;
1029
  display: flex;
1030
  align-items: center;
1031
- gap: 4px;
1032
  }
1033
 
1034
  .waveform span {
1035
- width: 4px;
1036
  border-radius: 999px;
1037
- background: color-mix(in srgb, var(--primary) 70%, var(--text-soft));
 
1038
  }
1039
 
 
 
 
1040
  .cue-list {
1041
  display: grid;
1042
- gap: 10px;
1043
  }
1044
 
1045
  .cue-row {
1046
  display: grid;
1047
- grid-template-columns: 112px minmax(0, 1fr);
1048
- gap: 10px;
1049
  align-items: start;
1050
  }
1051
 
 
 
 
 
 
 
 
 
 
 
 
1052
  .inspector-list {
1053
  display: grid;
1054
- gap: 12px;
1055
  margin: 0;
1056
  }
1057
 
1058
  .inspector-list div {
1059
- border-bottom: 1px solid var(--border);
 
1060
  padding-bottom: 10px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1061
  }
1062
 
1063
  .inspector-list dd {
1064
- margin: 4px 0 0;
 
1065
  color: var(--text);
1066
  line-height: 1.45;
1067
  }
@@ -1073,33 +1450,101 @@ textarea:focus,
1073
 
1074
  .inspector-actions a,
1075
  .inspector-actions button {
1076
- min-height: 40px;
1077
- padding: 0 12px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1078
  }
1079
 
1080
- .inspector-actions .danger {
1081
- border-color: color-mix(in srgb, var(--danger) 45%, var(--border));
1082
  background: var(--danger-soft);
1083
  color: var(--danger);
1084
  }
1085
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1086
  .caption-style-panel {
1087
- background: color-mix(in srgb, var(--surface-muted) 92%, var(--surface));
 
1088
  }
1089
 
1090
  .preset-row {
1091
  display: grid;
1092
- grid-template-columns: repeat(3, minmax(0, 1fr));
1093
- gap: 8px;
1094
  }
1095
 
1096
  .preset-row button {
1097
- min-height: 36px;
1098
  border: 1px solid var(--border);
1099
- border-radius: 7px;
1100
- background: var(--surface);
1101
- color: var(--text);
1102
- font-weight: 800;
 
 
 
 
 
 
 
 
 
 
 
 
1103
  }
1104
 
1105
  .style-grid,
@@ -1115,16 +1560,18 @@ textarea:focus,
1115
  .color-field,
1116
  .range-control {
1117
  display: grid;
1118
- gap: 7px;
1119
  }
1120
 
1121
  .color-field span,
1122
  .range-control span {
1123
  display: flex;
1124
  justify-content: space-between;
 
 
1125
  color: var(--text-muted);
1126
- font-size: 0.76rem;
1127
- font-weight: 800;
1128
  }
1129
 
1130
  .range-control strong {
@@ -1134,114 +1581,146 @@ textarea:focus,
1134
 
1135
  .color-field input {
1136
  width: 100%;
1137
- height: 38px;
1138
  border: 1px solid var(--border);
1139
- border-radius: 7px;
1140
- background: var(--surface);
1141
- padding: 4px;
 
 
1142
  }
1143
 
1144
- .range-control input {
 
 
 
 
1145
  width: 100%;
1146
  accent-color: var(--primary);
1147
  }
1148
 
 
 
 
1149
  .mini-transcript {
1150
  display: grid;
1151
- gap: 10px;
1152
- border-top: 1px solid var(--border);
1153
- padding-top: 14px;
1154
  }
1155
 
1156
- .mini-transcript div {
 
 
 
 
 
 
 
 
1157
  display: grid;
1158
- gap: 4px;
 
 
1159
  }
1160
 
1161
- .error-box {
1162
- border: 1px solid color-mix(in srgb, var(--danger) 45%, var(--border));
1163
- border-radius: 7px;
1164
- background: var(--danger-soft);
1165
- padding: 10px;
1166
- color: var(--danger);
1167
- font-size: 0.9rem;
1168
- line-height: 1.45;
 
 
 
 
 
 
 
 
 
1169
  }
1170
 
 
 
 
1171
  .spin {
1172
- animation: spin 0.9s linear infinite;
1173
  }
1174
 
1175
- @keyframes spin {
1176
- to {
1177
- transform: rotate(360deg);
 
 
 
1178
  }
1179
  }
1180
 
1181
- @media (max-width: 1260px) {
 
 
 
1182
  .workspace-grid {
1183
- grid-template-columns: minmax(290px, 340px) minmax(0, 1fr);
 
 
 
 
 
 
1184
  }
1185
 
1186
- .editor-grid {
1187
- grid-template-columns: 64px minmax(0, 1fr);
1188
  }
1189
 
1190
- .inspector-panel {
1191
- grid-column: 1 / -1;
 
1192
  }
1193
 
1194
- .results-column {
1195
- grid-column: 1 / -1;
 
 
1196
  }
1197
 
1198
  .clip-grid {
1199
  grid-template-columns: repeat(2, minmax(0, 1fr));
1200
  }
 
 
 
 
1201
  }
1202
 
1203
- @media (max-width: 900px) {
1204
- .app-header,
1205
- .editor-topbar {
 
 
1206
  align-items: flex-start;
1207
  flex-direction: column;
 
 
 
1208
  }
1209
 
1210
- .workspace-grid,
1211
- .editor-grid {
1212
- grid-template-columns: 1fr;
1213
- }
1214
-
1215
- .tool-rail {
1216
- position: static;
1217
- display: flex;
1218
- overflow-x: auto;
1219
- }
1220
-
1221
- .tool-rail button {
1222
- min-width: 72px;
1223
  }
1224
 
1225
- .input-panel {
1226
- position: static;
1227
- max-height: none;
1228
  }
1229
 
1230
  .clip-grid {
1231
  grid-template-columns: 1fr;
1232
  }
1233
 
1234
- .step-list {
1235
- grid-template-columns: repeat(3, minmax(0, 1fr));
1236
- }
1237
-
1238
- .editor-preview {
1239
- min-height: 420px;
1240
- }
1241
- }
1242
-
1243
- @media (max-width: 620px) {
1244
- .form-grid-two,
1245
  .range-sliders,
1246
  .timeline,
1247
  .cue-row,
@@ -1255,11 +1734,15 @@ textarea:focus,
1255
  grid-template-columns: 1fr;
1256
  }
1257
 
1258
- .progress-percent {
1259
- font-size: 1.5rem;
1260
  }
1261
 
1262
  .step-list {
1263
  grid-template-columns: repeat(2, minmax(0, 1fr));
1264
  }
 
 
 
 
1265
  }
 
2
  @tailwind components;
3
  @tailwind utilities;
4
 
5
+ /* ============================================================
6
+ ElevenClip.AI — Design System
7
+ Dark-first professional. Human-crafted feel.
8
+ ============================================================ */
9
+
10
  :root {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  color-scheme: dark;
12
+ font-family: Inter, ui-sans-serif, system-ui, sans-serif;
13
+
14
+ /* Core palette — dark (default) */
15
+ --bg: #0a0c12;
16
+ --surface: #13171f;
17
+ --surface2: #1c2130;
18
+ --border: #252d3f;
19
+ --border-strong:#2f3a52;
20
+
21
+ /* Brand */
22
+ --primary: #818cf8;
23
+ --primary-dim: #4f56b3;
24
+ --primary-glow: rgba(129,140,248,0.18);
25
+ --primary-ring: rgba(129,140,248,0.35);
26
+
27
+ /* Accent / score */
28
+ --accent: #f59e0b;
29
+ --accent-soft: rgba(245,158,11,0.12);
30
+
31
+ /* Semantic */
32
+ --success: #34d399;
33
+ --success-soft: rgba(52,211,153,0.12);
34
+ --danger: #f87171;
35
+ --danger-soft: rgba(248,113,113,0.12);
36
+
37
+ /* Text */
38
+ --text: #e2e8f0;
39
+ --text-muted: #8896a8;
40
+ --text-soft: #4a5568;
41
+
42
+ /* Shadows */
43
+ --shadow-sm: 0 1px 3px rgba(0,0,0,0.4), 0 1px 2px rgba(0,0,0,0.3);
44
+ --shadow-md: 0 4px 16px rgba(0,0,0,0.45), 0 1px 4px rgba(0,0,0,0.3);
45
+ --shadow-lg: 0 8px 32px rgba(0,0,0,0.55), 0 2px 8px rgba(0,0,0,0.4);
46
+ --shadow-glow: 0 0 0 1px var(--primary-ring), 0 8px 28px rgba(129,140,248,0.14);
47
+
48
+ --radius: 10px;
49
+ --radius-sm: 6px;
50
+ --radius-lg: 14px;
51
+ }
52
+
53
+ /* Light theme overrides */
54
+ :root[data-theme="light"] {
55
+ color-scheme: light;
56
+ --bg: #f0f2f7;
57
+ --surface: #ffffff;
58
+ --surface2: #f5f7fb;
59
+ --border: #d8dde8;
60
+ --border-strong:#b6bece;
61
+
62
+ --primary: #4f56e8;
63
+ --primary-dim: #3b43d6;
64
+ --primary-glow: rgba(79,86,232,0.12);
65
+ --primary-ring: rgba(79,86,232,0.3);
66
+
67
+ --accent: #d97706;
68
+ --accent-soft: rgba(217,119,6,0.1);
69
+
70
+ --success: #059669;
71
+ --success-soft: rgba(5,150,105,0.1);
72
+ --danger: #dc2626;
73
+ --danger-soft: rgba(220,38,38,0.1);
74
+
75
+ --text: #111827;
76
+ --text-muted: #4b5563;
77
+ --text-soft: #9ca3af;
78
+
79
+ --shadow-sm: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04);
80
+ --shadow-md: 0 4px 16px rgba(0,0,0,0.08), 0 1px 4px rgba(0,0,0,0.04);
81
+ --shadow-lg: 0 8px 32px rgba(0,0,0,0.1), 0 2px 8px rgba(0,0,0,0.06);
82
+ --shadow-glow: 0 0 0 1px var(--primary-ring), 0 8px 28px rgba(79,86,232,0.1);
83
+ }
84
+
85
+ /* ============================================================
86
+ Base reset
87
+ ============================================================ */
88
+ *,
89
+ *::before,
90
+ *::after {
91
  box-sizing: border-box;
92
  }
93
 
 
95
  margin: 0;
96
  background: var(--bg);
97
  color: var(--text);
98
+ -webkit-font-smoothing: antialiased;
99
+ -moz-osx-font-smoothing: grayscale;
100
  }
101
 
102
  button,
 
113
 
114
  button:disabled {
115
  cursor: not-allowed;
116
+ opacity: 0.5;
117
+ }
118
+
119
+ /* ============================================================
120
+ Keyframe animations
121
+ ============================================================ */
122
+ @keyframes fadeInUp {
123
+ from {
124
+ opacity: 0;
125
+ transform: translateY(8px);
126
+ }
127
+ to {
128
+ opacity: 1;
129
+ transform: translateY(0);
130
+ }
131
+ }
132
+
133
+ @keyframes spin {
134
+ to { transform: rotate(360deg); }
135
+ }
136
+
137
+ @keyframes progress-shimmer {
138
+ 0% { background-position: -200% center; }
139
+ 100% { background-position: 200% center; }
140
+ }
141
+
142
+ @keyframes caption-pop {
143
+ from { transform: translateX(-50%) scale(0.97); }
144
+ to { transform: translateX(-50%) scale(1.04); }
145
+ }
146
+
147
+ @keyframes caption-bounce {
148
+ from { transform: translateX(-50%) translateY(0); }
149
+ to { transform: translateX(-50%) translateY(-8px); }
150
+ }
151
+
152
+ @keyframes pulse-ring {
153
+ 0% { box-shadow: 0 0 0 0 var(--primary-ring); }
154
+ 70% { box-shadow: 0 0 0 6px transparent; }
155
+ 100% { box-shadow: 0 0 0 0 transparent; }
156
  }
157
 
158
+ /* ============================================================
159
+ App shell
160
+ ============================================================ */
161
  .app-shell {
162
  min-height: 100vh;
163
+ background: var(--bg);
 
 
164
  color: var(--text);
165
  }
166
 
167
+ /* ============================================================
168
+ Header
169
+ ============================================================ */
170
  .app-header {
171
  position: sticky;
172
  top: 0;
173
+ z-index: 30;
174
  display: flex;
175
+ min-height: 72px;
176
  align-items: center;
177
  justify-content: space-between;
178
  gap: 20px;
179
+ padding: 0 clamp(16px, 4vw, 48px);
180
  border-bottom: 1px solid var(--border);
181
+ background: rgba(10, 12, 18, 0.88);
182
+ backdrop-filter: blur(20px);
183
+ -webkit-backdrop-filter: blur(20px);
184
+ }
185
+
186
+ :root[data-theme="light"] .app-header {
187
+ background: rgba(255, 255, 255, 0.88);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  }
189
 
190
  .brand-block {
191
+ display: flex;
192
+ align-items: center;
193
  gap: 12px;
194
+ min-width: 0;
195
  }
196
 
197
  .brand-mark {
198
  display: grid;
 
 
 
199
  place-items: center;
200
+ width: 42px;
201
+ height: 42px;
202
+ flex: 0 0 auto;
203
+ border-radius: var(--radius-sm);
204
+ background: linear-gradient(135deg, var(--primary-dim), var(--primary));
205
+ color: #fff;
206
+ box-shadow: 0 2px 8px var(--primary-glow);
207
+ transition: box-shadow 150ms ease, transform 150ms ease;
208
  }
209
 
210
+ .brand-mark:hover {
211
+ box-shadow: 0 4px 16px var(--primary-ring);
212
+ transform: scale(1.05);
 
 
 
 
213
  }
214
 
215
  .brand-block h1 {
216
+ margin: 0;
217
+ font-size: 1.15rem;
218
  font-weight: 800;
219
+ color: var(--text);
220
+ letter-spacing: -0.01em;
221
  }
222
 
223
+ .brand-block p {
 
 
 
224
  margin: 2px 0 0;
225
+ font-size: 0.78rem;
226
  color: var(--text-muted);
227
+ line-height: 1.4;
228
+ white-space: nowrap;
229
+ overflow: hidden;
230
+ text-overflow: ellipsis;
231
+ max-width: 340px;
232
  }
233
 
234
  .header-actions {
235
+ display: flex;
236
+ align-items: center;
237
  flex-wrap: wrap;
238
  justify-content: flex-end;
239
  gap: 8px;
240
  }
241
 
242
+ /* ============================================================
243
+ Pills
244
+ ============================================================ */
245
  .mode-pill,
246
  .status-pill {
247
+ display: inline-flex;
248
+ align-items: center;
249
+ min-height: 30px;
250
  padding: 0 10px;
251
+ border: 1px solid var(--border);
252
+ border-radius: 999px;
253
+ background: var(--surface2);
254
  color: var(--text-muted);
255
+ font-size: 0.7rem;
256
+ font-weight: 700;
257
+ letter-spacing: 0.06em;
258
  text-transform: uppercase;
259
+ transition: color 150ms ease, background 150ms ease, border-color 150ms ease;
260
  }
261
 
262
  .mode-pill.prod,
263
  .status-pill.completed {
264
+ border-color: rgba(52, 211, 153, 0.35);
265
+ background: var(--success-soft);
266
+ color: var(--success);
267
  }
268
 
269
  .mode-pill.demo,
270
  .status-pill.running,
271
  .status-pill.queued {
272
+ border-color: rgba(129, 140, 248, 0.35);
273
+ background: var(--primary-glow);
274
+ color: var(--primary);
275
+ animation: pulse-ring 2s ease-out infinite;
 
 
 
 
 
 
276
  }
277
 
278
  .status-pill.failed {
279
+ border-color: rgba(248, 113, 113, 0.35);
280
  background: var(--danger-soft);
281
  color: var(--danger);
282
  }
283
 
284
+ /* ============================================================
285
+ Toolbar controls
286
+ ============================================================ */
287
+ .toolbar-select {
288
+ display: inline-flex;
289
+ align-items: center;
290
+ gap: 6px;
291
+ min-height: 34px;
292
+ padding: 0 10px;
 
293
  border: 1px solid var(--border);
294
+ border-radius: var(--radius-sm);
295
  background: var(--surface);
296
+ color: var(--text-muted);
297
+ font-size: 0.82rem;
298
+ transition: border-color 150ms ease, background 150ms ease, color 150ms ease;
299
  }
300
 
301
+ .toolbar-select:hover {
302
+ border-color: var(--border-strong);
303
+ background: var(--surface2);
304
+ color: var(--text);
305
  }
306
 
307
  .toolbar-select select {
 
308
  border: 0;
309
+ background: transparent;
310
+ color: inherit;
 
311
  outline: none;
312
+ min-width: 44px;
313
+ -webkit-text-fill-color: currentColor;
314
  }
315
 
316
  .toolbar-select select option,
317
  .text-input option {
318
+ background: #1c2130;
319
+ color: #e2e8f0;
320
+ }
321
+
322
+ :root[data-theme="light"] .toolbar-select select option,
323
+ :root[data-theme="light"] .text-input option {
324
  background: #ffffff;
325
  color: #111827;
326
  }
327
 
328
+ .icon-button {
329
+ display: inline-flex;
330
+ align-items: center;
331
+ justify-content: center;
332
+ width: 34px;
333
+ height: 34px;
334
+ border: 1px solid var(--border);
335
+ border-radius: var(--radius-sm);
336
+ background: var(--surface);
337
+ color: var(--text-muted);
338
+ text-decoration: none;
339
+ transition: border-color 150ms ease, background 150ms ease, color 150ms ease, transform 150ms ease;
340
+ }
341
+
342
+ .icon-button:hover {
343
+ border-color: var(--border-strong);
344
+ background: var(--surface2);
345
+ color: var(--text);
346
+ transform: scale(1.08);
347
  }
348
 
349
+ .icon-button:active {
350
+ transform: scale(0.94);
 
351
  }
352
 
353
+ /* ============================================================
354
+ Layout — workspace
355
+ ============================================================ */
356
  .workspace-grid {
357
  display: grid;
358
+ grid-template-columns: 320px minmax(0, 1fr);
359
+ grid-template-rows: auto;
360
+ gap: 16px;
361
  align-items: start;
362
+ padding: 20px clamp(16px, 3vw, 44px) 48px;
363
  }
364
 
365
+ .sidebar-column {
366
+ position: sticky;
367
+ top: 88px;
368
+ max-height: calc(100vh - 104px);
369
+ overflow-y: auto;
370
+ scrollbar-width: thin;
371
+ scrollbar-color: var(--border) transparent;
372
+ }
373
+
374
+ .sidebar-column::-webkit-scrollbar {
375
+ width: 4px;
376
+ }
377
+ .sidebar-column::-webkit-scrollbar-thumb {
378
+ background: var(--border);
379
+ border-radius: 999px;
380
+ }
381
+
382
+ .main-column {
383
  display: grid;
 
384
  gap: 16px;
385
+ align-content: start;
386
  }
387
 
388
+ /* ============================================================
389
+ Panels
390
+ ============================================================ */
391
  .panel {
392
  border: 1px solid var(--border);
393
+ border-radius: var(--radius-lg);
394
+ background: var(--surface);
395
+ box-shadow: var(--shadow-sm);
396
+ animation: fadeInUp 240ms ease both;
397
  }
398
 
399
+ .panel-body {
 
 
 
 
 
400
  padding: 18px;
401
  }
402
 
403
+ .input-panel,
404
+ .progress-panel,
405
+ .transcript-panel,
406
+ .clips-panel {
407
+ padding: 20px;
 
 
408
  }
409
 
410
  .panel-heading {
411
+ display: flex;
412
+ align-items: center;
413
  justify-content: space-between;
414
  gap: 12px;
415
+ margin-bottom: 16px;
416
  }
417
 
418
  .panel-heading.compact {
 
422
  .panel-heading h2,
423
  .editor-topbar h2,
424
  .mini-transcript h3 {
425
+ margin: 0;
426
+ font-size: 0.78rem;
427
+ font-weight: 700;
428
+ letter-spacing: 0.08em;
429
  text-transform: uppercase;
430
+ color: var(--text);
431
  }
432
 
433
+ .panel-heading p {
434
+ margin: 3px 0 0;
435
+ font-size: 0.82rem;
436
+ color: var(--text-muted);
437
+ line-height: 1.4;
438
+ }
439
+
440
+ .panel-heading-icon {
441
+ display: grid;
442
+ place-items: center;
443
+ width: 32px;
444
+ height: 32px;
445
+ border-radius: var(--radius-sm);
446
+ background: var(--surface2);
447
+ color: var(--text-muted);
448
+ flex: 0 0 auto;
449
+ }
450
+
451
+ /* ============================================================
452
+ Form elements
453
+ ============================================================ */
454
+ .form-stack {
455
+ display: grid;
456
+ gap: 14px;
457
  }
458
 
459
  .form-grid-two {
460
  display: grid;
461
+ grid-template-columns: 1fr 1fr;
462
+ gap: 10px;
463
  }
464
 
465
  .field-block {
 
468
  }
469
 
470
  .field-label {
471
+ font-size: 0.78rem;
472
+ font-weight: 600;
473
+ color: var(--text-muted);
474
+ letter-spacing: 0.02em;
475
  }
476
 
477
  .text-input,
 
478
  textarea,
479
+ .file-input {
480
  width: 100%;
481
+ border: 1px solid var(--border);
482
+ border-radius: var(--radius-sm);
483
+ background: var(--surface2);
484
  color: var(--text);
485
  outline: none;
486
+ transition: border-color 150ms ease, background 150ms ease, box-shadow 150ms ease;
487
  }
488
 
489
  .text-input,
490
  .file-input {
491
+ min-height: 40px;
492
+ padding: 8px 12px;
493
+ font-size: 0.88rem;
494
  }
495
 
496
  textarea {
497
  resize: vertical;
498
+ padding: 9px 12px;
499
+ font-size: 0.88rem;
500
+ line-height: 1.55;
501
+ }
502
+
503
+ .text-input:hover,
504
+ textarea:hover,
505
+ .file-input:hover {
506
+ border-color: var(--border-strong);
507
  }
508
 
509
  .text-input:focus,
 
510
  textarea:focus,
511
+ .file-input:focus {
512
  border-color: var(--primary);
513
+ background: var(--surface);
514
+ box-shadow: 0 0 0 3px var(--primary-ring);
515
+ }
516
+
517
+ .helper-text,
518
+ .file-name {
519
+ margin: 0;
520
+ font-size: 0.76rem;
521
+ color: var(--text-soft);
522
+ line-height: 1.45;
523
+ }
524
+
525
+ .divider {
526
+ height: 1px;
527
+ background: var(--border);
528
  }
529
 
530
+ /* ============================================================
531
+ Segmented control
532
+ ============================================================ */
533
  .segmented {
534
  display: grid;
535
  grid-template-columns: 1fr 1fr;
536
+ gap: 3px;
537
+ border: 1px solid var(--border);
538
+ border-radius: var(--radius-sm);
539
+ background: var(--surface2);
540
+ padding: 3px;
541
  }
542
 
543
  .segmented button {
544
  display: flex;
 
545
  align-items: center;
546
  justify-content: center;
547
+ gap: 7px;
548
+ min-height: 36px;
549
+ border: 1px solid transparent;
550
+ border-radius: 5px;
551
  background: transparent;
552
  color: var(--text-muted);
553
+ font-size: 0.84rem;
554
+ font-weight: 600;
555
+ transition: background 150ms ease, color 150ms ease, border-color 150ms ease, box-shadow 150ms ease;
556
+ }
557
+
558
+ .segmented button:hover {
559
+ color: var(--text);
560
+ background: rgba(255,255,255,0.04);
561
  }
562
 
563
  .segmented button.active {
564
+ border-color: var(--border);
565
  background: var(--surface);
566
  color: var(--text);
567
+ box-shadow: var(--shadow-sm);
568
  }
569
 
570
+ /* ============================================================
571
+ Buttons
572
+ ============================================================ */
573
  .primary-button {
574
+ display: inline-flex;
575
+ align-items: center;
576
  justify-content: center;
577
+ gap: 8px;
578
+ min-height: 42px;
579
+ padding: 0 18px;
580
+ border: 1px solid var(--primary-dim);
581
+ border-radius: var(--radius-sm);
582
+ background: linear-gradient(135deg, var(--primary-dim), var(--primary));
583
+ color: #fff;
584
+ font-size: 0.88rem;
585
+ font-weight: 700;
586
+ letter-spacing: 0.01em;
587
+ transition: filter 150ms ease, transform 150ms ease, box-shadow 150ms ease;
588
+ box-shadow: 0 2px 8px var(--primary-glow);
589
+ }
590
+
591
+ .primary-button:hover:not(:disabled) {
592
+ filter: brightness(1.12);
593
+ box-shadow: 0 4px 16px var(--primary-ring);
594
  }
595
 
596
+ .primary-button:active:not(:disabled) {
597
+ transform: scale(0.97);
598
+ filter: brightness(0.95);
599
  }
600
 
601
  .primary-button:disabled {
602
+ opacity: 0.45;
603
  }
604
 
605
+ .ghost-button {
606
+ display: inline-flex;
607
+ align-items: center;
608
+ gap: 8px;
609
+ min-height: 36px;
610
+ padding: 0 14px;
611
+ border: 1px solid var(--border);
612
+ border-radius: var(--radius-sm);
613
+ background: var(--surface);
614
+ color: var(--text-muted);
615
+ font-size: 0.84rem;
616
+ font-weight: 600;
617
+ text-decoration: none;
618
+ transition: background 150ms ease, color 150ms ease, border-color 150ms ease, transform 150ms ease;
619
+ }
620
+
621
+ .ghost-button:hover {
622
+ background: var(--surface2);
623
+ border-color: var(--border-strong);
624
  color: var(--text);
625
+ }
626
+
627
+ .ghost-button:active {
628
+ transform: scale(0.97);
629
+ }
630
+
631
+ /* Inline action buttons (clip cards, inspector) */
632
+ .btn {
633
+ display: inline-flex;
634
+ align-items: center;
635
+ justify-content: center;
636
+ gap: 6px;
637
+ min-height: 34px;
638
+ padding: 0 10px;
639
+ border: 1px solid var(--border);
640
+ border-radius: var(--radius-sm);
641
+ background: var(--surface2);
642
+ color: var(--text-muted);
643
+ font-size: 0.8rem;
644
+ font-weight: 600;
645
+ text-decoration: none;
646
+ transition: background 150ms ease, color 150ms ease, border-color 150ms ease, transform 150ms ease;
647
+ white-space: nowrap;
648
+ }
649
+
650
+ .btn:hover {
651
+ background: var(--surface);
652
+ border-color: var(--border-strong);
653
+ color: var(--text);
654
+ filter: brightness(1.08);
655
+ }
656
+
657
+ .btn:active {
658
+ transform: scale(0.95);
659
+ }
660
+
661
+ .btn-primary {
662
+ border-color: var(--primary-dim);
663
+ background: var(--primary-glow);
664
+ color: var(--primary);
665
+ }
666
+
667
+ .btn-primary:hover {
668
+ background: var(--primary);
669
+ color: #fff;
670
+ border-color: var(--primary);
671
+ }
672
+
673
+ .btn-success {
674
+ border-color: rgba(52,211,153,0.35);
675
+ background: var(--success-soft);
676
+ color: var(--success);
677
+ }
678
+
679
+ .btn-success:hover {
680
+ background: var(--success);
681
+ color: #0a0c12;
682
+ border-color: var(--success);
683
+ }
684
+
685
+ .btn-danger {
686
+ border-color: rgba(248,113,113,0.3);
687
+ background: var(--danger-soft);
688
+ color: var(--danger);
689
+ }
690
+
691
+ .btn-danger:hover {
692
+ background: var(--danger);
693
+ color: #fff;
694
+ border-color: var(--danger);
695
+ }
696
+
697
+ .btn-icon {
698
+ width: 34px;
699
+ padding: 0;
700
+ flex: 0 0 auto;
701
+ }
702
+
703
+ /* ============================================================
704
+ Error / status boxes
705
+ ============================================================ */
706
+ .error-box {
707
+ padding: 10px 14px;
708
+ border: 1px solid rgba(248,113,113,0.35);
709
+ border-radius: var(--radius-sm);
710
+ background: var(--danger-soft);
711
+ color: var(--danger);
712
+ font-size: 0.86rem;
713
+ line-height: 1.5;
714
+ }
715
+
716
+ /* ============================================================
717
+ Progress panel
718
+ ============================================================ */
719
+ .progress-percent {
720
  font-size: 2rem;
721
+ font-weight: 800;
722
  font-variant-numeric: tabular-nums;
723
+ color: var(--text);
724
+ flex: 0 0 auto;
725
  }
726
 
727
  .progress-track {
728
+ height: 8px;
729
  overflow: hidden;
730
  border-radius: 999px;
731
+ background: var(--surface2);
732
+ margin-top: 4px;
733
+ margin-bottom: 16px;
734
  }
735
 
736
  .progress-bar {
737
  height: 100%;
738
  border-radius: 999px;
739
+ background-size: 200% auto;
740
+ background-image: linear-gradient(90deg, var(--primary-dim), var(--primary), var(--accent), var(--primary));
741
+ animation: progress-shimmer 2.4s linear infinite;
742
+ transition: width 300ms ease;
743
  }
744
 
745
  .step-list {
746
  display: grid;
747
  grid-template-columns: repeat(6, minmax(0, 1fr));
748
+ gap: 6px;
 
749
  }
750
 
751
  .step-item {
752
  display: grid;
753
+ gap: 5px;
 
754
  color: var(--text-soft);
755
+ font-size: 0.7rem;
756
+ font-weight: 600;
757
  }
758
 
759
  .step-item span {
760
  display: grid;
761
+ width: 26px;
762
+ height: 26px;
763
  place-items: center;
764
  border: 1px solid var(--border);
765
+ border-radius: 50%;
766
+ background: var(--surface2);
767
  font-variant-numeric: tabular-nums;
768
+ font-size: 0.7rem;
769
+ transition: background 200ms ease, border-color 200ms ease, color 200ms ease;
770
  }
771
 
772
  .step-item p {
773
  margin: 0;
774
  overflow-wrap: anywhere;
775
+ line-height: 1.35;
776
+ }
777
+
778
+ .step-item.done {
779
+ color: var(--success);
780
+ }
781
+
782
+ .step-item.done span {
783
+ border-color: rgba(52,211,153,0.5);
784
+ background: var(--success-soft);
785
+ color: var(--success);
786
  }
787
 
 
788
  .step-item.active {
789
+ color: var(--primary);
790
  }
791
 
 
792
  .step-item.active span {
793
  border-color: var(--primary);
794
+ background: var(--primary-glow);
795
+ color: var(--primary);
796
  }
797
 
798
  .timing-grid {
799
  display: grid;
800
+ grid-template-columns: repeat(auto-fit, minmax(108px, 1fr));
801
+ gap: 8px;
802
+ margin-top: 12px;
803
  }
804
 
805
  .timing-grid div {
806
+ padding: 10px 12px;
807
  border: 1px solid var(--border);
808
+ border-radius: var(--radius-sm);
809
+ background: var(--surface2);
 
810
  }
811
 
812
+ .timing-grid span {
813
+ display: block;
814
+ font-size: 0.7rem;
815
+ font-weight: 600;
 
816
  color: var(--text-muted);
817
+ text-transform: uppercase;
818
+ letter-spacing: 0.05em;
819
  }
820
 
821
  .timing-grid strong {
822
  display: block;
823
  margin-top: 4px;
824
+ font-size: 1.1rem;
825
+ font-weight: 700;
826
  font-variant-numeric: tabular-nums;
827
+ color: var(--text);
828
  }
829
 
830
+ /* ============================================================
831
+ Transcript panel
832
+ ============================================================ */
833
  .transcript-list {
834
  display: grid;
835
+ gap: 6px;
836
+ max-height: 380px;
837
+ overflow-y: auto;
838
+ padding-right: 4px;
839
+ scrollbar-width: thin;
840
+ scrollbar-color: var(--border) transparent;
841
  }
842
 
843
  .transcript-row {
844
  display: grid;
845
+ grid-template-columns: 108px 1fr;
846
+ gap: 10px;
 
847
  padding-bottom: 8px;
848
+ border-bottom: 1px solid var(--border);
849
  }
850
 
851
+ .transcript-row span {
852
+ font-size: 0.72rem;
853
+ font-weight: 600;
854
+ color: var(--text-muted);
855
+ font-variant-numeric: tabular-nums;
856
+ padding-top: 2px;
857
+ }
858
+
859
+ .transcript-row p {
860
  margin: 0;
861
+ font-size: 0.86rem;
862
  color: var(--text);
 
863
  line-height: 1.55;
864
  }
865
 
866
+ /* ============================================================
867
+ Empty state
868
+ ============================================================ */
869
  .empty-state {
870
  display: grid;
871
+ min-height: 260px;
872
  place-items: center;
873
  align-content: center;
874
  gap: 10px;
875
  border: 1px dashed var(--border-strong);
876
  border-radius: var(--radius);
877
+ background: var(--surface2);
878
  color: var(--text-muted);
879
  text-align: center;
880
+ padding: 32px 24px;
881
  }
882
 
883
  .empty-state h3 {
884
  margin: 0;
 
885
  font-size: 1rem;
886
+ font-weight: 700;
887
+ color: var(--text);
888
  }
889
 
890
  .empty-state p {
891
+ max-width: 260px;
892
  margin: 0;
893
+ font-size: 0.84rem;
894
+ line-height: 1.55;
895
  }
896
 
897
+ /* ============================================================
898
+ Clip grid & cards
899
+ ============================================================ */
900
  .clip-grid {
901
  display: grid;
902
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
903
  gap: 14px;
904
  align-items: start;
905
  }
906
 
907
  .clip-card {
908
+ display: grid;
909
  overflow: hidden;
910
  border: 1px solid var(--border);
911
+ border-radius: var(--radius-lg);
912
+ background: var(--surface);
913
+ box-shadow: var(--shadow-sm);
914
+ transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease;
915
+ animation: fadeInUp 240ms ease both;
916
+ cursor: default;
917
+ }
918
+
919
+ .clip-card:hover {
920
+ transform: translateY(-3px) scale(1.012);
921
+ box-shadow: var(--shadow-md), 0 0 0 1px var(--border-strong);
922
+ border-color: var(--border-strong);
923
  }
924
 
925
  .clip-video {
926
  display: grid;
927
  aspect-ratio: 9 / 16;
928
+ max-height: 240px;
929
  place-items: center;
930
+ background: #070b14;
931
+ color: var(--text-soft);
932
+ overflow: hidden;
933
  }
934
 
935
  .clip-video video {
 
940
 
941
  .clip-body {
942
  display: grid;
943
+ gap: 10px;
944
+ padding: 14px;
945
  }
946
 
947
+ /* Title row: title + score badge side by side */
948
+ .clip-meta {
949
  display: flex;
950
  align-items: flex-start;
951
  justify-content: space-between;
952
  gap: 10px;
953
  }
954
 
955
+ .clip-title {
956
  margin: 0;
957
+ font-size: 0.9rem;
958
+ font-weight: 700;
959
  color: var(--text);
 
 
960
  line-height: 1.35;
961
  }
962
 
963
+ .clip-reason {
964
+ margin: 4px 0 0;
965
+ font-size: 0.78rem;
966
  color: var(--text-muted);
 
967
  line-height: 1.45;
968
+ display: -webkit-box;
969
+ -webkit-box-orient: vertical;
970
+ -webkit-line-clamp: 2;
971
+ overflow: hidden;
972
  }
973
 
974
+ .score-badge {
975
  display: inline-flex;
976
+ align-items: center;
977
+ gap: 4px;
978
  min-width: 52px;
979
+ height: 28px;
980
  flex: 0 0 auto;
981
+ padding: 0 8px;
982
+ border-radius: 999px;
 
 
983
  background: var(--accent-soft);
984
+ border: 1px solid rgba(245,158,11,0.25);
985
  color: var(--accent);
986
+ font-size: 0.78rem;
987
+ font-weight: 800;
988
+ font-variant-numeric: tabular-nums;
989
  }
990
 
991
+ .clip-duration-row {
992
+ display: flex;
993
+ align-items: center;
994
  justify-content: space-between;
995
  gap: 8px;
996
+ font-size: 0.76rem;
997
+ font-weight: 600;
998
  color: var(--text-muted);
 
 
999
  }
1000
 
1001
+ .clip-duration-row span {
1002
  display: inline-flex;
1003
  align-items: center;
1004
+ gap: 4px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1005
  }
1006
 
1007
  .subtitle-snippet {
 
 
1008
  margin: 0;
1009
+ padding: 8px 10px;
 
 
1010
  border: 1px solid var(--border);
1011
+ border-radius: var(--radius-sm);
1012
+ background: var(--surface2);
1013
+ color: var(--text-muted);
1014
+ font-size: 0.78rem;
1015
+ line-height: 1.5;
1016
+ display: -webkit-box;
1017
+ -webkit-box-orient: vertical;
1018
+ -webkit-line-clamp: 2;
1019
+ overflow: hidden;
1020
+ min-height: 46px;
 
 
 
1021
  }
1022
 
1023
+ /* Clip action row */
1024
+ .clip-actions {
1025
+ display: grid;
1026
+ grid-template-columns: 1fr auto auto auto auto;
1027
+ gap: 6px;
1028
+ align-items: center;
1029
  }
1030
 
1031
+ /* ============================================================
1032
+ Editor page
1033
+ ============================================================ */
1034
  .editor-shell {
1035
+ display: flex;
1036
+ flex-direction: column;
1037
+ min-height: calc(100vh - 72px);
1038
+ padding: 0;
1039
  }
1040
 
1041
  .editor-topbar {
1042
+ display: flex;
1043
+ align-items: center;
1044
+ gap: 14px;
1045
+ padding: 14px clamp(16px, 3vw, 44px);
1046
+ border-bottom: 1px solid var(--border);
1047
+ background: var(--surface);
1048
+ position: sticky;
1049
+ top: 72px;
1050
+ z-index: 20;
1051
  }
1052
 
1053
+ .editor-topbar-info {
1054
  flex: 1;
1055
+ min-width: 0;
1056
  }
1057
 
1058
+ .editor-topbar-info h2 {
1059
+ margin: 0;
1060
+ font-size: 0.9rem;
1061
+ font-weight: 700;
1062
+ color: var(--text);
1063
+ white-space: nowrap;
1064
+ overflow: hidden;
1065
+ text-overflow: ellipsis;
1066
  }
1067
 
1068
+ .editor-topbar-info p {
1069
+ margin: 3px 0 0;
1070
+ font-size: 0.76rem;
1071
+ color: var(--text-muted);
 
1072
  }
1073
 
1074
+ .editor-topbar-actions {
1075
+ display: flex;
1076
+ align-items: center;
 
1077
  gap: 8px;
1078
+ flex: 0 0 auto;
 
 
 
 
1079
  }
1080
 
1081
+ .editor-body {
1082
  display: grid;
1083
+ grid-template-columns: minmax(0, 3fr) minmax(320px, 2fr);
1084
+ gap: 0;
1085
+ flex: 1;
1086
+ align-items: start;
 
 
 
 
 
1087
  }
1088
 
1089
+ .editor-left {
1090
+ padding: 20px clamp(12px, 2vw, 28px) 32px;
1091
+ border-right: 1px solid var(--border);
1092
+ display: grid;
1093
+ gap: 20px;
1094
+ align-content: start;
1095
  }
1096
 
1097
+ .editor-right {
1098
+ padding: 20px clamp(12px, 2vw, 24px) 32px;
1099
  display: grid;
1100
+ gap: 20px;
1101
+ align-content: start;
1102
+ max-height: calc(100vh - 132px);
1103
+ overflow-y: auto;
1104
+ scrollbar-width: thin;
1105
+ scrollbar-color: var(--border) transparent;
1106
+ position: sticky;
1107
+ top: 132px;
1108
  }
1109
 
1110
+ /* ============================================================
1111
+ Editor preview
1112
+ ============================================================ */
1113
  .editor-preview {
 
1114
  position: relative;
1115
+ display: grid;
 
 
1116
  place-items: center;
1117
+ min-height: 400px;
1118
+ max-height: 60vh;
1119
+ overflow: hidden;
1120
+ border: 1px solid var(--border);
1121
+ border-radius: var(--radius-lg);
1122
+ background: #050810;
1123
+ color: var(--text-soft);
1124
  }
1125
 
1126
  .editor-preview video {
1127
  width: 100%;
1128
  height: 100%;
1129
+ max-height: 60vh;
1130
  object-fit: contain;
1131
  }
1132
 
1133
  .caption-preview {
1134
  position: absolute;
1135
  left: 50%;
1136
+ width: min(84%, 640px);
1137
  transform: translateX(-50%);
 
1138
  font-weight: 900;
1139
  letter-spacing: 0;
1140
+ line-height: 1.1;
1141
  text-align: center;
 
1142
  pointer-events: none;
1143
  }
1144
 
 
1154
  animation: caption-bounce 760ms ease-in-out infinite alternate;
1155
  }
1156
 
1157
+ /* ============================================================
1158
+ Range editor / timeline
1159
+ ============================================================ */
1160
+ .section-panel {
1161
+ border: 1px solid var(--border);
1162
+ border-radius: var(--radius);
1163
+ background: var(--surface2);
1164
+ padding: 16px;
1165
+ }
1166
+
1167
+ .section-panel .panel-heading {
1168
+ margin-bottom: 14px;
1169
  }
1170
 
1171
+ .editor-toolbox {
1172
+ display: flex;
1173
+ flex-wrap: wrap;
1174
+ gap: 6px;
1175
+ align-items: center;
1176
+ margin-bottom: 2px;
 
1177
  }
1178
 
1179
+ .editor-toolbox-label {
1180
+ font-size: 0.75rem;
1181
+ font-weight: 700;
1182
+ color: var(--text-muted);
1183
+ text-transform: uppercase;
1184
+ letter-spacing: 0.06em;
1185
+ margin-right: 4px;
1186
+ flex: 0 0 auto;
 
 
1187
  }
1188
 
1189
  .timeline-visual {
1190
  position: relative;
1191
+ height: 48px;
1192
  overflow: hidden;
 
 
1193
  border: 1px solid var(--border);
1194
+ border-radius: var(--radius-sm);
1195
+ background: var(--surface);
1196
  }
1197
 
1198
  .timeline-fill {
1199
  position: absolute;
1200
+ inset: 20px 12px;
1201
  border-radius: 999px;
1202
+ background: var(--surface2);
1203
  }
1204
 
1205
  .timeline-window {
1206
  position: absolute;
1207
+ top: 11px;
1208
  height: 26px;
1209
  border: 2px solid var(--primary);
1210
  border-radius: 999px;
1211
+ background: var(--primary-glow);
1212
+ box-shadow: 0 0 8px var(--primary-glow);
1213
+ transition: left 120ms ease, width 120ms ease;
1214
  }
1215
 
1216
  .timeline-window::before,
1217
  .timeline-window::after {
1218
+ content: "";
1219
  position: absolute;
1220
+ top: 5px;
1221
  width: 2px;
1222
+ height: 12px;
1223
  border-radius: 999px;
1224
+ background: var(--primary);
 
 
 
 
 
1225
  }
1226
 
1227
+ .timeline-window::before { left: 7px; }
1228
+ .timeline-window::after { right: 7px; }
 
1229
 
1230
  .timeline-visual span {
1231
  position: absolute;
1232
+ top: 16px;
1233
  width: 1px;
1234
+ height: 16px;
1235
+ background: var(--border);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1236
  }
1237
 
1238
  .range-sliders {
1239
  display: grid;
1240
+ grid-template-columns: 1fr 1fr;
1241
  gap: 12px;
1242
  }
1243
 
1244
  .range-sliders label {
1245
  display: grid;
1246
+ gap: 5px;
1247
+ font-size: 0.74rem;
1248
+ font-weight: 700;
1249
  color: var(--text-muted);
1250
+ text-transform: uppercase;
1251
+ letter-spacing: 0.05em;
1252
  }
1253
 
1254
  .range-sliders input[type="range"] {
 
1256
  accent-color: var(--primary);
1257
  }
1258
 
1259
+ .timeline {
1260
+ display: grid;
1261
+ grid-template-columns: 1fr 1fr 72px;
1262
+ gap: 10px;
1263
+ align-items: end;
1264
+ }
1265
+
1266
  .timeline label {
1267
  display: grid;
1268
  gap: 5px;
1269
+ font-size: 0.74rem;
1270
+ font-weight: 700;
1271
  color: var(--text-muted);
 
 
1272
  }
1273
 
1274
  .timeline input {
1275
+ width: 100%;
1276
+ min-height: 36px;
1277
+ padding: 7px 10px;
1278
+ border: 1px solid var(--border);
1279
+ border-radius: var(--radius-sm);
1280
+ background: var(--surface);
1281
+ color: var(--text);
1282
+ outline: none;
1283
+ font-variant-numeric: tabular-nums;
1284
+ transition: border-color 150ms ease, box-shadow 150ms ease;
1285
+ }
1286
+
1287
+ .timeline input:focus {
1288
+ border-color: var(--primary);
1289
+ box-shadow: 0 0 0 3px var(--primary-ring);
1290
  }
1291
 
1292
  .timeline strong {
1293
+ display: flex;
1294
+ align-items: center;
1295
+ justify-content: center;
1296
+ min-height: 36px;
1297
+ border-radius: var(--radius-sm);
1298
+ background: var(--surface2);
1299
+ color: var(--text-muted);
1300
+ font-size: 0.82rem;
1301
  font-variant-numeric: tabular-nums;
1302
+ border: 1px solid var(--border);
1303
  }
1304
 
1305
+ /* ============================================================
1306
+ Timeline tracks workbench
1307
+ ============================================================ */
1308
  .track-stack {
1309
  display: grid;
1310
  gap: 8px;
 
1312
 
1313
  .track-row {
1314
  display: grid;
1315
+ grid-template-columns: 80px 1fr;
1316
  gap: 10px;
1317
  align-items: center;
1318
  }
1319
 
1320
  .track-row > span {
1321
+ font-size: 0.72rem;
1322
+ font-weight: 700;
1323
  color: var(--text-muted);
1324
+ text-transform: uppercase;
1325
+ letter-spacing: 0.04em;
1326
  }
1327
 
1328
  .track-lane {
1329
  position: relative;
1330
+ min-height: 38px;
1331
  overflow: hidden;
1332
  border: 1px solid var(--border);
1333
+ border-radius: var(--radius-sm);
1334
  background:
1335
  repeating-linear-gradient(
1336
  90deg,
1337
  transparent 0,
1338
  transparent 11.8%,
1339
+ rgba(255,255,255,0.03) 12%,
1340
  transparent 12.2%
1341
  ),
1342
+ var(--surface2);
1343
  }
1344
 
1345
  .track-clip {
1346
  position: absolute;
1347
+ top: 5px;
1348
+ bottom: 5px;
1349
  display: flex;
1350
+ min-width: 44px;
1351
  align-items: center;
1352
  overflow: hidden;
1353
+ border-radius: 5px;
1354
+ font-size: 0.68rem;
1355
+ font-weight: 700;
1356
+ padding: 0 7px;
 
 
1357
  white-space: nowrap;
1358
+ text-overflow: ellipsis;
1359
  }
1360
 
1361
  .track-clip.video {
1362
+ background: linear-gradient(90deg, var(--primary-dim), var(--primary));
1363
+ color: #fff;
1364
  }
1365
 
1366
  .track-clip.subtitle {
1367
+ background: var(--accent-soft);
1368
+ border: 1px solid rgba(245,158,11,0.25);
1369
+ color: var(--accent);
1370
  }
1371
 
1372
  .waveform {
1373
  position: absolute;
1374
+ inset: 6px 8px;
1375
  display: flex;
1376
  align-items: center;
1377
+ gap: 3px;
1378
  }
1379
 
1380
  .waveform span {
1381
+ width: 3px;
1382
  border-radius: 999px;
1383
+ background: var(--primary);
1384
+ opacity: 0.6;
1385
  }
1386
 
1387
+ /* ============================================================
1388
+ Subtitle cue editor
1389
+ ============================================================ */
1390
  .cue-list {
1391
  display: grid;
1392
+ gap: 8px;
1393
  }
1394
 
1395
  .cue-row {
1396
  display: grid;
1397
+ grid-template-columns: 104px 1fr;
1398
+ gap: 8px;
1399
  align-items: start;
1400
  }
1401
 
1402
+ .cue-row > span {
1403
+ font-size: 0.72rem;
1404
+ font-weight: 600;
1405
+ color: var(--text-muted);
1406
+ font-variant-numeric: tabular-nums;
1407
+ padding-top: 10px;
1408
+ }
1409
+
1410
+ /* ============================================================
1411
+ Inspector panel
1412
+ ============================================================ */
1413
  .inspector-list {
1414
  display: grid;
1415
+ gap: 10px;
1416
  margin: 0;
1417
  }
1418
 
1419
  .inspector-list div {
1420
+ display: grid;
1421
+ gap: 3px;
1422
  padding-bottom: 10px;
1423
+ border-bottom: 1px solid var(--border);
1424
+ }
1425
+
1426
+ .inspector-list div:last-child {
1427
+ border-bottom: none;
1428
+ padding-bottom: 0;
1429
+ }
1430
+
1431
+ .inspector-list dt {
1432
+ font-size: 0.7rem;
1433
+ font-weight: 700;
1434
+ text-transform: uppercase;
1435
+ letter-spacing: 0.06em;
1436
+ color: var(--text-muted);
1437
  }
1438
 
1439
  .inspector-list dd {
1440
+ margin: 0;
1441
+ font-size: 0.86rem;
1442
  color: var(--text);
1443
  line-height: 1.45;
1444
  }
 
1450
 
1451
  .inspector-actions a,
1452
  .inspector-actions button {
1453
+ display: inline-flex;
1454
+ align-items: center;
1455
+ justify-content: center;
1456
+ gap: 7px;
1457
+ min-height: 38px;
1458
+ padding: 0 14px;
1459
+ border: 1px solid var(--border);
1460
+ border-radius: var(--radius-sm);
1461
+ background: var(--surface2);
1462
+ color: var(--text-muted);
1463
+ font-size: 0.84rem;
1464
+ font-weight: 600;
1465
+ text-decoration: none;
1466
+ cursor: pointer;
1467
+ transition: background 150ms ease, color 150ms ease, border-color 150ms ease, transform 150ms ease;
1468
+ }
1469
+
1470
+ .inspector-actions a:hover,
1471
+ .inspector-actions button:hover {
1472
+ background: var(--surface);
1473
+ border-color: var(--border-strong);
1474
+ color: var(--text);
1475
+ }
1476
+
1477
+ .inspector-actions button:active,
1478
+ .inspector-actions a:active {
1479
+ transform: scale(0.97);
1480
+ }
1481
+
1482
+ .inspector-actions .btn-success {
1483
+ border-color: rgba(52,211,153,0.35);
1484
+ background: var(--success-soft);
1485
+ color: var(--success);
1486
+ }
1487
+
1488
+ .inspector-actions .btn-success:hover {
1489
+ background: var(--success);
1490
+ color: #0a0c12;
1491
  }
1492
 
1493
+ .inspector-actions .btn-danger {
1494
+ border-color: rgba(248,113,113,0.3);
1495
  background: var(--danger-soft);
1496
  color: var(--danger);
1497
  }
1498
 
1499
+ .inspector-actions .btn-danger:hover {
1500
+ background: var(--danger);
1501
+ color: #fff;
1502
+ }
1503
+
1504
+ .inspector-actions .btn-primary {
1505
+ border-color: var(--primary-dim);
1506
+ background: var(--primary-glow);
1507
+ color: var(--primary);
1508
+ }
1509
+
1510
+ .inspector-actions .btn-primary:hover {
1511
+ background: var(--primary);
1512
+ color: #fff;
1513
+ }
1514
+
1515
+ /* ============================================================
1516
+ Caption style panel
1517
+ ============================================================ */
1518
  .caption-style-panel {
1519
+ display: grid;
1520
+ gap: 12px;
1521
  }
1522
 
1523
  .preset-row {
1524
  display: grid;
1525
+ grid-template-columns: repeat(3, 1fr);
1526
+ gap: 7px;
1527
  }
1528
 
1529
  .preset-row button {
1530
+ min-height: 34px;
1531
  border: 1px solid var(--border);
1532
+ border-radius: var(--radius-sm);
1533
+ background: var(--surface2);
1534
+ color: var(--text-muted);
1535
+ font-size: 0.78rem;
1536
+ font-weight: 700;
1537
+ transition: background 150ms ease, color 150ms ease, border-color 150ms ease, transform 150ms ease;
1538
+ }
1539
+
1540
+ .preset-row button:hover {
1541
+ background: var(--primary-glow);
1542
+ border-color: var(--primary-dim);
1543
+ color: var(--primary);
1544
+ }
1545
+
1546
+ .preset-row button:active {
1547
+ transform: scale(0.95);
1548
  }
1549
 
1550
  .style-grid,
 
1560
  .color-field,
1561
  .range-control {
1562
  display: grid;
1563
+ gap: 6px;
1564
  }
1565
 
1566
  .color-field span,
1567
  .range-control span {
1568
  display: flex;
1569
  justify-content: space-between;
1570
+ font-size: 0.72rem;
1571
+ font-weight: 700;
1572
  color: var(--text-muted);
1573
+ text-transform: uppercase;
1574
+ letter-spacing: 0.05em;
1575
  }
1576
 
1577
  .range-control strong {
 
1581
 
1582
  .color-field input {
1583
  width: 100%;
1584
+ height: 36px;
1585
  border: 1px solid var(--border);
1586
+ border-radius: var(--radius-sm);
1587
+ background: var(--surface2);
1588
+ padding: 3px;
1589
+ cursor: pointer;
1590
+ transition: border-color 150ms ease;
1591
  }
1592
 
1593
+ .color-field input:hover {
1594
+ border-color: var(--border-strong);
1595
+ }
1596
+
1597
+ .range-control input[type="range"] {
1598
  width: 100%;
1599
  accent-color: var(--primary);
1600
  }
1601
 
1602
+ /* ============================================================
1603
+ Mini transcript
1604
+ ============================================================ */
1605
  .mini-transcript {
1606
  display: grid;
1607
+ gap: 8px;
 
 
1608
  }
1609
 
1610
+ .mini-transcript h3 {
1611
+ font-size: 0.72rem;
1612
+ text-transform: uppercase;
1613
+ letter-spacing: 0.07em;
1614
+ color: var(--text-muted);
1615
+ margin: 0 0 4px;
1616
+ }
1617
+
1618
+ .mini-transcript > div {
1619
  display: grid;
1620
+ gap: 3px;
1621
+ padding-bottom: 8px;
1622
+ border-bottom: 1px solid var(--border);
1623
  }
1624
 
1625
+ .mini-transcript > div:last-child {
1626
+ border-bottom: none;
1627
+ padding-bottom: 0;
1628
+ }
1629
+
1630
+ .mini-transcript span {
1631
+ font-size: 0.7rem;
1632
+ font-weight: 600;
1633
+ color: var(--text-muted);
1634
+ font-variant-numeric: tabular-nums;
1635
+ }
1636
+
1637
+ .mini-transcript p {
1638
+ margin: 0;
1639
+ font-size: 0.82rem;
1640
+ color: var(--text);
1641
+ line-height: 1.5;
1642
  }
1643
 
1644
+ /* ============================================================
1645
+ Utility
1646
+ ============================================================ */
1647
  .spin {
1648
+ animation: spin 0.85s linear infinite;
1649
  }
1650
 
1651
+ /* ============================================================
1652
+ Responsive — 1300px
1653
+ ============================================================ */
1654
+ @media (max-width: 1300px) {
1655
+ .workspace-grid {
1656
+ grid-template-columns: 290px minmax(0, 1fr);
1657
  }
1658
  }
1659
 
1660
+ /* ============================================================
1661
+ Responsive — 1000px
1662
+ ============================================================ */
1663
+ @media (max-width: 1000px) {
1664
  .workspace-grid {
1665
+ grid-template-columns: 1fr;
1666
+ }
1667
+
1668
+ .sidebar-column {
1669
+ position: static;
1670
+ max-height: none;
1671
+ overflow: visible;
1672
  }
1673
 
1674
+ .editor-body {
1675
+ grid-template-columns: 1fr;
1676
  }
1677
 
1678
+ .editor-left {
1679
+ border-right: none;
1680
+ border-bottom: 1px solid var(--border);
1681
  }
1682
 
1683
+ .editor-right {
1684
+ position: static;
1685
+ max-height: none;
1686
+ overflow: visible;
1687
  }
1688
 
1689
  .clip-grid {
1690
  grid-template-columns: repeat(2, minmax(0, 1fr));
1691
  }
1692
+
1693
+ .step-list {
1694
+ grid-template-columns: repeat(3, minmax(0, 1fr));
1695
+ }
1696
  }
1697
 
1698
+ /* ============================================================
1699
+ Responsive — 720px
1700
+ ============================================================ */
1701
+ @media (max-width: 720px) {
1702
+ .app-header {
1703
  align-items: flex-start;
1704
  flex-direction: column;
1705
+ padding-top: 12px;
1706
+ padding-bottom: 12px;
1707
+ gap: 10px;
1708
  }
1709
 
1710
+ .brand-block p {
1711
+ max-width: 100%;
1712
+ white-space: normal;
 
 
 
 
 
 
 
 
 
 
1713
  }
1714
 
1715
+ .editor-topbar {
1716
+ align-items: flex-start;
1717
+ flex-wrap: wrap;
1718
  }
1719
 
1720
  .clip-grid {
1721
  grid-template-columns: 1fr;
1722
  }
1723
 
 
 
 
 
 
 
 
 
 
 
 
1724
  .range-sliders,
1725
  .timeline,
1726
  .cue-row,
 
1734
  grid-template-columns: 1fr;
1735
  }
1736
 
1737
+ .form-grid-two {
1738
+ grid-template-columns: 1fr;
1739
  }
1740
 
1741
  .step-list {
1742
  grid-template-columns: repeat(2, minmax(0, 1fr));
1743
  }
1744
+
1745
+ .progress-percent {
1746
+ font-size: 1.6rem;
1747
+ }
1748
  }