@hyperframes/studio 0.8.103 → 0.8.105

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 (200) hide show
  1. package/dist/assets/{hyperframes-player-DG795Hwu.js → hyperframes-player-tnfCiVdR.js} +1 -1
  2. package/dist/assets/{index-BiEtDkae.js → index-Ca6LZlVb.js} +1 -1
  3. package/dist/assets/index-Cre9X4RS.css +1 -0
  4. package/dist/assets/{index-BPJRf2X9.js → index-DEDhPmPD.js} +215 -215
  5. package/dist/assets/{index-DuCZ3pQw.js → index-zpe332hb.js} +1 -1
  6. package/dist/{chunk-62DDWOSH.js → chunk-EEXSYY5F.js} +19 -18
  7. package/dist/chunk-EEXSYY5F.js.map +1 -0
  8. package/dist/{chunk-6GAO75IU.js → chunk-NCUBAVGK.js} +10 -8
  9. package/dist/chunk-NCUBAVGK.js.map +1 -0
  10. package/dist/{chunk-HIGHVWYD.js → chunk-QADHJFDF.js} +2 -2
  11. package/dist/index.css +6 -1
  12. package/dist/index.css.map +1 -1
  13. package/dist/index.d.ts +181 -168
  14. package/dist/index.html +2 -2
  15. package/dist/index.js +1018 -919
  16. package/dist/index.js.map +1 -1
  17. package/dist/player.js +2 -2
  18. package/dist/styles/tailwind-preset.js.map +1 -1
  19. package/dist/styles.css +1 -1
  20. package/dist/ui.js +2 -2
  21. package/package.json +8 -8
  22. package/src/captions/components/CaptionAnimationPanel.tsx +2 -2
  23. package/src/captions/components/CaptionPropertyPanel.tsx +2 -2
  24. package/src/captions/components/CaptionTimeline.tsx +3 -3
  25. package/src/components/AskAgentModal.tsx +1 -1
  26. package/src/components/LintModal.tsx +35 -35
  27. package/src/components/StudioErrorBoundary.tsx +1 -1
  28. package/src/components/StudioHeader.dom.test.tsx +4 -4
  29. package/src/components/StudioHeader.tsx +2 -1
  30. package/src/components/StudioToast.tsx +7 -16
  31. package/src/components/TimelineToolPicker.tsx +1 -1
  32. package/src/components/TimelineToolbar.tsx +11 -11
  33. package/src/components/dock/Dock.tsx +1 -1
  34. package/src/components/dock/dock.css +9 -1
  35. package/src/components/editor/AnimationCard.tsx +5 -5
  36. package/src/components/editor/AnimationCardParts.tsx +1 -1
  37. package/src/components/editor/BlockParamsPanel.tsx +3 -3
  38. package/src/components/editor/BorderRadiusEditor.tsx +8 -15
  39. package/src/components/editor/CanvasContextMenu.tsx +1 -1
  40. package/src/components/editor/ComputedTweenNotice.tsx +1 -1
  41. package/src/components/editor/DomEditOverlay.tsx +5 -6
  42. package/src/components/editor/DomEditRotateHandle.tsx +1 -1
  43. package/src/components/editor/EaseCurveSection.tsx +14 -19
  44. package/src/components/editor/EaseModeControls.tsx +2 -2
  45. package/src/components/editor/EaseParamFields.tsx +8 -8
  46. package/src/components/editor/FileTree.tsx +1 -1
  47. package/src/components/editor/FileTreeIcons.tsx +47 -26
  48. package/src/components/editor/FileTreeNodes.tsx +11 -8
  49. package/src/components/editor/GestureRecordControl.tsx +3 -3
  50. package/src/components/editor/GsapAddAnimationControl.tsx +1 -1
  51. package/src/components/editor/GsapAnimationSection.tsx +2 -2
  52. package/src/components/editor/InlineTextToolbar.tsx +4 -4
  53. package/src/components/editor/InspectorHeaderActions.tsx +1 -1
  54. package/src/components/editor/KeyframeDiamond.tsx +6 -4
  55. package/src/components/editor/KeyframeNavigation.tsx +4 -4
  56. package/src/components/editor/LayersPanel.tsx +3 -3
  57. package/src/components/editor/PromotableControl.tsx +2 -2
  58. package/src/components/editor/PropertyPanel.tsx +3 -3
  59. package/src/components/editor/PropertyPanelEmptyState.tsx +4 -4
  60. package/src/components/editor/PropertyPanelFlatFooter.tsx +2 -2
  61. package/src/components/editor/PropertyPanelFlatHeader.test.tsx +1 -1
  62. package/src/components/editor/PropertyPanelFlatHeader.tsx +2 -2
  63. package/src/components/editor/SnapToolbar.tsx +5 -5
  64. package/src/components/editor/TimelineFxPopover.tsx +4 -4
  65. package/src/components/editor/Transform3DCube.tsx +1 -1
  66. package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +46 -6
  67. package/src/components/editor/anchoredResizeReleaseShift.test.ts +0 -84
  68. package/src/components/editor/domEditOverlayGestures.ts +13 -19
  69. package/src/components/editor/domEditOverlayStartGesture.test.ts +170 -13
  70. package/src/components/editor/domEditOverlayStartGesture.ts +15 -25
  71. package/src/components/editor/easeCurveSvg.tsx +1 -1
  72. package/src/components/editor/manualEdits.test.ts +3 -9
  73. package/src/components/editor/manualEdits.ts +0 -1
  74. package/src/components/editor/manualEditsDom.ts +3 -13
  75. package/src/components/editor/manualEditsDomPatches.test.ts +3 -131
  76. package/src/components/editor/manualEditsDomPatches.ts +1 -53
  77. package/src/components/editor/manualEditsSnapshot.ts +3 -0
  78. package/src/components/editor/manualEditsTypes.ts +2 -0
  79. package/src/components/editor/manualOffsetDrag.test.ts +41 -0
  80. package/src/components/editor/manualOffsetDrag.ts +6 -27
  81. package/src/components/editor/persistSeam.integration.test.ts +0 -18
  82. package/src/components/editor/plainTranslate.ts +42 -6
  83. package/src/components/editor/propertyPanelColorCurveGraph.tsx +2 -2
  84. package/src/components/editor/propertyPanelColorGradingControls.tsx +1 -1
  85. package/src/components/editor/propertyPanelColorGradingSection.tsx +8 -8
  86. package/src/components/editor/propertyPanelColorGradingSlider.tsx +1 -1
  87. package/src/components/editor/propertyPanelColorSecondary.tsx +4 -4
  88. package/src/components/editor/propertyPanelFill.tsx +6 -6
  89. package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +3 -3
  90. package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +1 -1
  91. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +6 -9
  92. package/src/components/editor/propertyPanelFlatEffectsSection.tsx +4 -4
  93. package/src/components/editor/propertyPanelFlatMediaSection.tsx +2 -2
  94. package/src/components/editor/propertyPanelFlatMotionSection.tsx +2 -2
  95. package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +2 -2
  96. package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +4 -3
  97. package/src/components/editor/propertyPanelFlatPrimitives.tsx +1 -1
  98. package/src/components/editor/propertyPanelFlatSliderReadout.tsx +1 -1
  99. package/src/components/editor/propertyPanelFont.tsx +2 -2
  100. package/src/components/editor/propertyPanelFxCarveModule.tsx +1 -1
  101. package/src/components/editor/propertyPanelFxControls.tsx +2 -2
  102. package/src/components/editor/propertyPanelFxEqModule.tsx +2 -2
  103. package/src/components/editor/propertyPanelFxNodeRow.tsx +1 -1
  104. package/src/components/editor/propertyPanelFxPresetRun.tsx +1 -1
  105. package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
  106. package/src/components/editor/propertyPanelPrimitives.tsx +1 -1
  107. package/src/components/editor/propertyPanelSections.tsx +2 -2
  108. package/src/components/editor/propertyPanelTransformCommit.test.ts +29 -0
  109. package/src/components/editor/propertyPanelTransformCommit.ts +8 -5
  110. package/src/components/editor/propertyPanelTypes.ts +1 -0
  111. package/src/components/editor/propertyPanelValueTier.test.ts +1 -1
  112. package/src/components/editor/propertyPanelValueTier.ts +1 -1
  113. package/src/components/editor/rotateInlineSave.test.ts +111 -0
  114. package/src/components/editor/rotationDraft.test.ts +111 -0
  115. package/src/components/editor/rotationDraft.ts +145 -0
  116. package/src/components/editor/useDomEditNudge.test.tsx +33 -0
  117. package/src/components/editor/useDomEditNudge.ts +6 -2
  118. package/src/components/editor/useDomEditOverlayGestures.ts +22 -24
  119. package/src/components/feedback/StudioFeedbackCard.tsx +11 -23
  120. package/src/components/nle/AssetPreviewOverlay.tsx +1 -1
  121. package/src/components/nle/AudioMeterStrip.tsx +2 -2
  122. package/src/components/nle/NLEPreview.test.ts +8 -4
  123. package/src/components/nle/PreviewOverlays.tsx +1 -1
  124. package/src/components/nle/PreviewPane.tsx +3 -3
  125. package/src/components/nle/PreviewPoster.tsx +29 -1
  126. package/src/components/nle/PreviewZoomOverlay.tsx +1 -1
  127. package/src/components/panels/SlideshowPanel.tsx +4 -4
  128. package/src/components/panels/SlideshowSubPanels.tsx +19 -21
  129. package/src/components/panels/VariablesBindElement.tsx +1 -1
  130. package/src/components/panels/VariablesDeclarationForm.tsx +1 -1
  131. package/src/components/panels/VariablesPanel.tsx +4 -6
  132. package/src/components/panels/VariablesRowAction.tsx +1 -1
  133. package/src/components/renders/FfmpegRequiredNotice.tsx +4 -4
  134. package/src/components/renders/RenderQueue.tsx +2 -2
  135. package/src/components/renders/RenderQueueItem.tsx +3 -3
  136. package/src/components/sidebar/AssetCard.tsx +3 -3
  137. package/src/components/sidebar/AssetContextMenu.tsx +7 -7
  138. package/src/components/sidebar/AssetsTab.tsx +6 -6
  139. package/src/components/sidebar/AudioRow.tsx +5 -3
  140. package/src/components/sidebar/BlocksTab.tsx +6 -6
  141. package/src/components/sidebar/CompositionsTab.drag.test.tsx +3 -69
  142. package/src/components/sidebar/CompositionsTab.rootBadge.test.tsx +4 -50
  143. package/src/components/sidebar/CompositionsTab.thumbnails.test.tsx +256 -0
  144. package/src/components/sidebar/CompositionsTab.tsx +65 -12
  145. package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
  146. package/src/components/sidebar/SidebarLintButton.tsx +3 -3
  147. package/src/components/sidebar/compositionsTabTestUtils.tsx +39 -0
  148. package/src/components/timelineToolbarStyles.ts +3 -3
  149. package/src/components/ui/Button.tsx +4 -4
  150. package/src/components/ui/HyperframesLogo.tsx +19 -18
  151. package/src/components/ui/Input.tsx +1 -1
  152. package/src/components/ui/Menu.tsx +4 -2
  153. package/src/hooks/draggedGsapPosition.ts +28 -0
  154. package/src/hooks/elementOffsetStager.ts +34 -13
  155. package/src/hooks/gsapRuntimeBridge.ts +5 -3
  156. package/src/hooks/gsapRuntimeKeyframes.ts +36 -2
  157. package/src/hooks/plainRotation.test.ts +122 -0
  158. package/src/hooks/plainRotation.ts +42 -0
  159. package/src/hooks/useClipboard.source.test.ts +42 -4
  160. package/src/hooks/useClipboard.ts +13 -3
  161. package/src/hooks/useDomEditCommits.ts +0 -3
  162. package/src/hooks/useDomEditSession.ts +5 -5
  163. package/src/hooks/useDomGeometryCommit.test.tsx +79 -1
  164. package/src/hooks/useDomGeometryCommit.ts +16 -17
  165. package/src/hooks/useDomGeometryCommits.test.tsx +233 -33
  166. package/src/hooks/useDomGeometryCommits.ts +27 -80
  167. package/src/hooks/useGsapAwareEditing.test.tsx +194 -11
  168. package/src/hooks/useGsapAwareEditing.ts +43 -42
  169. package/src/hooks/useInlineTextEdit.test.tsx +33 -3
  170. package/src/hooks/useInlineTextEdit.ts +2 -2
  171. package/src/player/components/ClipContextMenu.tsx +1 -1
  172. package/src/player/components/CompositionThumbnail.tsx +23 -11
  173. package/src/player/components/EditModal.tsx +6 -6
  174. package/src/player/components/KeyframeDiamondContextMenu.tsx +2 -2
  175. package/src/player/components/PlayerControls.tsx +3 -3
  176. package/src/player/components/ShortcutsPanel.tsx +8 -20
  177. package/src/player/components/TimelineCanvas.tsx +1 -1
  178. package/src/player/components/TimelineClip.tsx +2 -2
  179. package/src/player/components/TimelineEmptyState.tsx +2 -2
  180. package/src/player/components/TimelineLanes.tsx +0 -1
  181. package/src/player/components/TrackClipCount.tsx +1 -1
  182. package/src/player/components/VolumeControl.tsx +1 -1
  183. package/src/player/components/timelineClipChildren.tsx +1 -1
  184. package/src/player/components/timelineMotionStyles.test.ts +6 -22
  185. package/src/player/components/trackHeaderLabelRows.tsx +16 -16
  186. package/src/player/lib/thumbnailScheduler.ts +4 -5
  187. package/src/player/lib/timelineViewportBudgets.test.ts +1 -1
  188. package/src/player/lib/timelineViewportBudgets.ts +1 -1
  189. package/src/styles/hex-baseline.json +7 -19
  190. package/src/styles/studio.css +0 -9
  191. package/src/styles/tailwind-preset.shared.js +2 -1
  192. package/src/styles/theme.css +204 -147
  193. package/src/styles/theme.test.ts +13 -9
  194. package/src/utils/blockCategories.ts +41 -9
  195. package/src/utils/clipboardPayload.ts +2 -0
  196. package/dist/assets/index-CQN3M56N.css +0 -1
  197. package/dist/chunk-62DDWOSH.js.map +0 -1
  198. package/dist/chunk-6GAO75IU.js.map +0 -1
  199. package/src/hooks/gsapTargetCache.ts +0 -62
  200. /package/dist/{chunk-HIGHVWYD.js.map → chunk-QADHJFDF.js.map} +0 -0
