@bycrux/editor 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/package.json +1 -1
  2. package/src/ControlsInfoModal.tsx +4 -4
  3. package/src/__tests__/no-opacity-modifier-on-editor-vars.test.ts +84 -0
  4. package/src/__tests__/node-shims.d.ts +19 -0
  5. package/src/carousel/AddElementMenu.tsx +1 -1
  6. package/src/carousel/CarouselEditor.tsx +11 -11
  7. package/src/carousel/CarouselRenderModal.tsx +11 -11
  8. package/src/carousel/OverlayPicker.tsx +3 -3
  9. package/src/carousel/SlidePropertyPanel.tsx +6 -6
  10. package/src/carousel/__tests__/CarouselEditor.test.tsx +12 -0
  11. package/src/components/FilmstripScrubber.tsx +1 -1
  12. package/src/crop/VideoSourceCropModal.tsx +4 -4
  13. package/src/state/__tests__/use-project-sync.test.tsx +76 -0
  14. package/src/state/use-project-sync.ts +21 -1
  15. package/src/text/FontPicker.tsx +3 -3
  16. package/src/text/TextFormattingToolbar.tsx +3 -3
  17. package/src/types.ts +16 -0
  18. package/src/ui/Loader.tsx +1 -1
  19. package/src/ui/NumberField.tsx +3 -3
  20. package/src/ui/SwatchInput.tsx +1 -1
  21. package/src/ui/Tooltip.tsx +1 -1
  22. package/src/ui/input.tsx +1 -1
  23. package/src/ui/label.tsx +1 -1
  24. package/src/ui/textarea.tsx +1 -1
  25. package/src/video/AudioPolishModal.tsx +21 -21
  26. package/src/video/CaptionListPanel.test.tsx +8 -7
  27. package/src/video/CaptionListPanel.tsx +17 -17
  28. package/src/video/CaptionRegenModal.tsx +7 -7
  29. package/src/video/CaptionStyleGallery.tsx +1 -1
  30. package/src/video/CommandPalette.tsx +7 -7
  31. package/src/video/ImageToneMenu.tsx +7 -7
  32. package/src/video/OverlayInspector.tsx +7 -7
  33. package/src/video/RenderModal.tsx +52 -52
  34. package/src/video/VersionCompare.tsx +7 -7
  35. package/src/video/VersionPanel.tsx +2 -2
  36. package/src/video/VideoEditor.tsx +43 -23
  37. package/src/video/__tests__/VideoEditor.pendingSurface.test.tsx +233 -0
  38. package/src/video/__tests__/VideoEditor.saveError.test.tsx +105 -0
  39. package/src/video/panels/ClipPropertiesPanel.tsx +4 -4
  40. package/src/video/panels/OverlayContentPanel.tsx +5 -5
  41. package/src/video/preview/OverlayItemsLayer.tsx +7 -2
  42. package/src/video/preview/SocialPreviewMenu.tsx +5 -5
  43. package/src/video/preview/__tests__/resolveOverlayPropPaths.test.ts +21 -0
  44. package/src/video/timeline/EditableSegment.tsx +1 -1
  45. package/src/video/timeline/Scrubber.tsx +2 -2
  46. package/src/video/timeline/SpeedControl.tsx +3 -3
  47. package/src/video/timeline/Timeline.tsx +3 -3
  48. package/src/video/timeline/TrackGutter.tsx +6 -6
  49. package/src/video/timeline/TrackSettingsPopover.tsx +3 -3
  50. package/src/video/timeline/VolumeControl.tsx +3 -3
