@effectnode/media 0.5.0 → 0.7.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.
@@ -324,20 +324,38 @@ export default function MovieStudioTab({ projectId }: Props) {
324
324
 
325
325
  {/* ===== Submit ===== */}
326
326
  {store.generating ? (
327
- <div className="flex items-center gap-3">
328
- <div className="flex items-center gap-2 flex-1 px-4 py-3 bg-ink-50 border border-ink-200 rounded-2xl">
329
- {SpinnerIcon}
330
- <span className="text-sm font-medium text-ink-700">
331
- Generating production bible...
332
- </span>
327
+ <div className="flex flex-col gap-2">
328
+ <div className="flex items-center gap-3">
329
+ <div className="flex items-center gap-2 flex-1 px-4 py-3 bg-ink-50 border border-ink-200 rounded-2xl">
330
+ {SpinnerIcon}
331
+ <span className="text-sm font-medium text-ink-700">
332
+ {store.generateStatus ?? "Generating production bible..."}
333
+ </span>
334
+ </div>
335
+ <button
336
+ onClick={() => store.stop()}
337
+ className="flex items-center justify-center gap-2 px-4 py-3 bg-red-500 hover:bg-red-600 active:bg-red-700 text-white text-sm font-semibold rounded-2xl transition-all duration-150 shadow-sm"
338
+ >
339
+ {StopIcon}
340
+ Stop
341
+ </button>
333
342
  </div>
334
- <button
335
- onClick={() => store.stop()}
336
- className="flex items-center justify-center gap-2 px-4 py-3 bg-red-500 hover:bg-red-600 active:bg-red-700 text-white text-sm font-semibold rounded-2xl transition-all duration-150 shadow-sm"
337
- >
338
- {StopIcon}
339
- Stop
340
- </button>
343
+ {store.generateProgress && store.generateProgress.total > 0 && (
344
+ <div className="flex items-center gap-3 px-1">
345
+ <div className="flex-1 h-2 bg-ink-200 rounded-full overflow-hidden">
346
+ <div
347
+ className="h-full bg-tiffany-500 rounded-full transition-all duration-300"
348
+ style={{
349
+ width: `${(store.generateProgress.current / store.generateProgress.total) * 100}%`,
350
+ }}
351
+ />
352
+ </div>
353
+ <span className="text-xs font-semibold text-ink-700 tabular-nums whitespace-nowrap">
354
+ {store.generateProgress.current}/
355
+ {store.generateProgress.total}
356
+ </span>
357
+ </div>
358
+ )}
341
359
  </div>
342
360
  ) : (
343
361
  <button
@@ -387,7 +405,10 @@ export default function MovieStudioTab({ projectId }: Props) {
387
405
  `character:${c.slug}`,
388
406
  );
389
407
  return (
390
- <tr key={c.slug} className="border-b border-ink-200">
408
+ <tr
409
+ key={c.slug}
410
+ className="border-b border-ink-200"
411
+ >
391
412
  <td className="border border-ink-200 px-2 py-1.5 align-top font-mono text-[11px] text-tiffany-700">
392
413
  {c.slug}
393
414
  </td>
@@ -418,7 +439,9 @@ export default function MovieStudioTab({ projectId }: Props) {
418
439
  />
419
440
  </div>
420
441
  ) : (
421
- <span className="text-ink-300 text-xs">—</span>
442
+ <span className="text-ink-300 text-xs">
443
+
444
+ </span>
422
445
  )}
423
446
  </td>
424
447
  <td className="border border-ink-200 px-2 py-1.5 align-top">
@@ -468,7 +491,10 @@ export default function MovieStudioTab({ projectId }: Props) {
468
491
  `place:${p.slug}`,
469
492
  );
470
493
  return (
471
- <tr key={p.slug} className="border-b border-ink-200">
494
+ <tr
495
+ key={p.slug}
496
+ className="border-b border-ink-200"
497
+ >
472
498
  <td className="border border-ink-200 px-2 py-1.5 align-top font-mono text-[11px] text-tiffany-700">
473
499
  {p.slug}
474
500
  </td>
@@ -499,14 +525,18 @@ export default function MovieStudioTab({ projectId }: Props) {
499
525
  />
500
526
  </div>
501
527
  ) : (
502
- <span className="text-ink-300 text-xs">—</span>
528
+ <span className="text-ink-300 text-xs">
529
+
530
+ </span>
503
531
  )}
504
532
  </td>
505
533
  <td className="border border-ink-200 px-2 py-1.5 align-top">
506
534
  <EditableTextarea
507
535
  value={p.imagePrompt}
508
536
  onChange={(v) =>
509
- store.updatePlace(p.slug, { imagePrompt: v })
537
+ store.updatePlace(p.slug, {
538
+ imagePrompt: v,
539
+ })
510
540
  }
511
541
  rows={3}
512
542
  />
@@ -522,9 +552,47 @@ export default function MovieStudioTab({ projectId }: Props) {
522
552
 
523
553
  {/* Scenes */}
524
554
  <div>
525
- <h3 className="text-sm font-semibold text-ink-900 mb-2">
526
- Scenes
527
- </h3>
555
+ <div className="flex items-center justify-between mb-2">
556
+ <h3 className="text-sm font-semibold text-ink-900">Scenes</h3>
557
+ {store.videosRendering ? (
558
+ <span className="flex items-center gap-1.5 text-xs text-tiffany-600">
559
+ {SpinnerIcon}
560
+ {store.videoStatus ?? "Rendering..."}
561
+ </span>
562
+ ) : (
563
+ <button
564
+ onClick={() => store.renderVideos(projectId)}
565
+ className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-xl bg-tiffany-500 hover:bg-tiffany-600 text-ink-950 transition-colors"
566
+ >
567
+ {SparkleIcon}
568
+ Generate All Videos
569
+ </button>
570
+ )}
571
+ </div>
572
+
573
+ {store.videosRendering &&
574
+ store.videoProgress &&
575
+ store.videoProgress.total > 0 && (
576
+ <div className="flex items-center gap-3 mb-2 px-1">
577
+ <div className="flex-1 h-2 bg-ink-200 rounded-full overflow-hidden">
578
+ <div
579
+ className="h-full bg-tiffany-500 rounded-full transition-all duration-300"
580
+ style={{
581
+ width: `${(store.videoProgress.current / store.videoProgress.total) * 100}%`,
582
+ }}
583
+ />
584
+ </div>
585
+ <span className="text-xs font-semibold text-ink-700 tabular-nums whitespace-nowrap">
586
+ {store.videoProgress.current}/{store.videoProgress.total}
587
+ </span>
588
+ </div>
589
+ )}
590
+
591
+ {store.videosError && (
592
+ <div className="mb-2 p-3 bg-red-50 border border-red-200 rounded-xl text-red-600 text-xs">
593
+ {store.videosError}
594
+ </div>
595
+ )}
528
596
  {store.result.scenes.length === 0 ? (
529
597
  <p className="text-xs text-ink-500 italic py-3 border border-dashed border-ink-200 rounded-2xl text-center">
530
598
  No scenes found.
@@ -552,6 +620,7 @@ export default function MovieStudioTab({ projectId }: Props) {
552
620
  },
553
621
  { key: "place", label: "Place", className: "w-24" },
554
622
  { key: "image", label: "Image", className: "w-16" },
623
+ { key: "video", label: "Video", className: "w-40" },
555
624
  { key: "script", label: "Script", className: "w-60" },
556
625
  {
557
626
  key: "voiceover",
@@ -566,15 +635,26 @@ export default function MovieStudioTab({ projectId }: Props) {
566
635
  const url = imageUrlFor("scene", s.slug);
567
636
  const spinning =
568
637
  store.regeneratingSceneImages.includes(s.slug);
638
+ const video = store.videos.find(
639
+ (v) => v.slug === s.slug,
640
+ );
641
+ const videoUrl = resolveUrl(video?.url);
642
+ const videoSpinning =
643
+ store.regeneratingVideos.includes(s.slug);
569
644
  return (
570
- <tr key={s.slug} className="border-b border-ink-200">
645
+ <tr
646
+ key={s.slug}
647
+ className="border-b border-ink-200"
648
+ >
571
649
  <td className="border border-ink-200 px-2 py-1.5 align-top font-mono text-[11px] text-tiffany-700">
572
650
  {s.slug}
573
651
  </td>
574
652
  <td className="border border-ink-200 px-2 py-1.5 align-top">
575
653
  <EditableInput
576
654
  type="number"
577
- value={s.duration > 0 ? String(s.duration) : ""}
655
+ value={
656
+ s.duration > 0 ? String(s.duration) : ""
657
+ }
578
658
  onChange={(v) =>
579
659
  store.updateScene(s.slug, {
580
660
  duration: Number(v) || 0,
@@ -634,6 +714,30 @@ export default function MovieStudioTab({ projectId }: Props) {
634
714
  }
635
715
  />
636
716
  </div>
717
+ ) : (
718
+ <span className="text-ink-300 text-xs">
719
+
720
+ </span>
721
+ )}
722
+ </td>
723
+ <td className="border border-ink-200 px-2 py-1.5 align-top min-w-[180px]">
724
+ {videoUrl ? (
725
+ <div className="flex flex-col items-start">
726
+ <video
727
+ src={`${videoUrl}&t=${video?.updatedAt}`}
728
+ controls
729
+ className="w-40 rounded-lg border border-ink-200 cursor-pointer"
730
+ onClick={() =>
731
+ openPreview(videoUrl, s.slug, "video")
732
+ }
733
+ />
734
+ <RegenerateButton
735
+ spinning={videoSpinning}
736
+ onClick={() =>
737
+ store.regenerateVideo(projectId, s.slug)
738
+ }
739
+ />
740
+ </div>
637
741
  ) : (
638
742
  <span className="text-ink-300 text-xs">—</span>
639
743
  )}
@@ -191,9 +191,19 @@ const DOWNLOAD_MODELS: {
191
191
  desc: "Character, place & scene images",
192
192
  },
193
193
  {
194
- id: "qwen",
195
- name: "AbstractFramework/qwen-image-edit-2511-8bit",
196
- desc: "Image editing",
194
+ id: "ltx",
195
+ name: "dgrauet/ltx-2.3-mlx-q8",
196
+ desc: "Video generation",
197
+ },
198
+ {
199
+ id: "tts",
200
+ name: "Qwen/Qwen3-TTS-12Hz-1.7B-Base",
201
+ desc: "Voice-over & speech",
202
+ },
203
+ {
204
+ id: "gemma",
205
+ name: "mlx-community/gemma-4-e4b-it-8bit",
206
+ desc: "Story planning & dialogue (LLM)",
197
207
  },
198
208
  ];
199
209
 
@@ -228,7 +238,10 @@ export default function SetupAiModelTab() {
228
238
  <section>
229
239
  <h3 className="mb-2 text-sm font-semibold text-ink-900">Engines</h3>
230
240
  <div className="space-y-2">
231
- <ModelRow name="mlx-gen" desc="Image generation engine (z-image · flux · qwen)">
241
+ <ModelRow
242
+ name="mlx-gen"
243
+ desc="Image generation engine (z-image · flux)"
244
+ >
232
245
  <StatusBadge
233
246
  value={store.mlxgenInstalled}
234
247
  okText="Installed"
@@ -259,6 +272,22 @@ export default function SetupAiModelTab() {
259
272
  {store.downloading === "mlx-vlm" ? "Installing…" : "Install"}
260
273
  </button>
261
274
  </ModelRow>
275
+
276
+ <ModelRow name="huggingface-cli" desc="HF CLI for downloading models">
277
+ <StatusBadge
278
+ value={store.hfInstalled}
279
+ okText="Installed"
280
+ missingText="Not installed"
281
+ />
282
+ <button
283
+ onClick={() => store.installTool("hf-cli")}
284
+ disabled={busy}
285
+ className="flex items-center gap-1.5 whitespace-nowrap rounded-xl border border-tiffany-500 px-3 py-1.5 text-xs font-medium text-tiffany-700 transition-colors hover:bg-tiffany-50 disabled:opacity-50"
286
+ >
287
+ {store.downloading === "hf-cli" ? SpinnerIcon : InstallIcon}
288
+ {store.downloading === "hf-cli" ? "Installing…" : "Install"}
289
+ </button>
290
+ </ModelRow>
262
291
  </div>
263
292
  </section>
264
293
 
@@ -266,27 +295,17 @@ export default function SetupAiModelTab() {
266
295
  <section>
267
296
  <h3 className="mb-2 text-sm font-semibold text-ink-900">Models</h3>
268
297
  <div className="space-y-2">
269
- <ModelRow
270
- name="mlx-community/gemma-4-e2b-it-4bit"
271
- desc="Story planning & dialogue (LLM)"
272
- >
273
- <StatusBadge
274
- value={store.mlxVlmInstalled}
275
- okText="Ready"
276
- missingText="Not set up"
277
- />
278
- <span className="text-xs italic text-ink-400">
279
- Downloads on first server start
280
- </span>
281
- </ModelRow>
282
-
283
298
  {DOWNLOAD_MODELS.map((m) => {
284
299
  const downloaded =
285
300
  m.id === "z-image"
286
301
  ? store.zImageDownloaded
287
302
  : m.id === "flux"
288
303
  ? store.fluxDownloaded
289
- : store.qwenDownloaded;
304
+ : m.id === "ltx"
305
+ ? store.ltxDownloaded
306
+ : m.id === "gemma"
307
+ ? store.gemmaDownloaded
308
+ : store.ttsDownloaded;
290
309
  return (
291
310
  <ModelRow key={m.id} name={m.name} desc={m.desc}>
292
311
  <StatusBadge value={downloaded} />
@@ -301,24 +320,6 @@ export default function SetupAiModelTab() {
301
320
  </ModelRow>
302
321
  );
303
322
  })}
304
-
305
- <ModelRow
306
- name="dgrauet/ltx-2.3-mlx-q8"
307
- desc="Video generation"
308
- >
309
- <span className="text-xs italic text-ink-400">
310
- Downloads on first render
311
- </span>
312
- </ModelRow>
313
-
314
- <ModelRow
315
- name="Qwen/Qwen3-TTS-12Hz-1.7B-Base"
316
- desc="Voice-over & speech"
317
- >
318
- <span className="text-xs italic text-ink-400">
319
- Downloads on first use
320
- </span>
321
- </ModelRow>
322
323
  </div>
323
324
  </section>
324
325
 
@@ -126,9 +126,11 @@ function StatusBadge({ status }: { status: QueueTaskStatus }) {
126
126
 
127
127
  function TaskRow({
128
128
  task,
129
+ showProject = false,
129
130
  onCancel,
130
131
  }: {
131
132
  task: QueueTask;
133
+ showProject?: boolean;
132
134
  onCancel: () => void;
133
135
  }) {
134
136
  const active = task.status === "pending" || task.status === "running";
@@ -142,6 +144,11 @@ function TaskRow({
142
144
  <li className="flex flex-col gap-1.5 px-3 py-2 rounded-xl border border-ink-200 bg-white">
143
145
  <div className="flex items-center gap-2">
144
146
  <StatusBadge status={task.status} />
147
+ {showProject && task.projectId && (
148
+ <span className="px-1.5 py-0.5 rounded bg-ink-100 text-ink-500 text-[10px] font-mono whitespace-nowrap">
149
+ {task.projectId}
150
+ </span>
151
+ )}
145
152
  <span className="flex-1 text-sm font-medium text-ink-800 truncate">
146
153
  {task.label}
147
154
  </span>
@@ -179,19 +186,25 @@ function TaskRow({
179
186
 
180
187
  export default function TaskQueuePanel({ projectId }: Props) {
181
188
  const tasks = useQueueStore((s) => s.tasks);
189
+ const allTasks = useQueueStore((s) => s.allTasks);
190
+ const showAll = useQueueStore((s) => s.showAll);
182
191
  const paused = useQueueStore((s) => s.paused);
183
192
  const cancel = useQueueStore((s) => s.cancel);
184
193
  const pause = useQueueStore((s) => s.pause);
185
194
  const resume = useQueueStore((s) => s.resume);
186
195
  const clearFinished = useQueueStore((s) => s.clearFinished);
196
+ const setShowAll = useQueueStore((s) => s.setShowAll);
197
+
198
+ const displayTasks = showAll ? allTasks : tasks;
187
199
 
188
- if (tasks.length === 0 && !paused) return null;
200
+ if (!showAll && tasks.length === 0 && !paused) return null;
189
201
 
190
- const hasActive = tasks.some(
202
+ const hasActive = displayTasks.some(
191
203
  (t) => t.status === "pending" || t.status === "running",
192
204
  );
193
- const hasFinished = tasks.some(
194
- (t) => t.status !== "pending" && t.status !== "running" && t.status !== "paused",
205
+ const hasFinished = displayTasks.some(
206
+ (t) =>
207
+ t.status !== "pending" && t.status !== "running" && t.status !== "paused",
195
208
  );
196
209
 
197
210
  return (
@@ -199,9 +212,22 @@ export default function TaskQueuePanel({ projectId }: Props) {
199
212
  <div className="flex items-center gap-2">
200
213
  <span className="text-tiffany-600">{QueueIcon}</span>
201
214
  <h3 className="text-sm font-semibold text-ink-900">Generation Queue</h3>
202
- <span className="text-xs text-ink-500">{tasks.length}</span>
215
+ <span className="text-xs text-ink-500">{displayTasks.length}</span>
216
+
217
+ <button
218
+ onClick={() => setShowAll(!showAll)}
219
+ title={showAll ? "Show only this project" : "Show all projects"}
220
+ className={`flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium rounded-xl transition-colors ${
221
+ showAll
222
+ ? "bg-tiffany-500 text-ink-950 hover:bg-tiffany-600"
223
+ : "border border-ink-200 text-ink-600 hover:border-ink-300 hover:text-ink-900"
224
+ }`}
225
+ >
226
+ {showAll ? "All projects" : "This project"}
227
+ </button>
228
+
203
229
  <div className="ml-auto flex items-center gap-2">
204
- {paused ? (
230
+ {!showAll && paused ? (
205
231
  <button
206
232
  onClick={() => resume(projectId)}
207
233
  className="flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium rounded-xl bg-tiffany-500 hover:bg-tiffany-600 text-ink-950 transition-colors"
@@ -210,6 +236,7 @@ export default function TaskQueuePanel({ projectId }: Props) {
210
236
  Resume
211
237
  </button>
212
238
  ) : (
239
+ !showAll &&
213
240
  hasActive && (
214
241
  <button
215
242
  onClick={() => pause(projectId)}
@@ -220,7 +247,7 @@ export default function TaskQueuePanel({ projectId }: Props) {
220
247
  </button>
221
248
  )
222
249
  )}
223
- {hasFinished && (
250
+ {!showAll && hasFinished && (
224
251
  <button
225
252
  onClick={() => clearFinished(projectId)}
226
253
  className="flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium rounded-xl border border-ink-200 text-ink-600 hover:border-ink-300 hover:text-ink-900 transition-colors"
@@ -233,11 +260,12 @@ export default function TaskQueuePanel({ projectId }: Props) {
233
260
  </div>
234
261
 
235
262
  <ul className="flex flex-col gap-1.5">
236
- {tasks.map((task) => (
263
+ {displayTasks.map((task) => (
237
264
  <TaskRow
238
265
  key={task.id}
239
266
  task={task}
240
- onCancel={() => cancel(projectId, task.id)}
267
+ showProject={showAll}
268
+ onCancel={() => cancel(task.projectId ?? projectId, task.id)}
241
269
  />
242
270
  ))}
243
271
  </ul>
@@ -8,7 +8,6 @@ import MovieStudioTab from "./EditorTabs/MovieStudioTab";
8
8
  import GenerateVideoTab from "./EditorTabs/GenerateVideoTab";
9
9
  import AgentTab from "./EditorTabs/AgentTab";
10
10
  import StoryWriterTab from "./EditorTabs/StoryWriterTab";
11
- import CharactersTab from "./EditorTabs/CharactersTab";
12
11
  import ExtractImageTab from "./EditorTabs/ExtractImageTab";
13
12
  import SceneVisualTab from "./EditorTabs/SceneVisualTab";
14
13
  import TextToImageTab from "./EditorTabs/TextToImageTab";
@@ -173,22 +172,6 @@ export default function ProjectEditorPage() {
173
172
  </svg>
174
173
  );
175
174
 
176
- const CharacterIcon = (
177
- <svg
178
- width="18"
179
- height="18"
180
- viewBox="0 0 24 24"
181
- fill="none"
182
- stroke="currentColor"
183
- strokeWidth="2"
184
- strokeLinecap="round"
185
- strokeLinejoin="round"
186
- >
187
- <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
188
- <circle cx="12" cy="7" r="4" />
189
- </svg>
190
- );
191
-
192
175
  const ExtractIcon = (
193
176
  <svg
194
177
  width="18"
@@ -471,17 +454,6 @@ export default function ProjectEditorPage() {
471
454
  {StoryWriterIcon}
472
455
  Story Writer
473
456
  </button>
474
- <button
475
- onClick={() => store.setActiveTab("characters")}
476
- className={`flex items-center gap-2 px-5 py-2.5 text-sm font-medium rounded-2xl transition-all ${
477
- store.activeTab === "characters"
478
- ? "bg-tiffany-500/10 text-tiffany-700 shadow-glow-sm"
479
- : "text-ink-600 hover:bg-ink-100 hover:text-ink-800"
480
- }`}
481
- >
482
- {CharacterIcon}
483
- Characters
484
- </button>
485
457
  <button
486
458
  onClick={() => store.setActiveTab("extract")}
487
459
  className={`flex items-center gap-2 px-5 py-2.5 text-sm font-medium rounded-2xl transition-all ${
@@ -597,11 +569,6 @@ export default function ProjectEditorPage() {
597
569
  <StoryWriterTab projectId={id!} />
598
570
  )}
599
571
 
600
- {/* ========== CHARACTERS PANEL ========== */}
601
- {store.activeTab === "characters" && (
602
- <CharactersTab projectId={id!} />
603
- )}
604
-
605
572
  {/* ========== EXTRACT IMAGE PANEL ========== */}
606
573
  {store.activeTab === "extract" && <ExtractImageTab projectId={id!} />}
607
574
 
@@ -168,7 +168,7 @@ export default function ProjectManager() {
168
168
  <header className="flex flex-col gap-7 md:flex-row md:items-end md:justify-between">
169
169
  <div className="max-w-xl">
170
170
  <p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-tiffany-600">
171
- Lambobo Studio
171
+ EffectNode Media
172
172
  </p>
173
173
  <h1 className="mt-3 font-display text-4xl font-light leading-[1.06] text-ink-900 md:text-5xl">
174
174
  Make the movies{" "}
@@ -1,18 +1,20 @@
1
1
  @import "tailwindcss";
2
2
 
3
3
  /* ==========================================================================
4
- Lambobo Studio — design tokens
4
+ EffectNode Media — design tokens
5
5
  A dreamy, tiffany-blue film studio. Gradients read like light through
6
6
  shallow water: tiffany teal → aqua → periwinkle → blush.
7
7
  ========================================================================== */
8
8
 
9
9
  @theme {
10
10
  /* ---- Type ---- */
11
- --font-sans: "Avenir Next", Avenir, "Helvetica Neue", -apple-system,
12
- BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
13
- --font-display: "Cormorant", "Iowan Old Style", "Baskerville", "Georgia",
14
- serif;
15
- --font-mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas,
11
+ --font-sans:
12
+ "Avenir Next", Avenir, "Helvetica Neue", -apple-system, BlinkMacSystemFont,
13
+ "Segoe UI", Roboto, sans-serif;
14
+ --font-display:
15
+ "Cormorant", "Iowan Old Style", "Baskerville", "Georgia", serif;
16
+ --font-mono:
17
+ ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas,
16
18
  "Liberation Mono", monospace;
17
19
 
18
20
  /* ---- Tiffany (the brand) ---- */
@@ -48,13 +50,14 @@
48
50
  --color-dream-mint: #c8f1e6;
49
51
 
50
52
  /* ---- Shadows ---- */
51
- --shadow-glow: 0 8px 30px -6px rgba(10, 186, 181, 0.55),
53
+ --shadow-glow:
54
+ 0 8px 30px -6px rgba(10, 186, 181, 0.55),
52
55
  0 2px 10px -2px rgba(10, 186, 181, 0.4);
53
56
  --shadow-glow-sm: 0 0 14px -2px rgba(10, 186, 181, 0.45);
54
- --shadow-card: 0 1px 2px rgba(13, 25, 28, 0.04),
55
- 0 10px 30px -12px rgba(13, 25, 28, 0.16);
56
- --shadow-card-hover: 0 2px 4px rgba(13, 25, 28, 0.05),
57
- 0 18px 44px -14px rgba(10, 186, 181, 0.35);
57
+ --shadow-card:
58
+ 0 1px 2px rgba(13, 25, 28, 0.04), 0 10px 30px -12px rgba(13, 25, 28, 0.16);
59
+ --shadow-card-hover:
60
+ 0 2px 4px rgba(13, 25, 28, 0.05), 0 18px 44px -14px rgba(10, 186, 181, 0.35);
58
61
  --shadow-modal: 0 24px 80px -16px rgba(13, 25, 28, 0.35);
59
62
  }
60
63
 
@@ -65,16 +68,28 @@
65
68
  body {
66
69
  margin: 0;
67
70
  background-color: #f3f7f7;
68
- background-image: radial-gradient(
71
+ background-image:
72
+ radial-gradient(
69
73
  120% 90% at 85% -10%,
70
74
  rgba(183, 225, 251, 0.5),
71
75
  transparent 60%
72
76
  ),
73
- radial-gradient(100% 80% at 0% 0%, rgba(255, 214, 234, 0.4), transparent 55%);
77
+ radial-gradient(
78
+ 100% 80% at 0% 0%,
79
+ rgba(255, 214, 234, 0.4),
80
+ transparent 55%
81
+ );
74
82
  background-attachment: fixed;
75
83
  color: #283a40;
76
- font-family: "Avenir Next", Avenir, "Helvetica Neue", -apple-system,
77
- BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
84
+ font-family:
85
+ "Avenir Next",
86
+ Avenir,
87
+ "Helvetica Neue",
88
+ -apple-system,
89
+ BlinkMacSystemFont,
90
+ "Segoe UI",
91
+ Roboto,
92
+ sans-serif;
78
93
  -webkit-font-smoothing: antialiased;
79
94
  -moz-osx-font-smoothing: grayscale;
80
95
  }
@@ -82,7 +97,8 @@ body {
82
97
  code,
83
98
  pre,
84
99
  .font-mono {
85
- font-family: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas,
100
+ font-family:
101
+ ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas,
86
102
  "Liberation Mono", monospace;
87
103
  }
88
104
 
@@ -126,7 +142,8 @@ pre,
126
142
  z-index: 0;
127
143
  overflow: hidden;
128
144
  pointer-events: none;
129
- background: radial-gradient(
145
+ background:
146
+ radial-gradient(
130
147
  120% 90% at 85% -10%,
131
148
  rgba(183, 225, 251, 0.55),
132
149
  transparent 60%
@@ -251,7 +268,12 @@ pre,
251
268
  }
252
269
 
253
270
  .thumb {
254
- background-image: linear-gradient(135deg, #b0e8e3 0%, #81d8d0 50%, #c3b4f4 100%);
271
+ background-image: linear-gradient(
272
+ 135deg,
273
+ #b0e8e3 0%,
274
+ #81d8d0 50%,
275
+ #c3b4f4 100%
276
+ );
255
277
  }
256
278
 
257
279
  .btn-primary {
@@ -263,12 +285,16 @@ pre,
263
285
  );
264
286
  color: #ffffff;
265
287
  box-shadow: var(--shadow-glow);
266
- transition: filter 150ms ease, box-shadow 150ms ease, transform 150ms ease;
288
+ transition:
289
+ filter 150ms ease,
290
+ box-shadow 150ms ease,
291
+ transform 150ms ease;
267
292
  }
268
293
 
269
294
  .btn-primary:hover {
270
295
  filter: brightness(1.05);
271
- box-shadow: 0 10px 34px -6px rgba(10, 186, 181, 0.6),
296
+ box-shadow:
297
+ 0 10px 34px -6px rgba(10, 186, 181, 0.6),
272
298
  0 4px 12px -4px rgba(10, 186, 181, 0.45);
273
299
  }
274
300
 
@@ -5,7 +5,7 @@
5
5
  <meta charset="UTF-8" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <meta name="color-scheme" content="light" />
8
- <title>Lambobo Studio</title>
8
+ <title>EffectNode Media</title>
9
9
  </head>
10
10
 
11
11
  <body>