@@ -1,4 +1,5 @@
1
1
  import { memo, useState, useCallback, useRef, useEffect } from "react";
2
+ import { toastSurface } from "../StudioToast";
2
3
  import {
3
4
  trackStudioFeedback,
4
5
  trackStudioFeedbackShown,
@@ -212,24 +213,11 @@ export const StudioFeedbackCard = memo(function StudioFeedbackCard() {
212
213
  }}
213
214
  >
214
215
  <div
215
- className="flex w-[min(340px,calc(100vw-48px))] flex-col gap-2.5 overflow-hidden rounded-2xl px-3.5 py-3"
216
- style={{
217
- background: failed
218
- ? "linear-gradient(135deg, rgba(127,29,29,0.55), rgba(80,10,10,0.45))"
219
- : "linear-gradient(135deg, rgba(38,38,38,0.55), rgba(23,23,23,0.45))",
220
- backdropFilter: "blur(16px) saturate(1.6)",
221
- WebkitBackdropFilter: "blur(16px) saturate(1.6)",
222
- border: `1px solid ${failed ? "rgba(239,68,68,0.18)" : "rgba(255,255,255,0.08)"}`,
223
- boxShadow: [
224
- "0 8px 32px rgba(0,0,0,0.35)",
225
- `inset 0 1px 0 ${failed ? "rgba(239,68,68,0.12)" : "rgba(255,255,255,0.06)"}`,
226
- "inset 0 -1px 0 rgba(0,0,0,0.15)",
227
- ].join(", "),
228
- }}
216
+ className={`flex w-[min(340px,calc(100vw-48px))] flex-col gap-2.5 overflow-hidden rounded-2xl px-3.5 py-3 ${toastSurface(failed)}`}
229
217
  >
230
218
  <div className="flex items-start gap-3">
231
219
  <p
232
- className={`min-w-0 flex-1 text-[12px] leading-5 ${failed ? "text-red-100" : "text-neutral-100"}`}
220
+ className={`min-w-0 flex-1 text-[12px] leading-5 ${failed ? "text-danger-ink" : "text-text-0"}`}
233
221
  >
234
222
  {step === "thanks" ? "Thanks, that helps." : title}
235
223
  </p>
@@ -243,7 +231,7 @@ export const StudioFeedbackCard = memo(function StudioFeedbackCard() {
243
231
  if (req) trackStudioFeedbackInterviewClick({ reason: req.reason });
244
232
  close("sent");
245
233
  }}
246
- className="h-6 shrink-0 whitespace-nowrap rounded-full border border-white/20 bg-white/8 px-2.5 text-[11px] leading-6 text-neutral-100 transition-[background-color,border-color,transform] duration-150 ease-out hover:border-white/35 hover:bg-white/16 active:scale-[0.97] motion-reduce:transition-none"
234
+ className="h-6 shrink-0 whitespace-nowrap rounded-full border border-border-strong bg-hover px-2.5 text-[11px] leading-6 text-neutral-100 transition-[background-color,border-color,transform] duration-150 ease-out hover:border-text-muted hover:bg-press active:scale-[0.97] motion-reduce:transition-none"
247
235
  >
248
236
  Talk to us, 30 min
249
237
  </a>
@@ -251,7 +239,7 @@ export const StudioFeedbackCard = memo(function StudioFeedbackCard() {
251
239
  <button
252
240
  type="button"
253
241
  onClick={() => close("close")}
254
- className="-mr-1 -mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-md text-neutral-500 transition-colors duration-150 hover:bg-white/10 hover:text-neutral-200"
242
+ className="-mr-1 -mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-md text-neutral-500 transition-colors duration-150 hover:bg-press hover:text-neutral-200"
255
243
  aria-label="Dismiss"
256
244
  >
257
245
  <svg
@@ -270,7 +258,7 @@ export const StudioFeedbackCard = memo(function StudioFeedbackCard() {
270
258
  {/* The failure text the user already saw, quoted back so the report
271
259
  carries it without asking them to retype the error. */}
272
260
  {failed && request.detail && step !== "thanks" && (
273
- <p className="line-clamp-2 text-[11px] leading-4 text-red-200/70">{request.detail}</p>
261
+ <p className="line-clamp-2 text-[11px] leading-4 text-danger-ink">{request.detail}</p>
274
262
  )}
275
263
 
276
264
  {step === "rating" && (
@@ -283,7 +271,7 @@ export const StudioFeedbackCard = memo(function StudioFeedbackCard() {
283
271
  key={n}
284
272
  // A resting fill, so the row reads as eleven controls rather
285
273
  // than as a line of text that happens to be clickable.
286
- className="flex h-7 cursor-pointer items-center justify-center rounded-md bg-white/4 text-[11px] tabular-nums text-neutral-400 transition-[background-color,color,transform] duration-150 ease-out hover:bg-white/[0.14] hover:text-neutral-100 active:scale-[0.97] has-checked:bg-white/90 has-checked:text-neutral-900 has-focus-visible:ring-1 has-focus-visible:ring-white/50 motion-reduce:transition-none"
274
+ className="flex h-7 cursor-pointer items-center justify-center rounded-md bg-hover text-[11px] tabular-nums text-neutral-400 transition-[background-color,color,transform] duration-150 ease-out hover:bg-press hover:text-neutral-100 active:scale-[0.97] has-checked:bg-text-0 has-checked:text-bg-0 has-focus-visible:ring-1 has-focus-visible:ring-ring motion-reduce:transition-none"
287
275
  >
288
276
  <input
289
277
  type="radio"
@@ -328,8 +316,8 @@ export const StudioFeedbackCard = memo(function StudioFeedbackCard() {
328
316
  // attached to nothing.
329
317
  className={`h-6 rounded-full border px-2.5 text-[11px] transition-[background-color,border-color,color,transform] duration-150 ease-out active:scale-[0.97] motion-reduce:transition-none ${
330
318
  failed
331
- ? "border-red-300/20 bg-red-100/6 text-red-100/80 hover:border-red-200/70 hover:bg-red-100/25 hover:text-white"
332
- : "border-white/10 bg-white/4 text-neutral-300 hover:border-white/60 hover:bg-white/25 hover:text-white"
319
+ ? "border-danger/30 bg-danger/10 text-danger-ink hover:border-danger/60 hover:bg-danger/20"
320
+ : "border-border bg-hover text-neutral-300 hover:border-text-muted hover:bg-press hover:text-text-0"
333
321
  }`}
334
322
  >
335
323
  {preset.label}
@@ -355,12 +343,12 @@ export const StudioFeedbackCard = memo(function StudioFeedbackCard() {
355
343
  placeholder={followUp.placeholder}
356
344
  aria-label={followUp.prompt}
357
345
  maxLength={500}
358
- className="h-7 min-w-0 flex-1 rounded-md border border-white/10 bg-black/20 px-2 text-[11px] text-neutral-100 outline-hidden transition-colors duration-150 placeholder:text-neutral-500 focus:border-white/25"
346
+ className="h-7 min-w-0 flex-1 rounded-md border border-border bg-input px-2 text-[11px] text-neutral-100 outline-hidden transition-colors duration-150 placeholder:text-neutral-500 focus:border-ring"
359
347
  />
360
348
  <button
361
349
  type="button"
362
350
  onClick={() => submit()}
363
- className="h-7 shrink-0 rounded-md bg-white/10 px-2.5 text-[11px] text-neutral-100 transition-[background-color,transform] duration-150 ease-out hover:bg-white/16 active:scale-[0.97] motion-reduce:transition-none"
351
+ className="h-7 shrink-0 rounded-md bg-press px-2.5 text-[11px] text-neutral-100 transition-[background-color,transform] duration-150 ease-out hover:bg-on active:scale-[0.97] motion-reduce:transition-none"
364
352
  >
365
353
  Send
366
354
  </button>
@@ -142,7 +142,7 @@ export function AssetPreviewOverlay() {
142
142
  >
143
143
  {/* Close button */}
144
144
  <button
145
- className="absolute top-2 right-2 w-6 h-6 rounded-full bg-neutral-800 hover:bg-neutral-700 text-neutral-300 hover:text-white flex items-center justify-center transition-colors z-10"
145
+ className="absolute top-2 right-2 w-6 h-6 rounded-full bg-raised hover:bg-press text-text-2 hover:text-text-0 flex items-center justify-center transition-colors z-10"
146
146
  onClick={(e) => {
147
147
  e.stopPropagation();
148
148
  clearPreviewAsset();
@@ -215,7 +215,7 @@ function useMeterLoop(
215
215
 
216
216
  function Bar({ maskRef, peakRef }: { maskRef: Ref<HTMLDivElement>; peakRef: Ref<HTMLDivElement> }) {
217
217
  return (
218
- <div className="relative h-full w-[18px] overflow-hidden rounded-[2px] bg-neutral-900">
218
+ <div className="scheme-dark relative h-full w-[18px] overflow-hidden rounded-[2px] bg-neutral-900">
219
219
  <div
220
220
  className="absolute inset-x-0 bottom-0 bg-green-500"
221
221
  style={{ height: `${AMBER_AT * 100}%` }}
@@ -237,7 +237,7 @@ function Bar({ maskRef, peakRef }: { maskRef: Ref<HTMLDivElement>; peakRef: Ref<
237
237
  <div
238
238
  ref={peakRef}
239
239
  data-testid="meter-peak"
240
- className="absolute inset-x-0 bottom-0 h-px bg-white"
240
+ className="absolute inset-x-0 bottom-0 h-px bg-text-0"
241
241
  />
242
242
  </div>
243
243
  );
@@ -413,13 +413,14 @@ describe("NLEPreview", () => {
413
413
  describe("a missing poster", () => {
414
414
  const renderUrl =
415
415
  "/api/projects/timeline-edit-playground/thumbnail/index.html?t=0&output=source";
416
- const fetchSpy = vi.fn(() => Promise.resolve(new Response()));
416
+ const fetchSpy = vi.fn((_url: string, _init?: RequestInit) => Promise.resolve(new Response()));
417
417
  beforeEach(() => {
418
418
  fetchSpy.mockClear();
419
419
  vi.stubGlobal("fetch", fetchSpy);
420
420
  });
421
421
  afterEach(() => vi.unstubAllGlobals());
422
422
 
423
+ const renders = () => fetchSpy.mock.calls.map(([url]) => url);
423
424
  const settle = (
424
425
  view: ReturnType<typeof renderPreview>,
425
426
  steps: Array<"ready" | "missing" | "loaded">,
@@ -437,7 +438,10 @@ describe("NLEPreview", () => {
437
438
  it("is rendered for the next open when the live frame is ready first", () => {
438
439
  const view = renderPreview();
439
440
  settle(view, ["ready", "missing"]);
440
- expect(fetchSpy.mock.calls).toEqual([[renderUrl]]);
441
+ expect(renders()).toEqual([renderUrl]);
442
+ expect(fetchSpy.mock.calls[0]?.[1]?.signal, "rendered as scheduler work").toBeInstanceOf(
443
+ AbortSignal,
444
+ );
441
445
  expect(view.stage.querySelector('[data-testid="preview-poster"]')).toBeNull();
442
446
  view.cleanup();
443
447
  });
@@ -447,7 +451,7 @@ describe("NLEPreview", () => {
447
451
  settle(view, ["missing"]);
448
452
  expect(fetchSpy).not.toHaveBeenCalled();
449
453
  settle(view, ["ready", "ready"]);
450
- expect(fetchSpy.mock.calls).toEqual([[renderUrl]]);
454
+ expect(renders()).toEqual([renderUrl]);
451
455
  view.cleanup();
452
456
  });
453
457
 
@@ -458,7 +462,7 @@ describe("NLEPreview", () => {
458
462
  settle(view, ["ready"]);
459
463
  view.render();
460
464
  settle(view, ["ready", "missing"]);
461
- expect(fetchSpy.mock.calls).toEqual([[renderUrl]]);
465
+ expect(renders()).toEqual([renderUrl]);
462
466
  view.cleanup();
463
467
  });
464
468
  });
@@ -152,7 +152,7 @@ export function PreviewOverlays({
152
152
  <button
153
153
  type="button"
154
154
  onClick={enterCaptionMode}
155
- className="pointer-events-auto absolute top-2 left-1/2 -translate-x-1/2 z-60 rounded-full border border-neutral-700 bg-black/60 px-2.5 py-1 text-2xs text-neutral-300 transition-colors hover:border-studio-accent/50 hover:text-studio-accent focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-studio-accent"
155
+ className="pointer-events-auto absolute top-2 left-1/2 -translate-x-1/2 z-60 rounded-full border border-neutral-700 bg-black/60 px-2.5 py-1 text-2xs text-neutral-300 transition-colors hover:border-studio-accent/50 hover:text-accent-ink focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-studio-accent"
156
156
  >
157
157
  Edit captions
158
158
  </button>
@@ -116,9 +116,9 @@ export function PreviewPane({
116
116
  <div
117
117
  ref={containerRef}
118
118
  // A recessed well inset in its dock card; dropped in fullscreen so the
119
- // preview fills the screen edge-to-edge.
120
- className={`flex-1 min-h-0 flex flex-col overflow-hidden bg-[var(--studio-preview-bg,var(--color-neutral-950))] ${
121
- isFullscreen ? "" : "m-3 rounded-md border border-neutral-800/50"
119
+ // preview fills the screen edge-to-edge. The player stays dark in both themes.
120
+ className={`scheme-dark flex-1 min-h-0 flex flex-col overflow-hidden bg-[var(--studio-preview-bg,var(--color-neutral-950))] ${
121
+ isFullscreen ? "" : "m-3 rounded-md border border-border"
122
122
  }`}
123
123
  data-studio-fullscreen-target=""
124
124
  >
@@ -1,5 +1,10 @@
1
1
  import { useCallback, useRef, useState } from "react";
2
2
  import { buildProjectApiPath } from "../../utils/projectRouting";
3
+ import {
4
+ createThumbnailKey,
5
+ thumbnailScheduler,
6
+ type ThumbnailRequest,
7
+ } from "../../player/lib/thumbnailScheduler";
3
8
  import type { PreviewCompositionSize } from "../../utils/previewCompositionSize";
4
9
 
5
10
  /** Frame 0 as the thumbnail route last rendered it; `cachedOnly` never starts a render. */
@@ -65,7 +70,7 @@ export function usePreviewPoster(
65
70
  const renderMissingPoster = useCallback(() => {
66
71
  if (missingForRef.current !== visit || liveReadyForRef.current !== visit) return;
67
72
  missingForRef.current = null;
68
- void fetch(previewPosterUrl(projectId, false)).catch(() => {});
73
+ renderPosterForNextOpen(projectId);
69
74
  }, [visit, projectId]);
70
75
  const onLiveReadyToShowChange = useCallback(
71
76
  (ready: boolean) => {
@@ -91,6 +96,29 @@ export function usePreviewPoster(
91
96
  };
92
97
  }
93
98
 
99
+ /** Renders frame 0 under the thumbnail cap; the lease ends once the render settles. */
100
+ export function renderPosterForNextOpen(projectId: string): void {
101
+ const url = previewPosterUrl(projectId, false);
102
+ const request: ThumbnailRequest = {
103
+ key: createThumbnailKey({ kind: "poster-render", url }),
104
+ projectId,
105
+ sessionEpoch: 0,
106
+ kind: "composition",
107
+ priority: "overscan",
108
+ discardWhenReleased: true,
109
+ load: async (signal) => {
110
+ await (await fetch(url, { signal })).arrayBuffer();
111
+ return { value: { kind: "image", url, aspect: 16 / 9 }, weight: 0 };
112
+ },
113
+ };
114
+ const settled = () => ["ready", "error"].includes(thumbnailScheduler.getSnapshot(request).status);
115
+ let lease: { release(): void } | null = null;
116
+ lease = thumbnailScheduler.acquire(request, () => {
117
+ if (settled()) lease?.release();
118
+ });
119
+ if (settled()) lease.release();
120
+ }
121
+
94
122
  function useVisitNumber(key: string): number {
95
123
  const [seen, setSeen] = useState({ key, visit: 0 });
96
124
  if (seen.key === key) return seen.visit;
@@ -85,7 +85,7 @@ export function PreviewZoomOverlay({
85
85
  </span>
86
86
  <button
87
87
  type="button"
88
- className="rounded px-1.5 py-0.5 font-medium text-studio-accent hover:bg-white/10 transition-colors"
88
+ className="rounded px-1.5 py-0.5 font-medium text-accent-ink hover:bg-white/10 transition-colors"
89
89
  onClick={onFit}
90
90
  aria-label="Fit the whole frame in view"
91
91
  data-testid="preview-zoom-fit"
@@ -413,7 +413,7 @@ export function SlideshowPanel({ scenes, onPersist, onPersistNotes }: SlideshowP
413
413
 
414
414
  return (
415
415
  <div
416
- className="flex flex-col h-full overflow-y-auto text-white"
416
+ className="flex flex-col h-full overflow-y-auto text-text-0"
417
417
  onKeyDown={(e) => {
418
418
  // In-panel undo — scoped so it never fights the app-level file undo.
419
419
  if ((e.metaKey || e.ctrlKey) && !e.shiftKey && e.key.toLowerCase() === "z") {
@@ -428,14 +428,14 @@ export function SlideshowPanel({ scenes, onPersist, onPersistNotes }: SlideshowP
428
428
  {persistError && (
429
429
  <div
430
430
  role="alert"
431
- className="flex items-center justify-between gap-2 px-3 py-2 bg-red-950/40 border-b border-red-500/40"
431
+ className="flex items-center justify-between gap-2 px-3 py-2 bg-danger/15 border-b border-red-500/40"
432
432
  >
433
- <span className="text-[11px] text-red-300">Changes not saved</span>
433
+ <span className="text-[11px] text-danger-ink">Changes not saved</span>
434
434
  <button
435
435
  type="button"
436
436
  disabled={retrying}
437
437
  onClick={handleRetryPersist}
438
- className="px-2 py-0.5 text-[10px] rounded-sm bg-red-600 text-white enabled:hover:bg-red-500 enabled:active:scale-[0.97] disabled:opacity-50 transition-colors"
438
+ className="px-2 py-0.5 text-[10px] rounded-sm bg-danger text-on-danger enabled:hover:brightness-95 enabled:active:scale-[0.97] disabled:opacity-50 transition-colors"
439
439
  >
440
440
  {retrying ? "Retrying…" : "Retry"}
441
441
  </button>
@@ -72,9 +72,7 @@ export function SlideList({
72
72
  tabIndex={0}
73
73
  aria-pressed={isSelected}
74
74
  className={`flex items-center gap-2 px-3 py-1.5 rounded cursor-pointer text-[11px] transition-colors ${
75
- isSelected
76
- ? "bg-studio-accent/20 text-white"
77
- : "hover:bg-neutral-800/60 text-neutral-300"
75
+ isSelected ? "bg-on text-text-0" : "hover:bg-neutral-800/60 text-neutral-300"
78
76
  }`}
79
77
  onClick={() => onSelect(scene.id)}
80
78
  onKeyDown={(e) => {
@@ -100,7 +98,7 @@ export function SlideList({
100
98
  aria-label="Move slide up"
101
99
  title="Move up"
102
100
  disabled={slideIndex <= 0}
103
- className="px-1 py-0.5 text-[10px] text-neutral-400 enabled:hover:text-white enabled:active:scale-[0.95] disabled:opacity-30 disabled:cursor-not-allowed"
101
+ className="px-1 py-0.5 text-[10px] text-neutral-400 enabled:hover:text-text-0 enabled:active:scale-[0.95] disabled:opacity-30 disabled:cursor-not-allowed"
104
102
  onClick={(e) => {
105
103
  e.stopPropagation();
106
104
  onReorder(scene.id, "up");
@@ -113,7 +111,7 @@ export function SlideList({
113
111
  aria-label="Move slide down"
114
112
  title="Move down"
115
113
  disabled={slideIndex === slides.length - 1}
116
- className="px-1 py-0.5 text-[10px] text-neutral-400 enabled:hover:text-white enabled:active:scale-[0.95] disabled:opacity-30 disabled:cursor-not-allowed"
114
+ className="px-1 py-0.5 text-[10px] text-neutral-400 enabled:hover:text-text-0 enabled:active:scale-[0.95] disabled:opacity-30 disabled:cursor-not-allowed"
117
115
  onClick={(e) => {
118
116
  e.stopPropagation();
119
117
  onReorder(scene.id, "down");
@@ -162,7 +160,7 @@ export function SlideInspector({
162
160
  <div className="flex flex-col gap-1">
163
161
  <label className="text-[11px] text-neutral-400">Notes</label>
164
162
  <textarea
165
- className="bg-neutral-800 border border-neutral-700 rounded-sm px-2 py-1.5 text-[11px] text-white resize-none placeholder-neutral-600 focus:border-studio-accent/60 focus:outline-hidden"
163
+ className="bg-neutral-800 border border-neutral-700 rounded-sm px-2 py-1.5 text-[11px] text-text-0 resize-none placeholder-neutral-600 focus:border-studio-accent/60 focus:outline-hidden"
166
164
  rows={3}
167
165
  placeholder="Speaker notes or script..."
168
166
  value={slide?.notes ?? ""}
@@ -174,7 +172,7 @@ export function SlideInspector({
174
172
  <span className="text-[11px] text-neutral-400">Fragment hold-points</span>
175
173
  <button
176
174
  type="button"
177
- className="text-[10px] px-2 py-0.5 rounded-sm bg-neutral-700 hover:bg-neutral-600 text-neutral-200 transition-colors"
175
+ className="text-[10px] px-2 py-0.5 rounded-sm bg-hover hover:bg-press text-neutral-200 transition-colors"
178
176
  onClick={onMarkFragment}
179
177
  title={`Mark ${currentTime.toFixed(2)}s as hold-point`}
180
178
  >
@@ -192,7 +190,7 @@ export function SlideInspector({
192
190
  <button
193
191
  type="button"
194
192
  aria-label={`Remove fragment at ${t.toFixed(2)}s`}
195
- className="text-neutral-400 hover:text-red-400 transition-colors"
193
+ className="text-neutral-400 hover:text-danger-ink transition-colors"
196
194
  onClick={() => onRemoveFragment(t)}
197
195
  >
198
196
  ×
@@ -249,7 +247,7 @@ export function BranchTree({
249
247
  <input
250
248
  id={inputId}
251
249
  type="text"
252
- className="flex-1 bg-neutral-800 border border-neutral-700 rounded-sm px-2 py-1 text-[11px] text-white placeholder-neutral-600 focus:border-studio-accent/60 focus:outline-hidden"
250
+ className="flex-1 bg-neutral-800 border border-neutral-700 rounded-sm px-2 py-1 text-[11px] text-text-0 placeholder-neutral-600 focus:border-studio-accent/60 focus:outline-hidden"
253
251
  placeholder="New branch name..."
254
252
  value={newLabel}
255
253
  onChange={(e) => setNewLabel(e.target.value)}
@@ -260,7 +258,7 @@ export function BranchTree({
260
258
  />
261
259
  <button
262
260
  type="button"
263
- className="px-2 py-1 rounded-sm bg-neutral-700 hover:bg-neutral-600 text-[11px] text-neutral-200 transition-colors shrink-0"
261
+ className="px-2 py-1 rounded-sm bg-hover hover:bg-press text-[11px] text-neutral-200 transition-colors shrink-0"
264
262
  onClick={handleCreate}
265
263
  >
266
264
  Add
@@ -326,7 +324,7 @@ function BranchItem({
326
324
  <div className="flex items-center gap-1">
327
325
  {editing ? (
328
326
  <input
329
- className="flex-1 bg-neutral-800 border border-neutral-600 rounded-sm px-1.5 py-0.5 text-[11px] text-white focus:border-studio-accent/60 focus:outline-hidden"
327
+ className="flex-1 bg-neutral-800 border border-neutral-600 rounded-sm px-1.5 py-0.5 text-[11px] text-text-0 focus:border-studio-accent/60 focus:outline-hidden"
330
328
  value={draft}
331
329
  autoFocus
332
330
  onChange={(e) => setDraft(e.target.value)}
@@ -341,7 +339,7 @@ function BranchItem({
341
339
  <span
342
340
  role="button"
343
341
  tabIndex={0}
344
- className="flex-1 text-[11px] text-white font-medium truncate cursor-pointer hover:text-neutral-300"
342
+ className="flex-1 text-[11px] text-text-0 font-medium truncate cursor-pointer hover:text-neutral-300"
345
343
  title="Click to rename"
346
344
  onClick={() => setEditing(true)}
347
345
  onKeyDown={(e) => {
@@ -357,15 +355,15 @@ function BranchItem({
357
355
  <button
358
356
  type="button"
359
357
  aria-label={`Delete branch ${seq.label}`}
360
- className="text-[10px] text-neutral-500 hover:text-red-400 active:scale-[0.95] transition-colors px-1"
358
+ className="text-[10px] text-neutral-500 hover:text-danger-ink active:scale-[0.95] transition-colors px-1"
361
359
  onClick={() => setConfirmingDelete(true)}
362
360
  >
363
361
  ✕
364
362
  </button>
365
363
  </div>
366
364
  {confirmingDelete && (
367
- <div className="px-2 py-1.5 bg-red-950/30 border-l-2 border-red-500 flex flex-col gap-1 rounded-xs">
368
- <span className="text-[10px] text-red-400">
365
+ <div className="px-2 py-1.5 bg-danger/15 border-l-2 border-red-500 flex flex-col gap-1 rounded-xs">
366
+ <span className="text-[10px] text-danger-ink">
369
367
  Delete branch &ldquo;{seq.label}&rdquo;
370
368
  {seq.slides.length > 0
371
369
  ? ` and its ${seq.slides.length} slide${seq.slides.length === 1 ? "" : "s"}`
@@ -379,7 +377,7 @@ function BranchItem({
379
377
  setConfirmingDelete(false);
380
378
  onDelete(seq.id);
381
379
  }}
382
- className="px-2 py-0.5 text-[10px] rounded-sm bg-red-600 text-white hover:bg-red-500 active:bg-red-700 transition-colors"
380
+ className="px-2 py-0.5 text-[10px] rounded-sm bg-danger text-on-danger hover:brightness-95 active:brightness-90 transition-colors"
383
381
  >
384
382
  Delete
385
383
  </button>
@@ -414,7 +412,7 @@ function BranchItem({
414
412
  type="button"
415
413
  aria-pressed={isSelected}
416
414
  className={`flex-1 text-left truncate transition-colors hover:text-neutral-200 ${
417
- isSelected ? "text-white" : "text-neutral-400"
415
+ isSelected ? "text-text-0" : "text-neutral-400"
418
416
  }`}
419
417
  onClick={() => onSelectBranchSlide(seq.id, scene.id)}
420
418
  >
@@ -497,7 +495,7 @@ export function HotspotTool({
497
495
  <label className="text-[11px] text-neutral-400">Hotspot label</label>
498
496
  <input
499
497
  type="text"
500
- className="bg-neutral-800 border border-neutral-700 rounded-sm px-2 py-1 text-[11px] text-white placeholder-neutral-600 focus:border-studio-accent/60 focus:outline-hidden"
498
+ className="bg-neutral-800 border border-neutral-700 rounded-sm px-2 py-1 text-[11px] text-text-0 placeholder-neutral-600 focus:border-studio-accent/60 focus:outline-hidden"
501
499
  placeholder="Button label..."
502
500
  value={hotspotLabel}
503
501
  onChange={(e) => setHotspotLabel(e.target.value)}
@@ -505,7 +503,7 @@ export function HotspotTool({
505
503
  />
506
504
  <label className="text-[11px] text-neutral-400">Target branch</label>
507
505
  <select
508
- className="bg-neutral-800 border border-neutral-700 rounded-sm px-2 py-1 text-[11px] text-white focus:border-studio-accent/60 focus:outline-hidden"
506
+ className="bg-neutral-800 border border-neutral-700 rounded-sm px-2 py-1 text-[11px] text-text-0 focus:border-studio-accent/60 focus:outline-hidden"
509
507
  value={targetSequenceId}
510
508
  onChange={(e) => setTargetSequenceId(e.target.value)}
511
509
  aria-label="Target branch sequence"
@@ -527,7 +525,7 @@ export function HotspotTool({
527
525
  ? "Choose a target branch first"
528
526
  : undefined
529
527
  }
530
- className="px-3 py-1.5 rounded-sm bg-studio-accent/80 enabled:hover:bg-studio-accent enabled:active:scale-[0.98] text-white text-[11px] font-medium transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
528
+ className="px-3 py-1.5 rounded-sm bg-accent enabled:hover:bg-accent-hover enabled:active:scale-[0.98] text-on-accent text-[11px] font-medium transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
531
529
  onClick={handleMakeHotspot}
532
530
  >
533
531
  Make hotspot
@@ -550,7 +548,7 @@ export function HotspotTool({
550
548
  <button
551
549
  type="button"
552
550
  aria-label={`Remove hotspot ${h.label}`}
553
- className="text-[10px] text-neutral-500 hover:text-red-400 transition-colors"
551
+ className="text-[10px] text-neutral-500 hover:text-danger-ink transition-colors"
554
552
  onClick={() => onRemoveHotspot(selectedSceneId, h.id)}
555
553
  >
556
554
  ✕
@@ -215,7 +215,7 @@ export function VariablesBindElement({
215
215
  className={`${VARIABLES_INPUT_CLASS} font-mono`}
216
216
  />
217
217
  {existingDecl && (
218
- <p className="text-[9px] leading-snug text-amber-400/90">
218
+ <p className="text-[9px] leading-snug text-warning-ink">
219
219
  "{trimmedId}" already exists. This element will use its current value
220
220
  {existingDecl.default !== undefined && (
221
221
  <span className="font-mono"> ({String(existingDecl.default)})</span>
@@ -299,7 +299,7 @@ export function DeclarationForm({
299
299
  className={VARIABLES_INPUT_CLASS}
300
300
  />
301
301
  </Field>
302
- {error && <p className="text-[10px] text-red-400">{error}</p>}
302
+ {error && <p className="text-[10px] text-danger-ink">{error}</p>}
303
303
  <div className="flex items-center justify-end gap-2 pt-1">
304
304
  <button
305
305
  type="button"
@@ -56,9 +56,9 @@ function formatIssue(issue: VariableValidationIssue): string {
56
56
  function ValidationStrip({ issues }: { issues: VariableValidationIssue[] }) {
57
57
  if (issues.length === 0) return null;
58
58
  return (
59
- <div className="space-y-1 rounded-lg border border-red-900/60 bg-red-950/30 p-2">
59
+ <div className="space-y-1 rounded-lg border border-danger/40 bg-danger/15 p-2">
60
60
  {issues.map((issue) => (
61
- <p key={`${issue.kind}:${issue.variableId}`} className="text-[10px] text-red-300">
61
+ <p key={`${issue.kind}:${issue.variableId}`} className="text-[10px] text-danger-ink">
62
62
  {formatIssue(issue)}
63
63
  </p>
64
64
  ))}
@@ -99,7 +99,7 @@ function VariableRow({
99
99
  </span>
100
100
  {unused && (
101
101
  <span
102
- className="rounded-sm bg-amber-900/40 px-1 py-px text-[8px] text-amber-400"
102
+ className="rounded-sm bg-amber-500/15 px-1 py-px text-[8px] text-warning-ink"
103
103
  title="No script reads this variable"
104
104
  >
105
105
  unused
@@ -175,9 +175,7 @@ function PreviewModeHeader({
175
175
  <span className="text-[11px] font-semibold text-neutral-200">Variables</span>
176
176
  <span
177
177
  className={`rounded-full px-2 py-0.5 text-[9px] font-medium ${
178
- hasOverrides
179
- ? "bg-studio-accent/20 text-studio-accent"
180
- : "bg-neutral-800 text-neutral-500"
178
+ hasOverrides ? "bg-studio-accent/20 text-accent-ink" : "bg-neutral-800 text-neutral-500"
181
179
  }`}
182
180
  >
183
181
  {hasOverrides ? `Previewing ${overrideCount} custom` : "Previewing defaults"}
@@ -16,7 +16,7 @@ export function RowAction({
16
16
  onClick={onClick}
17
17
  title={title}
18
18
  className={`h-5 rounded px-1.5 text-[9px] text-neutral-500 hover:bg-neutral-800 ${
19
- danger ? "hover:text-red-400" : "hover:text-neutral-200"
19
+ danger ? "hover:text-danger-ink" : "hover:text-neutral-200"
20
20
  }`}
21
21
  >
22
22
  {label}
@@ -67,7 +67,7 @@ export const FfmpegRequiredNotice = memo(function FfmpegRequiredNotice({
67
67
  className="flex flex-col gap-2 rounded-xl border border-amber-500/30 bg-amber-500/10 p-2.5"
68
68
  >
69
69
  <div className="flex flex-col gap-0.5">
70
- <span className="text-[11px] font-semibold text-amber-300">
70
+ <span className="text-[11px] font-semibold text-warning-ink">
71
71
  {status.title ?? "FFmpeg not found"}
72
72
  </span>
73
73
  {/* text-2, not the panel's usual text-4 for secondary copy: the amber
@@ -81,7 +81,7 @@ export const FfmpegRequiredNotice = memo(function FfmpegRequiredNotice({
81
81
 
82
82
  {status.command ? (
83
83
  <div className="flex items-center gap-1.5">
84
- <code className="flex-1 overflow-x-auto whitespace-nowrap rounded-xs bg-black/40 px-2 py-1 text-[10px] text-panel-text-2">
84
+ <code className="flex-1 overflow-x-auto whitespace-nowrap rounded-xs bg-input px-2 py-1 text-[10px] text-panel-text-2">
85
85
  {status.command}
86
86
  </code>
87
87
  {/* Fixed width so swapping the label to "Copied" cannot shift the
@@ -89,7 +89,7 @@ export const FfmpegRequiredNotice = memo(function FfmpegRequiredNotice({
89
89
  <button
90
90
  type="button"
91
91
  onClick={() => void copy(status.command ?? "")}
92
- className={`h-6 w-14 shrink-0 rounded-xs border border-amber-500/30 text-[10px] font-medium text-amber-200 transition-colors hover:bg-amber-500/20 active:scale-[0.98] ${FOCUS_RING}`}
92
+ className={`h-6 w-14 shrink-0 rounded-xs border border-amber-500/30 text-[10px] font-medium text-warning-ink transition-colors hover:bg-amber-500/20 active:scale-[0.98] ${FOCUS_RING}`}
93
93
  >
94
94
  {copied ? "Copied" : "Copy"}
95
95
  </button>
@@ -107,7 +107,7 @@ export const FfmpegRequiredNotice = memo(function FfmpegRequiredNotice({
107
107
  type="button"
108
108
  onClick={onRecheck}
109
109
  disabled={checking}
110
- className={`rounded-xs py-0.5 text-[10px] font-medium text-amber-200 underline-offset-2 transition-colors hover:underline disabled:opacity-50 ${FOCUS_RING}`}
110
+ className={`rounded-xs py-0.5 text-[10px] font-medium text-warning-ink underline-offset-2 transition-colors hover:underline disabled:opacity-50 ${FOCUS_RING}`}
111
111
  >
112
112
  {checking ? "Checking…" : "Recheck"}
113
113
  </button>
@@ -467,7 +467,7 @@ export const RenderQueue = memo(function RenderQueue({
467
467
  role="alert"
468
468
  className="flex items-start justify-between gap-2 px-3 py-2 border-b border-border bg-danger/10"
469
469
  >
470
- <span className="text-step-10 text-danger">{actionError}</span>
470
+ <span className="text-step-10 text-danger-ink">{actionError}</span>
471
471
  {onDismissActionError && (
472
472
  <IconButton
473
473
  size="sm"
@@ -497,7 +497,7 @@ export const RenderQueue = memo(function RenderQueue({
497
497
  <div ref={listRef} className="flex-1 overflow-y-auto">
498
498
  {loadError && jobs.length === 0 ? (
499
499
  <div className="flex flex-col items-center justify-center h-full px-4 gap-2" role="alert">
500
- <p className="text-step-10 text-danger text-center">{loadError}</p>
500
+ <p className="text-step-10 text-danger-ink text-center">{loadError}</p>
501
501
  {onRetryLoad && (
502
502
  <Button size="sm" variant="secondary" onClick={onRetryLoad}>
503
503
  Retry
@@ -115,7 +115,7 @@ export const RenderQueueItem = memo(function RenderQueueItem({
115
115
  )}
116
116
  {job.status === "failed" && (
117
117
  <div className="w-full h-full flex items-center justify-center">
118
- <div className="w-2 h-2 rounded-full bg-danger" />
118
+ <div className="w-2 h-2 rounded-full bg-danger-ink" />
119
119
  </div>
120
120
  )}
121
121
  {job.status === "cancelled" && (
@@ -140,7 +140,7 @@ export const RenderQueueItem = memo(function RenderQueueItem({
140
140
  <div className="mt-1">
141
141
  <div className="flex items-center justify-between mb-0.5">
142
142
  <span className="text-step-9 text-text-4">{job.stage || "Rendering"}</span>
143
- <span className="text-step-9 font-mono text-accent">{job.progress}%</span>
143
+ <span className="text-step-9 font-mono text-accent-ink">{job.progress}%</span>
144
144
  </div>
145
145
  <div
146
146
  className="w-full h-1 bg-border rounded-full overflow-hidden"
@@ -159,7 +159,7 @@ export const RenderQueueItem = memo(function RenderQueueItem({
159
159
  )}
160
160
 
161
161
  {job.status === "failed" && job.error && (
162
- <span className="text-step-9 text-danger mt-0.5 block">{job.error}</span>
162
+ <span className="text-step-9 text-danger-ink mt-0.5 block">{job.error}</span>
163
163
  )}
164
164
  {job.status === "cancelled" && (
165
165
  <span className="text-step-9 text-text-4 mt-0.5 block">Cancelled</span>
@@ -31,7 +31,7 @@ function CopyChip({ feedback, asset }: { feedback: CopyFeedback; asset: string }
31
31
  <span
32
32
  role="status"
33
33
  className={`shrink-0 text-[9px] font-medium px-1.5 py-px rounded ${
34
- feedback.ok ? "text-panel-accent bg-panel-accent/10" : "text-red-400 bg-red-500/10"
34
+ feedback.ok ? "text-accent-ink bg-panel-accent/10" : "text-danger-ink bg-red-500/10"
35
35
  }`}
36
36
  >
37
37
  {feedback.ok ? "Copied" : "Copy failed"}
@@ -355,7 +355,7 @@ export function FontRow({
355
355
  }`}
356
356
  >
357
357
  <div className="w-[50px] h-[32px] rounded-sm overflow-hidden bg-neutral-900 shrink-0 flex items-center justify-center">
358
- <span className="text-[9px] font-medium text-neutral-700">{extension}</span>
358
+ <span className="text-[9px] font-medium text-text-muted">{extension}</span>
359
359
  </div>
360
360
  <div className="min-w-0 flex-1">
361
361
  <span
@@ -366,7 +366,7 @@ export function FontRow({
366
366
  <div className="flex items-center gap-1.5">
367
367
  <span className="text-[10px] text-neutral-600 truncate">{extension}</span>
368
368
  {used && (
369
- <span className="text-[9px] font-medium text-panel-accent bg-panel-accent/10 px-1.5 py-px rounded-sm">
369
+ <span className="text-[9px] font-medium text-accent-ink bg-panel-accent/10 px-1.5 py-px rounded-sm">
370
370
  in use
371
371
  </span>
372
372
  )}