@@ -63,7 +63,7 @@ export default function ImageToneMenu({ value, onChange, variant = 'icon', mode
63
63
  title="How photos and logos are converted for the HDR render"
64
64
  aria-haspopup="menu"
65
65
  aria-expanded={open}
66
- className="flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-md border border-[var(--editor-border)] bg-[var(--editor-surface)] text-[var(--editor-text)]/80 hover:text-[var(--editor-text)] transition-colors"
66
+ className="flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-md border border-[var(--editor-border)] bg-[var(--editor-surface)] text-[color-mix(in_srgb,var(--editor-text)_80%,transparent)] hover:text-[var(--editor-text)] transition-colors"
67
67
  >
68
68
  <Palette size={12} className="opacity-70" />
69
69
  <span className="opacity-60">Image color:</span>
@@ -79,7 +79,7 @@ export default function ImageToneMenu({ value, onChange, variant = 'icon', mode
79
79
  className={`flex items-center justify-center w-5 h-5 rounded transition-colors ${
80
80
  open
81
81
  ? (mode === 'light' ? 'text-violet-600 bg-violet-400/15 hover:bg-violet-400/25' : 'text-violet-400 bg-violet-400/15 hover:bg-violet-400/25')
82
- : 'text-[var(--editor-text)]/60 bg-transparent hover:text-[var(--editor-text)]'
82
+ : 'text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)] bg-transparent hover:text-[var(--editor-text)]'
83
83
  }`}
84
84
  >
85
85
  <Palette size={12} />
@@ -91,9 +91,9 @@ export default function ImageToneMenu({ value, onChange, variant = 'icon', mode
91
91
  role="menu"
92
92
  className={`${menuPosition} z-40 w-[300px] rounded-xl border border-[var(--editor-border)] bg-[var(--editor-surface)] shadow-2xl p-2 flex flex-col gap-1`}
93
93
  >
94
- <p className="px-2 pt-1 pb-1.5 text-[11px] font-semibold text-[var(--editor-text)]/80">
94
+ <p className="px-2 pt-1 pb-1.5 text-[11px] font-semibold text-[color-mix(in_srgb,var(--editor-text)_80%,transparent)]">
95
95
  Image color mapping
96
- <span className="block font-normal text-[10px] text-[var(--editor-text)]/50">
96
+ <span className="block font-normal text-[10px] text-[color-mix(in_srgb,var(--editor-text)_50%,transparent)]">
97
97
  How photos and logos are converted for the HDR render.
98
98
  </span>
99
99
  </p>
@@ -108,7 +108,7 @@ export default function ImageToneMenu({ value, onChange, variant = 'icon', mode
108
108
  className={`flex items-start gap-2.5 rounded-lg p-2 text-left transition-colors border ${
109
109
  active
110
110
  ? 'border-violet-400/60 bg-violet-400/10'
111
- : 'border-transparent hover:bg-[var(--editor-text)]/5'
111
+ : 'border-transparent hover:bg-[color-mix(in_srgb,var(--editor-text)_5%,transparent)]'
112
112
  }`}
113
113
  >
114
114
  <img
@@ -120,10 +120,10 @@ export default function ImageToneMenu({ value, onChange, variant = 'icon', mode
120
120
  <span className="text-xs font-semibold text-[var(--editor-text)] flex items-center gap-1.5">
121
121
  {tone.label}
122
122
  {tone.id === DEFAULT_IMAGE_TONE && (
123
- <span className="text-[9px] font-normal px-1 py-px rounded bg-[var(--editor-text)]/10 text-[var(--editor-text)]/55">default</span>
123
+ <span className="text-[9px] font-normal px-1 py-px rounded bg-[color-mix(in_srgb,var(--editor-text)_10%,transparent)] text-[color-mix(in_srgb,var(--editor-text)_55%,transparent)]">default</span>
124
124
  )}
125
125
  </span>
126
- <span className="text-[10px] leading-snug text-[var(--editor-text)]/60">
126
+ <span className="text-[10px] leading-snug text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)]">
127
127
  {tone.summary}
128
128
  </span>
129
129
  </span>
@@ -216,9 +216,9 @@ function normalizeDegrees(deg: number): number {
216
216
 
217
217
  // ── Shared chrome ────────────────────────────────────────────────────────
218
218
 
219
- const ROW_LABEL_CLASS = 'w-14 shrink-0 text-[11px] text-[var(--editor-text)]/55'
219
+ const ROW_LABEL_CLASS = 'w-14 shrink-0 text-[11px] text-[color-mix(in_srgb,var(--editor-text)_55%,transparent)]'
220
220
  const ICON_BUTTON_CLASS =
221
- 'flex h-5 w-5 shrink-0 items-center justify-center rounded text-[var(--editor-text)]/45 transition-colors hover:bg-[var(--editor-surface)] hover:text-[var(--editor-text)] disabled:pointer-events-none disabled:opacity-25'
221
+ 'flex h-5 w-5 shrink-0 items-center justify-center rounded text-[color-mix(in_srgb,var(--editor-text)_45%,transparent)] transition-colors hover:bg-[var(--editor-surface)] hover:text-[var(--editor-text)] disabled:pointer-events-none disabled:opacity-25'
222
222
 
223
223
  function IconButton({
224
224
  label,
@@ -282,10 +282,10 @@ function KeyframeNav({
282
282
  // invites a support question; one that is visibly unavailable and
283
283
  // says why reads as a limitation, which is what it is.
284
284
  diamondDisabled
285
- ? 'cursor-not-allowed text-[var(--editor-text)]/20'
285
+ ? 'cursor-not-allowed text-[color-mix(in_srgb,var(--editor-text)_20%,transparent)]'
286
286
  : pressed
287
287
  ? 'text-[var(--editor-accent)]'
288
- : 'text-[var(--editor-text)]/40 hover:text-[var(--editor-text)]/70',
288
+ : 'text-[color-mix(in_srgb,var(--editor-text)_40%,transparent)] hover:text-[color-mix(in_srgb,var(--editor-text)_70%,transparent)]',
289
289
  )}
290
290
  >
291
291
  <Diamond size={11} fill={pressed ? 'currentColor' : 'none'} />
@@ -518,7 +518,7 @@ export default function OverlayInspector({ item, clock, onPreview, onCommit, onC
518
518
  if (!canKeyframe(item)) {
519
519
  return (
520
520
  <div className={SECTION_CLASS}>
521
- <div className="px-3 py-6 text-center text-[11px] text-[var(--editor-text)]/45">
521
+ <div className="px-3 py-6 text-center text-[11px] text-[color-mix(in_srgb,var(--editor-text)_45%,transparent)]">
522
522
  Select an overlay to edit its properties.
523
523
  </div>
524
524
  </div>
@@ -769,7 +769,7 @@ export default function OverlayInspector({ item, clock, onPreview, onCommit, onC
769
769
  only hosts the ALL-properties actions (reset-all + keyframe-all); the
770
770
  muted "All" scopes them apart from each row's own per-property unit. */}
771
771
  <div className="shrink-0 flex items-center gap-1 border-b border-[var(--editor-border)] px-2 py-1.5">
772
- <span className="px-1 py-0.5 text-[10px] font-medium uppercase tracking-wide text-[var(--editor-text)]/40">
772
+ <span className="px-1 py-0.5 text-[10px] font-medium uppercase tracking-wide text-[color-mix(in_srgb,var(--editor-text)_40%,transparent)]">
773
773
  All
774
774
  </span>
775
775
  <div className="ml-auto flex items-center gap-1">
@@ -857,7 +857,7 @@ export default function OverlayInspector({ item, clock, onPreview, onCommit, onC
857
857
  (Space/Enter both fire click), so it needs no key handler of its
858
858
  own — only a visible focus ring. */}
859
859
  <div className="flex items-center gap-2">
860
- <span className="shrink-0 text-[11px] text-[var(--editor-text)]/55">Uniform scale</span>
860
+ <span className="shrink-0 text-[11px] text-[color-mix(in_srgb,var(--editor-text)_55%,transparent)]">Uniform scale</span>
861
861
  <button
862
862
  type="button"
863
863
  role="checkbox"
@@ -290,10 +290,10 @@ function PhaseStepper({ current, phases }: { current: RenderPhase; phases: Rende
290
290
  <span
291
291
  className={
292
292
  complete
293
- ? 'text-sm text-[var(--editor-text)]/70'
293
+ ? 'text-sm text-[color-mix(in_srgb,var(--editor-text)_70%,transparent)]'
294
294
  : active
295
295
  ? 'text-sm font-semibold text-[var(--editor-text)]'
296
- : 'text-sm text-[var(--editor-text)]/35'
296
+ : 'text-sm text-[color-mix(in_srgb,var(--editor-text)_35%,transparent)]'
297
297
  }
298
298
  >
299
299
  {phaseLabel(phase)}
@@ -309,15 +309,15 @@ function PhaseStepper({ current, phases }: { current: RenderPhase; phases: Rende
309
309
 
310
310
  function LogLine({ text }: { text: string }) {
311
311
  const t = text.replace(/^\[montaj render\]\s*/, '')
312
- let color = 'text-[var(--editor-text)]/60'
312
+ let color = 'text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)]'
313
313
  if (/ready|complete|done|encoded|assembled/i.test(t)) color = 'text-green-400'
314
314
  else if (/rendering|bundling|launching|browsers/i.test(t)) color = 'text-sky-400'
315
315
  else if (/trimming|building|composing/i.test(t)) color = 'text-amber-400'
316
- else if (/frame\s+\d+\/\d+/i.test(t)) color = 'text-[var(--editor-text)]/55'
316
+ else if (/frame\s+\d+\/\d+/i.test(t)) color = 'text-[color-mix(in_srgb,var(--editor-text)_55%,transparent)]'
317
317
  else if (/error|fail|warn/i.test(t)) color = 'text-red-400'
318
318
 
319
319
  const prefix = text.startsWith('[montaj render]')
320
- ? <span className="text-[var(--editor-text)]/40">[render] </span>
320
+ ? <span className="text-[color-mix(in_srgb,var(--editor-text)_40%,transparent)]">[render] </span>
321
321
  : null
322
322
 
323
323
  return (
@@ -385,7 +385,7 @@ function ProgressBar({ value }: { value: number | null }) {
385
385
  return (
386
386
  <div className="w-full max-w-xs" role="progressbar" aria-label="Render progress"
387
387
  aria-valuenow={pct === null ? undefined : Math.round(pct)} aria-valuemin={0} aria-valuemax={100}>
388
- <div className="h-1.5 w-full overflow-hidden rounded-full bg-[var(--editor-text)]/10">
388
+ <div className="h-1.5 w-full overflow-hidden rounded-full bg-[color-mix(in_srgb,var(--editor-text)_10%,transparent)]">
389
389
  {pct === null ? (
390
390
  <>
391
391
  <style>{'@keyframes montajRenderIndet{0%{transform:translateX(-100%)}100%{transform:translateX(320%)}}'}</style>
@@ -801,7 +801,7 @@ export default function RenderModal<P extends Project = Project>({ projectId, ad
801
801
  <button
802
802
  onClick={handleCancel}
803
803
  aria-label="Close"
804
- className="text-[var(--editor-text)]/55 hover:text-[var(--editor-text)] transition-colors text-lg leading-none"
804
+ className="text-[color-mix(in_srgb,var(--editor-text)_55%,transparent)] hover:text-[var(--editor-text)] transition-colors text-lg leading-none"
805
805
  >
806
806
  ×
807
807
  </button>
@@ -827,7 +827,7 @@ export default function RenderModal<P extends Project = Project>({ projectId, ad
827
827
  ) : coverLoading ? (
828
828
  <Loader size="md" label="Loading cover" />
829
829
  ) : (
830
- <span className="text-[11px] text-[var(--editor-text)]/40">No preview</span>
830
+ <span className="text-[11px] text-[color-mix(in_srgb,var(--editor-text)_40%,transparent)]">No preview</span>
831
831
  )}
832
832
  </div>
833
833
  {adapter.getSampleFrame && (
@@ -835,7 +835,7 @@ export default function RenderModal<P extends Project = Project>({ projectId, ad
835
835
  <button
836
836
  onClick={() => setCoverEditing(o => !o)}
837
837
  aria-expanded={coverEditing}
838
- className="self-start text-xs text-[var(--editor-text)]/70 hover:text-[var(--editor-text)] transition-colors"
838
+ className="self-start text-xs text-[color-mix(in_srgb,var(--editor-text)_70%,transparent)] hover:text-[var(--editor-text)] transition-colors"
839
839
  >
840
840
  {coverEditing ? 'Done' : 'Edit cover'}
841
841
  </button>
@@ -869,7 +869,7 @@ export default function RenderModal<P extends Project = Project>({ projectId, ad
869
869
  })}
870
870
  </div>
871
871
  ) : (
872
- <p className="text-[11px] text-[var(--editor-text)]/40">No frames to pick from.</p>
872
+ <p className="text-[11px] text-[color-mix(in_srgb,var(--editor-text)_40%,transparent)]">No frames to pick from.</p>
873
873
  )
874
874
  )}
875
875
  </>
@@ -880,7 +880,7 @@ export default function RenderModal<P extends Project = Project>({ projectId, ad
880
880
  <div className="flex-1 min-w-0 flex flex-col gap-4">
881
881
  {/* Name */}
882
882
  <label className="flex flex-col gap-1.5">
883
- <span className="text-[11px] font-semibold uppercase tracking-wide text-[var(--editor-text)]/50">Name</span>
883
+ <span className="text-[11px] font-semibold uppercase tracking-wide text-[color-mix(in_srgb,var(--editor-text)_50%,transparent)]">Name</span>
884
884
  <input
885
885
  type="text"
886
886
  value={name}
@@ -892,9 +892,9 @@ export default function RenderModal<P extends Project = Project>({ projectId, ad
892
892
 
893
893
  {/* Save to */}
894
894
  <div className="flex flex-col gap-1">
895
- <span className="text-[11px] font-semibold uppercase tracking-wide text-[var(--editor-text)]/50">Save to</span>
896
- <span className="text-xs font-mono text-[var(--editor-text)]/70 truncate">output/{outputName}.mp4</span>
897
- <span className="text-[11px] text-[var(--editor-text)]/45">Download after export.</span>
895
+ <span className="text-[11px] font-semibold uppercase tracking-wide text-[color-mix(in_srgb,var(--editor-text)_50%,transparent)]">Save to</span>
896
+ <span className="text-xs font-mono text-[color-mix(in_srgb,var(--editor-text)_70%,transparent)] truncate">output/{outputName}.mp4</span>
897
+ <span className="text-[11px] text-[color-mix(in_srgb,var(--editor-text)_45%,transparent)]">Download after export.</span>
898
898
  </div>
899
899
 
900
900
  {/* Resolution — source-capped tier picker. Only rendered when the
@@ -908,7 +908,7 @@ export default function RenderModal<P extends Project = Project>({ projectId, ad
908
908
  const currentShort = Math.min(...res.value)
909
909
  return (
910
910
  <div className="flex flex-col gap-2">
911
- <p className="text-[11px] font-semibold uppercase tracking-wide text-[var(--editor-text)]/50">Resolution</p>
911
+ <p className="text-[11px] font-semibold uppercase tracking-wide text-[color-mix(in_srgb,var(--editor-text)_50%,transparent)]">Resolution</p>
912
912
  <div role="radiogroup" aria-label="Resolution" className="grid grid-cols-2 sm:grid-cols-4 gap-2">
913
913
  {res.available.map(([w, h]) => {
914
914
  const short = Math.min(w, h)
@@ -922,11 +922,11 @@ export default function RenderModal<P extends Project = Project>({ projectId, ad
922
922
  className={`flex flex-col gap-1 rounded-lg border p-2.5 text-left transition-colors ${
923
923
  active
924
924
  ? 'border-violet-400/60 bg-violet-400/10'
925
- : 'border-[var(--editor-border)] hover:bg-[var(--editor-text)]/5'
925
+ : 'border-[var(--editor-border)] hover:bg-[color-mix(in_srgb,var(--editor-text)_5%,transparent)]'
926
926
  }`}
927
927
  >
928
928
  <span className="text-xs font-semibold text-[var(--editor-text)]">{resLabel([w, h])}</span>
929
- <span className="text-[10px] leading-snug text-[var(--editor-text)]/55">{w} × {h}</span>
929
+ <span className="text-[10px] leading-snug text-[color-mix(in_srgb,var(--editor-text)_55%,transparent)]">{w} × {h}</span>
930
930
  </button>
931
931
  )
932
932
  })}
@@ -940,7 +940,7 @@ export default function RenderModal<P extends Project = Project>({ projectId, ad
940
940
  const fps = preRenderOptions.fps
941
941
  return (
942
942
  <div className="flex flex-col gap-2">
943
- <p className="text-[11px] font-semibold uppercase tracking-wide text-[var(--editor-text)]/50">Frame rate</p>
943
+ <p className="text-[11px] font-semibold uppercase tracking-wide text-[color-mix(in_srgb,var(--editor-text)_50%,transparent)]">Frame rate</p>
944
944
  <div role="radiogroup" aria-label="Frame rate" className="grid grid-cols-3 gap-2">
945
945
  {fps.available.map(f => {
946
946
  const active = f === fps.value
@@ -954,11 +954,11 @@ export default function RenderModal<P extends Project = Project>({ projectId, ad
954
954
  className={`flex flex-col gap-1 rounded-lg border p-2.5 text-left transition-colors ${
955
955
  active
956
956
  ? 'border-violet-400/60 bg-violet-400/10'
957
- : 'border-[var(--editor-border)] hover:bg-[var(--editor-text)]/5'
957
+ : 'border-[var(--editor-border)] hover:bg-[color-mix(in_srgb,var(--editor-text)_5%,transparent)]'
958
958
  }`}
959
959
  >
960
960
  <span className="text-xs font-semibold text-[var(--editor-text)]">{f} fps</span>
961
- <span className="text-[10px] leading-snug text-[var(--editor-text)]/55">{hint}</span>
961
+ <span className="text-[10px] leading-snug text-[color-mix(in_srgb,var(--editor-text)_55%,transparent)]">{hint}</span>
962
962
  </button>
963
963
  )
964
964
  })}
@@ -972,7 +972,7 @@ export default function RenderModal<P extends Project = Project>({ projectId, ad
972
972
  <>
973
973
  {/* Format */}
974
974
  <div className="flex flex-col gap-2">
975
- <p className="text-[11px] font-semibold uppercase tracking-wide text-[var(--editor-text)]/50">Format</p>
975
+ <p className="text-[11px] font-semibold uppercase tracking-wide text-[color-mix(in_srgb,var(--editor-text)_50%,transparent)]">Format</p>
976
976
  <div role="radiogroup" aria-label="Format" className="grid grid-cols-3 gap-2">
977
977
  {EXPORT_CHOICES.map(choice => {
978
978
  const active = choice.id === exportChoice
@@ -985,11 +985,11 @@ export default function RenderModal<P extends Project = Project>({ projectId, ad
985
985
  className={`flex flex-col gap-1 rounded-lg border p-2.5 text-left transition-colors ${
986
986
  active
987
987
  ? 'border-violet-400/60 bg-violet-400/10'
988
- : 'border-[var(--editor-border)] hover:bg-[var(--editor-text)]/5'
988
+ : 'border-[var(--editor-border)] hover:bg-[color-mix(in_srgb,var(--editor-text)_5%,transparent)]'
989
989
  }`}
990
990
  >
991
991
  <span className="text-xs font-semibold text-[var(--editor-text)]">{choice.label}</span>
992
- <span className="text-[10px] leading-snug text-[var(--editor-text)]/55">{choice.blurb}</span>
992
+ <span className="text-[10px] leading-snug text-[color-mix(in_srgb,var(--editor-text)_55%,transparent)]">{choice.blurb}</span>
993
993
  </button>
994
994
  )
995
995
  })}
@@ -1010,7 +1010,7 @@ export default function RenderModal<P extends Project = Project>({ projectId, ad
1010
1010
  <button
1011
1011
  onClick={() => setImageColor(o => !o)}
1012
1012
  aria-expanded={imageColorOpen}
1013
- className="flex items-center gap-1.5 self-start text-xs text-[var(--editor-text)]/70 hover:text-[var(--editor-text)] transition-colors"
1013
+ className="flex items-center gap-1.5 self-start text-xs text-[color-mix(in_srgb,var(--editor-text)_70%,transparent)] hover:text-[var(--editor-text)] transition-colors"
1014
1014
  >
1015
1015
  {imageColorOpen ? <ChevronDown size={13} /> : <ChevronRight size={13} />}
1016
1016
  Image color
@@ -1018,7 +1018,7 @@ export default function RenderModal<P extends Project = Project>({ projectId, ad
1018
1018
 
1019
1019
  {imageColorOpen ? (
1020
1020
  <div className="flex flex-col gap-2">
1021
- <p className="text-[11px] font-semibold uppercase tracking-wide text-[var(--editor-text)]/50">
1021
+ <p className="text-[11px] font-semibold uppercase tracking-wide text-[color-mix(in_srgb,var(--editor-text)_50%,transparent)]">
1022
1022
  How photos and logos convert for HDR
1023
1023
  </p>
1024
1024
  <div role="radiogroup" aria-label="Image color" className="grid grid-cols-2 gap-2">
@@ -1033,7 +1033,7 @@ export default function RenderModal<P extends Project = Project>({ projectId, ad
1033
1033
  className={`flex flex-col gap-1.5 rounded-lg border p-2 text-left transition-colors ${
1034
1034
  active
1035
1035
  ? 'border-violet-400/60 bg-violet-400/10'
1036
- : 'border-[var(--editor-border)] hover:bg-[var(--editor-text)]/5'
1036
+ : 'border-[var(--editor-border)] hover:bg-[color-mix(in_srgb,var(--editor-text)_5%,transparent)]'
1037
1037
  }`}
1038
1038
  >
1039
1039
  <img
@@ -1044,17 +1044,17 @@ export default function RenderModal<P extends Project = Project>({ projectId, ad
1044
1044
  <span className="text-xs font-semibold text-[var(--editor-text)] flex items-center gap-1.5">
1045
1045
  {t.label}
1046
1046
  {t.id === DEFAULT_IMAGE_TONE && (
1047
- <span className="text-[9px] font-normal px-1 py-px rounded bg-[var(--editor-text)]/10 text-[var(--editor-text)]/55">default</span>
1047
+ <span className="text-[9px] font-normal px-1 py-px rounded bg-[color-mix(in_srgb,var(--editor-text)_10%,transparent)] text-[color-mix(in_srgb,var(--editor-text)_55%,transparent)]">default</span>
1048
1048
  )}
1049
1049
  </span>
1050
- <span className="text-[10px] leading-snug text-[var(--editor-text)]/60">{t.summary}</span>
1050
+ <span className="text-[10px] leading-snug text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)]">{t.summary}</span>
1051
1051
  </button>
1052
1052
  )
1053
1053
  })}
1054
1054
  </div>
1055
1055
  </div>
1056
1056
  ) : (
1057
- <p className="text-[11px] text-[var(--editor-text)]/45">
1057
+ <p className="text-[11px] text-[color-mix(in_srgb,var(--editor-text)_45%,transparent)]">
1058
1058
  Photos and logos use the {toneInfo.label} conversion. Open to change.
1059
1059
  </p>
1060
1060
  )}
@@ -1067,7 +1067,7 @@ export default function RenderModal<P extends Project = Project>({ projectId, ad
1067
1067
  <button
1068
1068
  onClick={() => setAdvanced(o => !o)}
1069
1069
  aria-expanded={advancedOpen}
1070
- className="flex items-center gap-1.5 self-start text-xs text-[var(--editor-text)]/70 hover:text-[var(--editor-text)] transition-colors"
1070
+ className="flex items-center gap-1.5 self-start text-xs text-[color-mix(in_srgb,var(--editor-text)_70%,transparent)] hover:text-[var(--editor-text)] transition-colors"
1071
1071
  >
1072
1072
  {advancedOpen ? <ChevronDown size={13} /> : <ChevronRight size={13} />}
1073
1073
  Advanced
@@ -1075,7 +1075,7 @@ export default function RenderModal<P extends Project = Project>({ projectId, ad
1075
1075
 
1076
1076
  {advancedOpen ? (
1077
1077
  <div className="flex flex-col gap-2">
1078
- <p className="text-[11px] font-semibold uppercase tracking-wide text-[var(--editor-text)]/50">
1078
+ <p className="text-[11px] font-semibold uppercase tracking-wide text-[color-mix(in_srgb,var(--editor-text)_50%,transparent)]">
1079
1079
  Tone curve for SDR
1080
1080
  </p>
1081
1081
  <div role="radiogroup" aria-label="Tone curve for SDR" className="grid grid-cols-2 gap-2">
@@ -1091,7 +1091,7 @@ export default function RenderModal<P extends Project = Project>({ projectId, ad
1091
1091
  className={`flex flex-col gap-1.5 rounded-lg border p-2 text-left transition-colors ${
1092
1092
  active
1093
1093
  ? 'border-violet-400/60 bg-violet-400/10'
1094
- : 'border-[var(--editor-border)] hover:bg-[var(--editor-text)]/5'
1094
+ : 'border-[var(--editor-border)] hover:bg-[color-mix(in_srgb,var(--editor-text)_5%,transparent)]'
1095
1095
  }`}
1096
1096
  >
1097
1097
  {thumb ? (
@@ -1120,17 +1120,17 @@ export default function RenderModal<P extends Project = Project>({ projectId, ad
1120
1120
  </span>
1121
1121
  </span>
1122
1122
  ) : thumbsPending ? (
1123
- <span style={{ aspectRatio: coverAspect }} className="w-full rounded border border-[var(--editor-border)] bg-[var(--editor-text)]/5 flex items-center justify-center">
1123
+ <span style={{ aspectRatio: coverAspect }} className="w-full rounded border border-[var(--editor-border)] bg-[color-mix(in_srgb,var(--editor-text)_5%,transparent)] flex items-center justify-center">
1124
1124
  <Loader size="sm" />
1125
1125
  </span>
1126
1126
  ) : null}
1127
1127
  <span className="text-xs font-semibold text-[var(--editor-text)] flex items-center gap-1.5">
1128
1128
  {curve.label}
1129
1129
  {curve.id === DEFAULT_SDR_CURVE && (
1130
- <span className="text-[9px] font-normal px-1 py-px rounded bg-[var(--editor-text)]/10 text-[var(--editor-text)]/55">default</span>
1130
+ <span className="text-[9px] font-normal px-1 py-px rounded bg-[color-mix(in_srgb,var(--editor-text)_10%,transparent)] text-[color-mix(in_srgb,var(--editor-text)_55%,transparent)]">default</span>
1131
1131
  )}
1132
1132
  </span>
1133
- <span className="text-[10px] leading-snug text-[var(--editor-text)]/60">{curve.blurb}</span>
1133
+ <span className="text-[10px] leading-snug text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)]">{curve.blurb}</span>
1134
1134
  </button>
1135
1135
  )
1136
1136
  })}
@@ -1139,7 +1139,7 @@ export default function RenderModal<P extends Project = Project>({ projectId, ad
1139
1139
  data-testid="sdr-honesty-line"
1140
1140
  className={`text-[11px] leading-snug ${
1141
1141
  sdrCurve === DEFAULT_SDR_CURVE
1142
- ? 'text-[var(--editor-text)]/55'
1142
+ ? 'text-[color-mix(in_srgb,var(--editor-text)_55%,transparent)]'
1143
1143
  : (mode === 'light' ? 'text-amber-700' : 'text-amber-400/90')
1144
1144
  }`}
1145
1145
  >
@@ -1147,7 +1147,7 @@ export default function RenderModal<P extends Project = Project>({ projectId, ad
1147
1147
  </p>
1148
1148
  </div>
1149
1149
  ) : (
1150
- <p className="text-[11px] text-[var(--editor-text)]/45">
1150
+ <p className="text-[11px] text-[color-mix(in_srgb,var(--editor-text)_45%,transparent)]">
1151
1151
  SDR files use the {curveInfo.label} look. Open Advanced to compare curves.
1152
1152
  </p>
1153
1153
  )}
@@ -1159,13 +1159,13 @@ export default function RenderModal<P extends Project = Project>({ projectId, ad
1159
1159
 
1160
1160
  {/* Footer — duration on the left, actions on the right. */}
1161
1161
  <div className="flex items-center justify-between gap-2 px-5 py-3 border-t border-[var(--editor-border)]">
1162
- <span className="text-xs text-[var(--editor-text)]/50 tabular-nums">
1162
+ <span className="text-xs text-[color-mix(in_srgb,var(--editor-text)_50%,transparent)] tabular-nums">
1163
1163
  {durationSec > 0 ? formatTimecode(durationSec) : ''}
1164
1164
  </span>
1165
1165
  <div className="flex items-center gap-2">
1166
1166
  <button
1167
1167
  onClick={handleCancel}
1168
- className="text-sm px-4 py-1.5 rounded-md bg-[var(--editor-surface)] border border-[var(--editor-border)] text-[var(--editor-text)]/80 hover:opacity-90 transition-colors"
1168
+ className="text-sm px-4 py-1.5 rounded-md bg-[var(--editor-surface)] border border-[var(--editor-border)] text-[color-mix(in_srgb,var(--editor-text)_80%,transparent)] hover:opacity-90 transition-colors"
1169
1169
  >
1170
1170
  Cancel
1171
1171
  </button>
@@ -1257,7 +1257,7 @@ export default function RenderModal<P extends Project = Project>({ projectId, ad
1257
1257
  className="h-full w-full object-contain"
1258
1258
  />
1259
1259
  ) : (
1260
- <p className="text-sm text-[var(--editor-text)]/60">Render complete.</p>
1260
+ <p className="text-sm text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)]">Render complete.</p>
1261
1261
  )}
1262
1262
  </div>
1263
1263
 
@@ -1268,10 +1268,10 @@ export default function RenderModal<P extends Project = Project>({ projectId, ad
1268
1268
  <span className="w-2 h-2 rounded-full bg-green-400" />
1269
1269
  <div>
1270
1270
  <p className="text-sm font-semibold text-[var(--editor-text)]">Render complete</p>
1271
- <p className="text-xs text-[var(--editor-text)]/60">Saved to your library.</p>
1271
+ <p className="text-xs text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)]">Saved to your library.</p>
1272
1272
  </div>
1273
1273
  </div>
1274
- <button onClick={onClose} className="text-[var(--editor-text)]/55 hover:text-[var(--editor-text)] transition-colors text-lg leading-none">×</button>
1274
+ <button onClick={onClose} className="text-[color-mix(in_srgb,var(--editor-text)_55%,transparent)] hover:text-[var(--editor-text)] transition-colors text-lg leading-none">×</button>
1275
1275
  </div>
1276
1276
 
1277
1277
  <div className="flex flex-col gap-3 p-5 flex-1">
@@ -1299,7 +1299,7 @@ export default function RenderModal<P extends Project = Project>({ projectId, ad
1299
1299
  ))}
1300
1300
  <button
1301
1301
  onClick={onClose}
1302
- className="w-full text-center text-sm px-4 py-2.5 rounded-lg bg-[var(--editor-surface)] border border-[var(--editor-border)] text-[var(--editor-text)]/80 hover:opacity-90 transition-colors"
1302
+ className="w-full text-center text-sm px-4 py-2.5 rounded-lg bg-[var(--editor-surface)] border border-[var(--editor-border)] text-[color-mix(in_srgb,var(--editor-text)_80%,transparent)] hover:opacity-90 transition-colors"
1303
1303
  >
1304
1304
  Close
1305
1305
  </button>
@@ -1325,7 +1325,7 @@ export default function RenderModal<P extends Project = Project>({ projectId, ad
1325
1325
  </h2>
1326
1326
  </div>
1327
1327
  {status !== 'running' && (
1328
- <button onClick={onClose} className="text-[var(--editor-text)]/55 hover:text-[var(--editor-text)] transition-colors text-lg leading-none">×</button>
1328
+ <button onClick={onClose} className="text-[color-mix(in_srgb,var(--editor-text)_55%,transparent)] hover:text-[var(--editor-text)] transition-colors text-lg leading-none">×</button>
1329
1329
  )}
1330
1330
  </div>
1331
1331
 
@@ -1336,7 +1336,7 @@ export default function RenderModal<P extends Project = Project>({ projectId, ad
1336
1336
 
1337
1337
  <div className="flex flex-col items-center gap-1 text-center">
1338
1338
  <h3 className="text-base font-semibold text-[var(--editor-text)]">Exporting your video</h3>
1339
- <p className="text-xs text-[var(--editor-text)]/50">This can take a few minutes for longer videos.</p>
1339
+ <p className="text-xs text-[color-mix(in_srgb,var(--editor-text)_50%,transparent)]">This can take a few minutes for longer videos.</p>
1340
1340
  </div>
1341
1341
 
1342
1342
  {/* Progress bar — always visible while rendering, above the stepper
@@ -1366,7 +1366,7 @@ export default function RenderModal<P extends Project = Project>({ projectId, ad
1366
1366
  <button
1367
1367
  onClick={() => setShowLog(o => !o)}
1368
1368
  aria-expanded={showLog}
1369
- className="mx-auto flex items-center gap-1 text-[11px] text-[var(--editor-text)]/50 hover:text-[var(--editor-text)]/80 transition-colors"
1369
+ className="mx-auto flex items-center gap-1 text-[11px] text-[color-mix(in_srgb,var(--editor-text)_50%,transparent)] hover:text-[color-mix(in_srgb,var(--editor-text)_80%,transparent)] transition-colors"
1370
1370
  >
1371
1371
  {showLog ? <ChevronDown size={13} /> : <ChevronRight size={13} />}
1372
1372
  {showLog ? 'Hide render log' : 'Show render log'}
@@ -1375,17 +1375,17 @@ export default function RenderModal<P extends Project = Project>({ projectId, ad
1375
1375
  <div className="relative mt-2">
1376
1376
  <button
1377
1377
  onClick={() => navigator.clipboard.writeText(logs.join('\n'))}
1378
- className="absolute top-2 right-2 z-10 rounded border border-white/10 bg-[#0a0e17] px-2 py-0.5 text-[10px] text-[var(--editor-text)]/60 hover:text-[var(--editor-text)] transition-colors"
1378
+ className="absolute top-2 right-2 z-10 rounded border border-white/10 bg-[#0a0e17] px-2 py-0.5 text-[10px] text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)] hover:text-[var(--editor-text)] transition-colors"
1379
1379
  title="Copy logs"
1380
1380
  >
1381
1381
  Copy
1382
1382
  </button>
1383
1383
  <div
1384
1384
  ref={logRef}
1385
- className="flex h-52 flex-col gap-0.5 overflow-y-auto rounded-lg border border-white/10 bg-[#0a0e17] px-3 py-2 font-mono text-[11px] text-[var(--editor-text)]/80"
1385
+ className="flex h-52 flex-col gap-0.5 overflow-y-auto rounded-lg border border-white/10 bg-[#0a0e17] px-3 py-2 font-mono text-[11px] text-[color-mix(in_srgb,var(--editor-text)_80%,transparent)]"
1386
1386
  >
1387
1387
  {logs.length === 0
1388
- ? <span className="italic text-[var(--editor-text)]/40">Starting render engine…</span>
1388
+ ? <span className="italic text-[color-mix(in_srgb,var(--editor-text)_40%,transparent)]">Starting render engine…</span>
1389
1389
  : logs.map((line, i) => <LogLine key={i} text={line} />)}
1390
1390
  </div>
1391
1391
  </div>
@@ -1400,14 +1400,14 @@ export default function RenderModal<P extends Project = Project>({ projectId, ad
1400
1400
  <div className="relative">
1401
1401
  <button
1402
1402
  onClick={() => navigator.clipboard.writeText(logs.join('\n') + (errorMsg ? '\n' + errorMsg : ''))}
1403
- className="absolute top-2 right-2 z-10 rounded border border-white/10 bg-[#0a0e17] px-2 py-0.5 text-[10px] text-[var(--editor-text)]/60 hover:text-[var(--editor-text)] transition-colors"
1403
+ className="absolute top-2 right-2 z-10 rounded border border-white/10 bg-[#0a0e17] px-2 py-0.5 text-[10px] text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)] hover:text-[var(--editor-text)] transition-colors"
1404
1404
  title="Copy logs"
1405
1405
  >
1406
1406
  Copy
1407
1407
  </button>
1408
1408
  <div
1409
1409
  ref={logRef}
1410
- className="flex h-72 flex-col gap-0.5 overflow-y-auto rounded-lg border border-white/10 bg-[#0a0e17] px-3 py-2 font-mono text-[11px] text-[var(--editor-text)]/80"
1410
+ className="flex h-72 flex-col gap-0.5 overflow-y-auto rounded-lg border border-white/10 bg-[#0a0e17] px-3 py-2 font-mono text-[11px] text-[color-mix(in_srgb,var(--editor-text)_80%,transparent)]"
1411
1411
  >
1412
1412
  {logs.map((line, i) => <LogLine key={i} text={line} />)}
1413
1413
  {errorMsg && <span className="mt-1 text-red-400">{errorMsg}</span>}
@@ -1426,7 +1426,7 @@ export default function RenderModal<P extends Project = Project>({ projectId, ad
1426
1426
  {status === 'running' ? (
1427
1427
  <button
1428
1428
  onClick={handleCancel}
1429
- className={`text-sm px-4 py-1.5 rounded-md bg-[#0e131f] border border-white/10 text-[var(--editor-text)]/80 transition-colors ${mode === 'light' ? 'hover:bg-red-50 hover:border-red-300 hover:text-red-700' : 'hover:bg-red-900/40 hover:border-red-700 hover:text-red-300'}`}
1429
+ className={`text-sm px-4 py-1.5 rounded-md bg-[#0e131f] border border-white/10 text-[color-mix(in_srgb,var(--editor-text)_80%,transparent)] transition-colors ${mode === 'light' ? 'hover:bg-red-50 hover:border-red-300 hover:text-red-700' : 'hover:bg-red-900/40 hover:border-red-700 hover:text-red-300'}`}
1430
1430
  >
1431
1431
  Cancel
1432
1432
  </button>
@@ -86,7 +86,7 @@ function VersionFramePane({
86
86
 
87
87
  return (
88
88
  <div className="flex-1 min-w-0 flex flex-col gap-1.5">
89
- <span className="text-[11px] font-medium text-[var(--editor-text)]/60 truncate" title={label}>
89
+ <span className="text-[11px] font-medium text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)] truncate" title={label}>
90
90
  {label}
91
91
  </span>
92
92
  <div className="relative aspect-video w-full overflow-hidden rounded-lg border border-[var(--editor-border)] bg-[var(--editor-bg)] flex items-center justify-center">
@@ -101,7 +101,7 @@ function VersionFramePane({
101
101
  style={error ? { display: 'none' } : undefined}
102
102
  />
103
103
  {loading && !error && (
104
- <span className="absolute inset-0 flex items-center justify-center text-xs text-[var(--editor-text)]/50">
104
+ <span className="absolute inset-0 flex items-center justify-center text-xs text-[color-mix(in_srgb,var(--editor-text)_50%,transparent)]">
105
105
  Loading…
106
106
  </span>
107
107
  )}
@@ -178,7 +178,7 @@ export default function VersionCompare({
178
178
  <button
179
179
  onClick={onClose}
180
180
  aria-label="Close"
181
- className="text-[var(--editor-text)]/55 hover:text-[var(--editor-text)] transition-colors text-lg leading-none"
181
+ className="text-[color-mix(in_srgb,var(--editor-text)_55%,transparent)] hover:text-[var(--editor-text)] transition-colors text-lg leading-none"
182
182
  >
183
183
  ×
184
184
  </button>
@@ -190,7 +190,7 @@ export default function VersionCompare({
190
190
  {/* Picker row */}
191
191
  <div className="flex flex-col sm:flex-row gap-3">
192
192
  <label className="flex-1 min-w-0 flex flex-col gap-1.5">
193
- <span className="text-[11px] font-semibold uppercase tracking-wide text-[var(--editor-text)]/50">Left</span>
193
+ <span className="text-[11px] font-semibold uppercase tracking-wide text-[color-mix(in_srgb,var(--editor-text)_50%,transparent)]">Left</span>
194
194
  <select
195
195
  value={leftHash}
196
196
  onChange={(e) => setLeftHash(e.target.value)}
@@ -202,7 +202,7 @@ export default function VersionCompare({
202
202
  </select>
203
203
  </label>
204
204
  <label className="flex-1 min-w-0 flex flex-col gap-1.5">
205
- <span className="text-[11px] font-semibold uppercase tracking-wide text-[var(--editor-text)]/50">Right</span>
205
+ <span className="text-[11px] font-semibold uppercase tracking-wide text-[color-mix(in_srgb,var(--editor-text)_50%,transparent)]">Right</span>
206
206
  <select
207
207
  value={rightHash}
208
208
  onChange={(e) => setRightHash(e.target.value)}
@@ -238,8 +238,8 @@ export default function VersionCompare({
238
238
  {/* Time-scrub slider */}
239
239
  <div className="flex flex-col gap-1.5">
240
240
  <div className="flex items-center justify-between">
241
- <span className="text-[11px] font-semibold uppercase tracking-wide text-[var(--editor-text)]/50">Time</span>
242
- <span className="text-xs text-[var(--editor-text)]/60 tabular-nums">{formatT(t)}</span>
241
+ <span className="text-[11px] font-semibold uppercase tracking-wide text-[color-mix(in_srgb,var(--editor-text)_50%,transparent)]">Time</span>
242
+ <span className="text-xs text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)] tabular-nums">{formatT(t)}</span>
243
243
  </div>
244
244
  <Slider
245
245
  min={0}
@@ -87,7 +87,7 @@ export default function VersionPanel({ versions, restoring, onRestore, onSaveVer
87
87
  IS the list. The count sits on the right as an explicit "N versions"
88
88
  label so it reads as a quantity rather than part of the title. */}
89
89
  <div className="shrink-0 flex items-center justify-between px-3 py-2 border-b border-[var(--editor-border)]">
90
- <span className="text-xs font-medium text-[var(--editor-text)]/60 uppercase tracking-wide">Versions</span>
90
+ <span className="text-xs font-medium text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)] uppercase tracking-wide">Versions</span>
91
91
  {rows.length > 0 && (
92
92
  <span className="text-[10px] text-[var(--editor-text)] opacity-50">
93
93
  {rows.length} {rows.length === 1 ? 'version' : 'versions'}
@@ -106,7 +106,7 @@ export default function VersionPanel({ versions, restoring, onRestore, onSaveVer
106
106
  onChange={e => setNameInput(e.target.value)}
107
107
  placeholder="Name (optional)"
108
108
  disabled={saving || !onSaveVersion}
109
- className="min-w-0 flex-1 h-8 text-[11px] bg-[var(--editor-bg)] border border-[var(--editor-border)] rounded-md px-2 text-[var(--editor-text)] placeholder:text-[var(--editor-text)]/40 focus:outline-none focus:border-[var(--editor-accent)] disabled:opacity-40"
109
+ className="min-w-0 flex-1 h-8 text-[11px] bg-[var(--editor-bg)] border border-[var(--editor-border)] rounded-md px-2 text-[var(--editor-text)] placeholder:text-[color-mix(in_srgb,var(--editor-text)_40%,transparent)] focus:outline-none focus:border-[var(--editor-accent)] disabled:opacity-40"
110
110
  />
111
111
  <button
112
112
  onClick={handleSaveClick}