github-actions[bot] commited on
Commit
0b16c0f
·
1 Parent(s): 0ea274e

deploy: switch to chatterbox requirements @ 219b12b

Browse files
Files changed (6) hide show
  1. .gitattributes +3 -0
  2. .gitignore +1 -0
  3. frontend/app.js +0 -1318
  4. frontend/index.html +0 -1089
  5. frontend/style.css +0 -3469
  6. server.py +5 -2
.gitattributes CHANGED
@@ -9,3 +9,6 @@ deploy.sh export-ignore
9
  Dockerfile export-ignore
10
  .dockerignore export-ignore
11
  social_media_distributor/ export-ignore
 
 
 
 
9
  Dockerfile export-ignore
10
  .dockerignore export-ignore
11
  social_media_distributor/ export-ignore
12
+ frontend/ export-ignore
13
+ batch_translate.py export-ignore
14
+ client_insta_links.jsonl export-ignore
.gitignore CHANGED
@@ -23,6 +23,7 @@ tmp/
23
  uploads/
24
  outputs/
25
  data/
 
26
  # Subproject runtime artifacts (not for HF Space)
27
  social_distributor/.venv/
28
  social_distributor/poster/auth/storage/
 
23
  uploads/
24
  outputs/
25
  data/
26
+ batch_outputs/
27
  # Subproject runtime artifacts (not for HF Space)
28
  social_distributor/.venv/
29
  social_distributor/poster/auth/storage/
frontend/app.js DELETED
@@ -1,1318 +0,0 @@
1
- /* ═══════════════════════════════════════════════════════
2
- app.js — VideoVoice Frontend Application
3
- ═══════════════════════════════════════════════════════ */
4
-
5
- // ── Configuration ───────────────────────────────────────
6
- const API_BASE = window.location.origin;
7
-
8
- // ── DOM Cache ───────────────────────────────────────────
9
- const $ = (sel) => document.querySelector(sel);
10
- const $$ = (sel) => [...document.querySelectorAll(sel)];
11
-
12
- // ── Scroll-triggered Reveal Animations ──────────────────
13
- const revealObserver = new IntersectionObserver(
14
- (entries) => {
15
- entries.forEach((entry) => {
16
- if (entry.isIntersecting) {
17
- const siblings = [...entry.target.parentElement.querySelectorAll('.reveal:not(.visible)')];
18
- const delay = siblings.indexOf(entry.target) * 80;
19
- setTimeout(() => entry.target.classList.add('visible'), Math.max(delay, 0));
20
- revealObserver.unobserve(entry.target);
21
- }
22
- });
23
- },
24
- { threshold: 0.1 }
25
- );
26
- $$('.reveal').forEach((el) => revealObserver.observe(el));
27
-
28
- // ── Nav scroll effect ───────────────────────────────────
29
- const nav = $('#navbar');
30
- let lastScroll = 0;
31
- window.addEventListener('scroll', () => {
32
- const scrollY = window.scrollY;
33
- if (scrollY > 50) {
34
- nav.classList.add('scrolled');
35
- } else {
36
- nav.classList.remove('scrolled');
37
- }
38
- lastScroll = scrollY;
39
- }, { passive: true });
40
-
41
- // ── Mobile menu ─────────────────────────────────────────
42
- const mobileBtn = $('#mobile-menu-btn');
43
- const mobileNav = $('#mobile-nav');
44
- if (mobileBtn) {
45
- mobileBtn.addEventListener('click', () => {
46
- mobileBtn.classList.toggle('active');
47
- mobileNav.classList.toggle('open');
48
- });
49
- // Close on link click
50
- $$('#mobile-nav a').forEach(a => {
51
- a.addEventListener('click', () => {
52
- mobileBtn.classList.remove('active');
53
- mobileNav.classList.remove('open');
54
- });
55
- });
56
- }
57
-
58
- // ── Active nav highlight ────────────────────────────────
59
- const sections = $$('section[id]');
60
- const navLinks = $$('.nav-links a:not(.btn)');
61
- const navObserver = new IntersectionObserver(
62
- (entries) => {
63
- entries.forEach((entry) => {
64
- if (entry.isIntersecting) {
65
- navLinks.forEach((link) => {
66
- const isActive = link.getAttribute('href') === `#${entry.target.id}`;
67
- link.style.color = isActive ? 'var(--accent)' : '';
68
- });
69
- }
70
- });
71
- },
72
- { threshold: 0.3 }
73
- );
74
- sections.forEach((s) => navObserver.observe(s));
75
-
76
- // ════════════════════════════════════════════════════════
77
- // SHOWCASE (before / after comparison rows)
78
- // ════════════════════════════════════════════════════════
79
- const showcaseContainer = $('#showcase-container');
80
- const showcaseLoading = $('#showcase-loading');
81
- const showcaseError = $('#showcase-error');
82
- const showcaseRows = $('#showcase-rows');
83
-
84
- // Fallback masonry elements
85
- const demoVideosWall = $('#demo-videos-wall');
86
- const demoVideosLoading = $('#demo-videos-loading');
87
- const demoVideosError = $('#demo-videos-error');
88
- const demoVideosEmpty = $('#demo-videos-empty');
89
- const demoVideosLanes = $('#demo-videos-lanes');
90
-
91
- if (showcaseContainer) {
92
- loadShowcase();
93
- }
94
-
95
- async function loadShowcase() {
96
- setShowcaseState('loading');
97
-
98
- try {
99
- const res = await fetch(`${API_BASE}/api/showcase`);
100
- if (!res.ok) throw new Error('Failed to load showcase');
101
-
102
- const payload = await res.json();
103
- const showcases = Array.isArray(payload?.showcases) ? payload.showcases : [];
104
-
105
- if (showcases.length === 0) {
106
- // Fall back to old masonry grid
107
- setShowcaseState('hidden');
108
- loadDemoVideosFallback();
109
- return;
110
- }
111
-
112
- renderShowcaseRows(showcases);
113
- setShowcaseState('ready');
114
- } catch (err) {
115
- console.error('Showcase load failed, falling back:', err);
116
- setShowcaseState('hidden');
117
- loadDemoVideosFallback();
118
- }
119
- }
120
-
121
- function setShowcaseState(state) {
122
- if (showcaseLoading) showcaseLoading.hidden = true;
123
- if (showcaseError) showcaseError.hidden = true;
124
- if (showcaseRows) showcaseRows.hidden = state !== 'ready';
125
-
126
- if (state === 'loading' && showcaseLoading) showcaseLoading.hidden = false;
127
- if (state === 'error' && showcaseError) showcaseError.hidden = false;
128
- if (state === 'hidden' && showcaseContainer) showcaseContainer.hidden = true;
129
- }
130
-
131
- // ── Platform icon SVGs ────────────────────────
132
- const PLATFORM_ICONS = {
133
- youtube: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.5 6.19a3.02 3.02 0 0 0-2.12-2.14C19.5 3.5 12 3.5 12 3.5s-7.5 0-9.38.55A3.02 3.02 0 0 0 .5 6.19 31.6 31.6 0 0 0 0 12a31.6 31.6 0 0 0 .5 5.81 3.02 3.02 0 0 0 2.12 2.14c1.88.55 9.38.55 9.38.55s7.5 0 9.38-.55a3.02 3.02 0 0 0 2.12-2.14A31.6 31.6 0 0 0 24 12a31.6 31.6 0 0 0-.5-5.81zM9.75 15.02V8.98L15.5 12l-5.75 3.02z"/></svg>`,
134
- instagram: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.16c3.2 0 3.58.01 4.85.07 1.17.05 1.97.24 2.44.41a4.08 4.08 0 0 1 1.52.99c.47.47.77.93.99 1.52.17.47.36 1.27.41 2.44.06 1.27.07 1.65.07 4.85s-.01 3.58-.07 4.85c-.05 1.17-.24 1.97-.41 2.44a4.08 4.08 0 0 1-.99 1.52 4.08 4.08 0 0 1-1.52.99c-.47.17-1.27.36-2.44.41-1.27.06-1.65.07-4.85.07s-3.58-.01-4.85-.07c-1.17-.05-1.97-.24-2.44-.41a4.08 4.08 0 0 1-1.52-.99 4.08 4.08 0 0 1-.99-1.52c-.17-.47-.36-1.27-.41-2.44C2.17 15.58 2.16 15.2 2.16 12s.01-3.58.07-4.85c.05-1.17.24-1.97.41-2.44a4.08 4.08 0 0 1 .99-1.52 4.08 4.08 0 0 1 1.52-.99c.47-.17 1.27-.36 2.44-.41C8.42 2.17 8.8 2.16 12 2.16zM12 0C8.74 0 8.33.01 7.05.07 5.78.13 4.9.33 4.14.63a5.87 5.87 0 0 0-2.13 1.38A5.87 5.87 0 0 0 .63 4.14C.33 4.9.13 5.78.07 7.05.01 8.33 0 8.74 0 12s.01 3.67.07 4.95c.06 1.27.26 2.15.56 2.91a5.87 5.87 0 0 0 1.38 2.13 5.87 5.87 0 0 0 2.13 1.38c.76.3 1.64.5 2.91.56C8.33 23.99 8.74 24 12 24s3.67-.01 4.95-.07c1.27-.06 2.15-.26 2.91-.56a5.87 5.87 0 0 0 2.13-1.38 5.87 5.87 0 0 0 1.38-2.13c.3-.76.5-1.64.56-2.91.06-1.28.07-1.69.07-4.95s-.01-3.67-.07-4.95c-.06-1.27-.26-2.15-.56-2.91a5.87 5.87 0 0 0-1.38-2.13A5.87 5.87 0 0 0 19.86.63C19.1.33 18.22.13 16.95.07 15.67.01 15.26 0 12 0zm0 5.84a6.16 6.16 0 1 0 0 12.32 6.16 6.16 0 0 0 0-12.32zM12 16a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm7.85-10.4a1.44 1.44 0 1 0-2.88 0 1.44 1.44 0 0 0 2.88 0z"/></svg>`,
135
- };
136
-
137
- function renderShowcaseRows(showcases) {
138
- if (!showcaseRows) return;
139
- showcaseRows.innerHTML = '';
140
-
141
- showcases.forEach((entry) => {
142
- const row = document.createElement('div');
143
- row.className = 'showcase-row reveal';
144
-
145
- // ── Row header ──
146
- const header = document.createElement('div');
147
- header.className = 'showcase-row-header';
148
-
149
- const iconWrap = document.createElement('div');
150
- iconWrap.className = `showcase-platform-icon showcase-platform-icon--${entry.platform || 'youtube'}`;
151
- iconWrap.innerHTML = PLATFORM_ICONS[entry.platform] || PLATFORM_ICONS.youtube;
152
-
153
- const info = document.createElement('div');
154
- info.className = 'showcase-row-info';
155
- info.innerHTML = `<div class="showcase-row-title">${escapeHtml(entry.title || '')}</div>
156
- <div class="showcase-row-desc">${escapeHtml(entry.description || '')}</div>`;
157
-
158
- header.appendChild(iconWrap);
159
- header.appendChild(info);
160
- row.appendChild(header);
161
-
162
- // ── Mobile tabs ──
163
- const allCards = [];
164
- const tabs = document.createElement('div');
165
- tabs.className = 'showcase-tabs';
166
-
167
- // ── Cards flow: Original → Their Dub → Our Dub ──
168
- const flow = document.createElement('div');
169
- flow.className = 'showcase-flow';
170
-
171
- // Original card
172
- const original = entry.original || {};
173
- const origCard = createShowcaseCard({
174
- type: original.type,
175
- embed_url: original.embed_url,
176
- language: original.language || 'Original',
177
- cardStyle: 'original',
178
- });
179
- allCards.push({ card: origCard, label: 'Original' });
180
- flow.appendChild(origCard);
181
-
182
- // Their dub card (YouTube/Insta auto-dub)
183
- const theirDub = entry.their_dub || {};
184
- if (theirDub.url || theirDub.filename) {
185
- const theirCard = createShowcaseCard({
186
- type: 'local',
187
- url: theirDub.url,
188
- language: theirDub.language || 'Their Dub',
189
- cardStyle: 'their-dub',
190
- });
191
- allCards.push({ card: theirCard, label: theirDub.language || 'Their Dub' });
192
- flow.appendChild(theirCard);
193
- }
194
-
195
- // Our dub card (VideoVoice)
196
- const ourDub = entry.our_dub || {};
197
- if (ourDub.url || ourDub.filename) {
198
- const ourCard = createShowcaseCard({
199
- type: 'local',
200
- url: ourDub.url,
201
- language: ourDub.language || 'Our Dub',
202
- cardStyle: 'our-dub',
203
- });
204
- allCards.push({ card: ourCard, label: ourDub.language || 'Our Dub' });
205
- flow.appendChild(ourCard);
206
- }
207
-
208
- // Build tab buttons
209
- allCards.forEach((item, idx) => {
210
- const tab = document.createElement('button');
211
- tab.className = 'showcase-tab' + (idx === 0 ? ' showcase-tab--active' : '');
212
- tab.textContent = item.label;
213
- tab.addEventListener('click', () => {
214
- tabs.querySelectorAll('.showcase-tab').forEach((t) => t.classList.remove('showcase-tab--active'));
215
- tab.classList.add('showcase-tab--active');
216
- allCards.forEach((c) => c.card.classList.remove('showcase-card--active'));
217
- item.card.classList.add('showcase-card--active');
218
- });
219
- tabs.appendChild(tab);
220
- });
221
-
222
- // First card is active by default on mobile
223
- allCards[0].card.classList.add('showcase-card--active');
224
-
225
- row.appendChild(tabs);
226
- row.appendChild(flow);
227
-
228
- // Playback sync within row
229
- setupShowcasePlaybackSync(flow);
230
-
231
- showcaseRows.appendChild(row);
232
- });
233
-
234
- // Re-observe new reveal elements
235
- showcaseRows.querySelectorAll('.reveal').forEach((el) => {
236
- revealObserver.observe(el);
237
- });
238
- }
239
-
240
- function createShowcaseCard({ type, embed_url, url, language, cardStyle }) {
241
- const card = document.createElement('article');
242
- card.className = `showcase-card showcase-card--${cardStyle}`;
243
-
244
- // Label pill
245
- const label = document.createElement('div');
246
- label.className = `showcase-label showcase-label--${cardStyle}`;
247
- label.textContent = language;
248
- card.appendChild(label);
249
-
250
- // Video / embed frame
251
- const frame = document.createElement('div');
252
- frame.className = 'showcase-video-frame';
253
-
254
- if ((type === 'youtube' || type === 'instagram') && embed_url) {
255
- const iframe = document.createElement('iframe');
256
- iframe.src = embed_url;
257
- iframe.allow = 'autoplay; encrypted-media';
258
- iframe.allowFullscreen = true;
259
- iframe.loading = 'lazy';
260
- iframe.title = `${language} - Original`;
261
- frame.appendChild(iframe);
262
- } else {
263
- const video = document.createElement('video');
264
- video.className = 'showcase-video';
265
- video.controls = true;
266
- video.playsInline = true;
267
- video.preload = 'none';
268
- video.setAttribute('playsinline', '');
269
- if (url) video.src = url;
270
- frame.appendChild(video);
271
- }
272
-
273
- card.appendChild(frame);
274
-
275
- // Info
276
- const info = document.createElement('div');
277
- info.className = 'showcase-card-info';
278
- const title = document.createElement('div');
279
- title.className = 'showcase-card-title';
280
- title.textContent = language;
281
- info.appendChild(title);
282
- card.appendChild(info);
283
-
284
- return card;
285
- }
286
-
287
- function setupShowcasePlaybackSync(container) {
288
- if (!container) return;
289
- container.addEventListener(
290
- 'play',
291
- (event) => {
292
- const current = event.target;
293
- if (!(current instanceof HTMLVideoElement)) return;
294
- container.querySelectorAll('.showcase-video').forEach((v) => {
295
- if (v !== current) v.pause();
296
- });
297
- },
298
- true
299
- );
300
- }
301
-
302
- function escapeHtml(str) {
303
- const div = document.createElement('div');
304
- div.textContent = str;
305
- return div.innerHTML;
306
- }
307
-
308
- // ════════════════════════════════════════════════════════
309
- // DEMO VIDEOS WALL — fallback (outputs/ + data)
310
- // ════════════════════════════════════════════════════════
311
- function loadDemoVideosFallback() {
312
- if (!demoVideosWall || !demoVideosLanes) return;
313
- demoVideosWall.hidden = false;
314
- setDemoState('loading');
315
-
316
- fetch(`${API_BASE}/api/demo-videos`)
317
- .then((res) => {
318
- if (!res.ok) throw new Error('Failed to load demo videos');
319
- return res.json();
320
- })
321
- .then((payload) => {
322
- const videos = Array.isArray(payload?.videos) ? payload.videos : [];
323
- if (videos.length === 0) {
324
- setDemoState('empty');
325
- return;
326
- }
327
- renderDemoVideos(videos);
328
- setDemoState('ready');
329
- setupDemoPlaybackSync(demoVideosLanes);
330
- })
331
- .catch((err) => {
332
- console.error(err);
333
- setDemoState('error');
334
- });
335
- }
336
-
337
- function setDemoState(state) {
338
- if (!demoVideosLanes) return;
339
- [demoVideosLoading, demoVideosError, demoVideosEmpty].forEach((el) => {
340
- if (el) el.hidden = true;
341
- });
342
- demoVideosLanes.hidden = state !== 'ready';
343
-
344
- if (state === 'loading' && demoVideosLoading) demoVideosLoading.hidden = false;
345
- if (state === 'error' && demoVideosError) demoVideosError.hidden = false;
346
- if (state === 'empty' && demoVideosEmpty) demoVideosEmpty.hidden = false;
347
- }
348
-
349
- function renderDemoVideos(videos) {
350
- const lanes = [...demoVideosLanes.querySelectorAll('.demos-lane')];
351
- if (lanes.length === 0) return;
352
-
353
- lanes.forEach((lane) => (lane.innerHTML = ''));
354
-
355
- videos.forEach((video, idx) => {
356
- const lane = lanes[idx % lanes.length];
357
- lane.appendChild(createDemoCard(video));
358
- });
359
- }
360
-
361
- function createDemoCard(videoData) {
362
- const card = document.createElement('article');
363
- card.className = 'demo-card';
364
-
365
- const frame = document.createElement('div');
366
- frame.className = 'demo-video-frame';
367
-
368
- const video = document.createElement('video');
369
- video.className = 'demo-video';
370
- video.controls = true;
371
- video.playsInline = true;
372
- video.preload = 'metadata';
373
- video.src = videoData.url;
374
- video.setAttribute('playsinline', '');
375
-
376
- const badge = document.createElement('div');
377
- badge.className = 'demo-badge';
378
- badge.textContent = (videoData.folder || 'demo').toUpperCase();
379
-
380
- const info = document.createElement('div');
381
- info.className = 'demo-info';
382
-
383
- const title = document.createElement('h4');
384
- title.textContent = formatDemoTitle(videoData.name || 'Video');
385
-
386
- const meta = document.createElement('p');
387
- meta.className = 'demo-meta';
388
- meta.textContent = `${videoData.folder || 'demo'} • ${formatBytes(videoData.size_bytes)} • ${formatDemoDate(videoData.modified_at)}`;
389
-
390
- frame.appendChild(video);
391
- frame.appendChild(badge);
392
- info.appendChild(title);
393
- info.appendChild(meta);
394
- card.appendChild(frame);
395
- card.appendChild(info);
396
- return card;
397
- }
398
-
399
- function formatDemoTitle(fileName) {
400
- const withoutExt = String(fileName).replace(/\.[^/.]+$/, '');
401
- const normalized = withoutExt.replace(/[_-]+/g, ' ').replace(/\s+/g, ' ').trim();
402
- return normalized || String(fileName);
403
- }
404
-
405
- function formatBytes(bytes) {
406
- const size = Number(bytes);
407
- if (!Number.isFinite(size) || size <= 0) return 'Unknown size';
408
- const units = ['B', 'KB', 'MB', 'GB'];
409
- let value = size;
410
- let unitIdx = 0;
411
- while (value >= 1024 && unitIdx < units.length - 1) {
412
- value /= 1024;
413
- unitIdx += 1;
414
- }
415
- return `${value.toFixed(unitIdx === 0 ? 0 : 1)} ${units[unitIdx]}`;
416
- }
417
-
418
- function formatDemoDate(modifiedAt) {
419
- const raw = Number(modifiedAt);
420
- if (!Number.isFinite(raw) || raw <= 0) return 'Unknown date';
421
- const ms = raw > 1e12 ? raw : raw * 1000;
422
- const date = new Date(ms);
423
- if (Number.isNaN(date.getTime())) return 'Unknown date';
424
- return date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
425
- }
426
-
427
- function setupDemoPlaybackSync(container) {
428
- if (!container || container.dataset.playbackSync === 'ready') return;
429
- container.dataset.playbackSync = 'ready';
430
-
431
- container.addEventListener(
432
- 'play',
433
- (event) => {
434
- const currentVideo = event.target;
435
- if (!(currentVideo instanceof HTMLVideoElement) || !currentVideo.classList.contains('demo-video')) {
436
- return;
437
- }
438
- container.querySelectorAll('.demo-video').forEach((video) => {
439
- if (video !== currentVideo) video.pause();
440
- });
441
- },
442
- true
443
- );
444
- }
445
-
446
- // ════════════════════════════════════════════════════════
447
- // TAB SWITCHING (URL / Upload)
448
- // ════════════════════════════════════════════════════════
449
- const tabBtns = $$('.tab-btn');
450
- const tabContents = $$('.tab-content');
451
-
452
- tabBtns.forEach(btn => {
453
- btn.addEventListener('click', () => {
454
- const target = btn.dataset.tab;
455
- tabBtns.forEach(b => b.classList.remove('active'));
456
- tabContents.forEach(c => c.classList.remove('active'));
457
- btn.classList.add('active');
458
- $(`#content-${target}`).classList.add('active');
459
- });
460
- });
461
-
462
- // ════════════════════════════════════════════════════════
463
- // DRAG & DROP FILE UPLOAD
464
- // ════════════════════════════════════════════════════════
465
- const dropZone = $('#drop-zone');
466
- const fileInput = $('#file-input');
467
- const filePreview = $('#file-preview');
468
- const fileName = $('#file-name');
469
- const fileRemove = $('#file-remove');
470
- let selectedFile = null;
471
-
472
- if (dropZone) {
473
- // Click to browse
474
- dropZone.addEventListener('click', (e) => {
475
- if (e.target === fileRemove || fileRemove.contains(e.target)) return;
476
- if (!selectedFile) fileInput.click();
477
- });
478
-
479
- // Drag events
480
- ['dragenter', 'dragover'].forEach(evt => {
481
- dropZone.addEventListener(evt, (e) => {
482
- e.preventDefault();
483
- dropZone.classList.add('drag-over');
484
- });
485
- });
486
- ['dragleave', 'drop'].forEach(evt => {
487
- dropZone.addEventListener(evt, (e) => {
488
- e.preventDefault();
489
- dropZone.classList.remove('drag-over');
490
- });
491
- });
492
-
493
- dropZone.addEventListener('drop', (e) => {
494
- const file = e.dataTransfer.files[0];
495
- if (file) handleFileSelect(file);
496
- });
497
-
498
- fileInput.addEventListener('change', () => {
499
- if (fileInput.files[0]) handleFileSelect(fileInput.files[0]);
500
- });
501
-
502
- fileRemove.addEventListener('click', (e) => {
503
- e.stopPropagation();
504
- clearFile();
505
- });
506
- }
507
-
508
- function handleFileSelect(file) {
509
- // Validate type
510
- const validTypes = ['video/mp4', 'video/quicktime', 'video/webm'];
511
- if (!validTypes.includes(file.type)) {
512
- showToast('Please upload an MP4, MOV, or WebM file.', 'error');
513
- return;
514
- }
515
- // (Upload size validation removed)
516
-
517
- selectedFile = file;
518
- fileName.textContent = `${file.name} (${(file.size / 1024 / 1024).toFixed(1)}MB)`;
519
- dropZone.querySelector('.drop-zone-content').style.display = 'none';
520
- filePreview.style.display = 'flex';
521
-
522
- // Show video preview
523
- showUploadPreview(file);
524
- }
525
-
526
- function clearFile() {
527
- selectedFile = null;
528
- fileInput.value = '';
529
- dropZone.querySelector('.drop-zone-content').style.display = '';
530
- filePreview.style.display = 'none';
531
- hideUploadPreview();
532
- }
533
-
534
- // ════════════════════════════════════════════════════════
535
- // VIDEO PREVIEWS
536
- // ════════════════════════════════════════════════════════
537
- let uploadObjectUrl = null;
538
-
539
- function showUploadPreview(file) {
540
- const preview = $('#upload-preview');
541
- const video = $('#upload-preview-video');
542
- if (!preview || !video) return;
543
-
544
- // Revoke previous object URL
545
- if (uploadObjectUrl) URL.revokeObjectURL(uploadObjectUrl);
546
- uploadObjectUrl = URL.createObjectURL(file);
547
-
548
- video.src = uploadObjectUrl;
549
- video.play().catch(() => {});
550
- preview.style.display = '';
551
- }
552
-
553
- function hideUploadPreview() {
554
- const preview = $('#upload-preview');
555
- const video = $('#upload-preview-video');
556
- if (!preview || !video) return;
557
-
558
- video.pause();
559
- video.removeAttribute('src');
560
- video.load();
561
- if (uploadObjectUrl) {
562
- URL.revokeObjectURL(uploadObjectUrl);
563
- uploadObjectUrl = null;
564
- }
565
- preview.style.display = 'none';
566
- }
567
-
568
- // Upload preview remove button
569
- const uploadPreviewRemove = $('#upload-preview-remove');
570
- if (uploadPreviewRemove) {
571
- uploadPreviewRemove.addEventListener('click', () => clearFile());
572
- }
573
-
574
- function detectPlatform(url) {
575
- if (/instagram\.com|instagr\.am/i.test(url)) return { name: 'Instagram Reel', icon: '&#9654;', cls: 'ig', emoji: '📸' };
576
- if (/youtube\.com\/shorts/i.test(url)) return { name: 'YouTube Short', icon: '&#9654;', cls: 'yt', emoji: '▶' };
577
- if (/tiktok\.com/i.test(url)) return { name: 'TikTok', icon: '♪', cls: 'tt', emoji: '♪' };
578
- if (/youtube\.com|youtu\.be/i.test(url)) return { name: 'YouTube', icon: '&#9654;', cls: 'yt', emoji: '▶' };
579
- return { name: 'Video URL', icon: '🔗', cls: 'generic', emoji: '🔗' };
580
- }
581
-
582
- function showUrlPreview(url) {
583
- const preview = $('#url-preview');
584
- if (!preview) return;
585
-
586
- const platform = detectPlatform(url);
587
- const iconEl = $('#url-preview-icon');
588
- const platformEl = $('#url-preview-platform');
589
- const linkEl = $('#url-preview-link');
590
-
591
- iconEl.className = 'url-preview-icon ' + platform.cls;
592
- iconEl.innerHTML = platform.emoji;
593
- platformEl.textContent = platform.name + ' detected';
594
- linkEl.textContent = url;
595
-
596
- preview.style.display = '';
597
- }
598
-
599
- function hideUrlPreview() {
600
- const preview = $('#url-preview');
601
- if (!preview) return;
602
- preview.style.display = 'none';
603
- }
604
-
605
- // URL preview remove button
606
- const urlPreviewRemove = $('#url-preview-remove');
607
- if (urlPreviewRemove) {
608
- urlPreviewRemove.addEventListener('click', () => {
609
- $('#video-url').value = '';
610
- hideUrlPreview();
611
- });
612
- }
613
-
614
- // Show URL preview on paste / input
615
- const videoUrlInput = $('#video-url');
616
- if (videoUrlInput) {
617
- let urlDebounce = null;
618
- videoUrlInput.addEventListener('input', () => {
619
- clearTimeout(urlDebounce);
620
- const url = videoUrlInput.value.trim();
621
- if (!url) {
622
- hideUrlPreview();
623
- return;
624
- }
625
- urlDebounce = setTimeout(() => {
626
- try { new URL(url); showUrlPreview(url); } catch { hideUrlPreview(); }
627
- }, 400);
628
- });
629
- }
630
-
631
- // ════════════════════════════════════════════════════════
632
- // LANGUAGE SWAP
633
- // ════════════════════════════════════════════════════════
634
- const langSwap = $('#lang-swap');
635
- if (langSwap) {
636
- langSwap.addEventListener('click', () => {
637
- const source = $('#source-lang');
638
- const target = $('#target-lang');
639
- const sv = source.value;
640
- const tv = target.value;
641
- // Only swap if the value exists in both dropdowns
642
- if ([...source.options].some(o => o.value === tv) &&
643
- [...target.options].some(o => o.value === sv)) {
644
- source.value = tv;
645
- target.value = sv;
646
- }
647
- // Rotate animation
648
- langSwap.style.transform = `rotate(${(parseFloat(langSwap.dataset.rot || 0)) + 180}deg)`;
649
- langSwap.dataset.rot = (parseFloat(langSwap.dataset.rot || 0)) + 180;
650
- });
651
- }
652
-
653
- // ════════════════════════════════════════════════════════
654
- // TRANSLATE SUBMISSION
655
- // ════════════════════════════════════════════════════════
656
- const translateBtn = $('#translate-btn');
657
- const appInput = $('#app-input');
658
- const appProcessing = $('#app-processing');
659
- const appResult = $('#app-result');
660
-
661
- if (translateBtn) {
662
- translateBtn.addEventListener('click', async () => {
663
- const activeTab = $('.tab-btn.active')?.dataset.tab;
664
- const targetLang = $('#target-lang').value;
665
-
666
- // Gather input
667
- let videoUrl = null;
668
- let file = null;
669
-
670
- if (activeTab === 'url') {
671
- videoUrl = $('#video-url').value.trim();
672
- if (!videoUrl) {
673
- showToast('Please paste a video URL.', 'error');
674
- return;
675
- }
676
- } else {
677
- file = selectedFile;
678
- if (!file) {
679
- showToast('Please select a video file.', 'error');
680
- return;
681
- }
682
- }
683
-
684
- // Switch to processing view
685
- setState('processing');
686
-
687
- // On mobile the input panel is hidden (display:none) so the user
688
- // loses playback controls — pause the video to prevent audio leak.
689
- const uploadVideo = $('#upload-preview-video');
690
- if (uploadVideo && window.innerWidth <= 999) { uploadVideo.pause(); }
691
-
692
- // Read voice model mode
693
- const voiceMode = document.querySelector('input[name="voice_mode"]:checked')?.value || 'chatterbox';
694
-
695
- // Build form data
696
- const formData = new FormData();
697
- if (file) {
698
- formData.append('file', file);
699
- } else {
700
- formData.append('url', videoUrl);
701
- }
702
- formData.append('target_language', targetLang);
703
- formData.append('source_language', $('#source-lang').value);
704
- formData.append('voice_mode', voiceMode);
705
- formData.append('captions', $('#captions-toggle').checked ? 'true' : 'false');
706
- formData.append('preserve_music', $('#music-toggle').checked ? 'true' : 'false');
707
-
708
- try {
709
- // Submit job
710
- const res = await fetch(`${API_BASE}/api/jobs`, {
711
- method: 'POST',
712
- body: formData,
713
- });
714
-
715
- if (!res.ok) {
716
- const err = await res.json().catch(() => ({ detail: 'Server error' }));
717
- throw new Error(err.detail || 'Failed to start job');
718
- }
719
-
720
- const { job_id } = await res.json();
721
-
722
- // Stream progress via SSE
723
- streamProgress(job_id, targetLang);
724
- } catch (err) {
725
- showToast(err.message, 'error');
726
- setState('input');
727
- }
728
- });
729
- }
730
-
731
- function streamProgress(jobId, targetLang) {
732
- const log = $('#processing-log');
733
- const progressBar = $('#progress-bar');
734
- const stepText = $('#processing-step');
735
- const previewPanel = $('#voice-preview-panel');
736
- log.innerHTML = '';
737
-
738
- const voiceMode = document.querySelector('input[name="voice_mode"]:checked')?.value || 'chatterbox';
739
- const totalSteps = voiceMode === 'preview_both' ? 7 : 6;
740
-
741
- let currentStep = 0;
742
- let cursor = 0;
743
- let stopped = false;
744
- let previewBound = false;
745
-
746
- let waitHeader = null; // "Waiting for GPU..." static line
747
- let waitDetail = null; // "2 jobs ahead (Step 4/6 — Synthesising)" live line
748
-
749
- async function poll() {
750
- if (stopped) return;
751
- try {
752
- const res = await fetch(`${API_BASE}/api/jobs/${jobId}?after=${cursor}`);
753
- if (!res.ok) throw new Error('Poll failed');
754
- const { messages, next, wait_status } = await res.json();
755
- cursor = next;
756
-
757
- // Show / update queue status
758
- if (wait_status) {
759
- if (!waitHeader) {
760
- waitHeader = document.createElement('div');
761
- waitHeader.className = 'terminal-line';
762
- waitHeader.style.opacity = '1';
763
- waitHeader.style.color = 'var(--accent)';
764
- waitHeader.style.fontWeight = '600';
765
- waitHeader.textContent = 'Waiting for GPU...';
766
- log.appendChild(waitHeader);
767
-
768
- waitDetail = document.createElement('div');
769
- waitDetail.className = 'terminal-line';
770
- waitDetail.style.opacity = '1';
771
- waitDetail.style.color = 'var(--text-2)';
772
- log.appendChild(waitDetail);
773
- }
774
- waitDetail.textContent = wait_status;
775
- stepText.textContent = wait_status;
776
- } else if (waitHeader) {
777
- // GPU acquired — remove both wait lines
778
- waitHeader.remove();
779
- if (waitDetail) waitDetail.remove();
780
- waitHeader = null;
781
- waitDetail = null;
782
- }
783
-
784
- for (const data of messages) {
785
- handleMessage(data);
786
- if (stopped) return;
787
- }
788
- } catch (err) {
789
- console.warn('Poll error:', err);
790
- }
791
- if (!stopped) setTimeout(poll, 500);
792
- }
793
-
794
- function handleMessage(data) {
795
- if (data.type === 'progress') {
796
- const line = document.createElement('div');
797
- line.className = 'terminal-line';
798
- line.style.opacity = '1';
799
- line.textContent = data.message;
800
- log.appendChild(line);
801
- log.scrollTop = log.scrollHeight;
802
-
803
- if (data.step) {
804
- currentStep = data.step;
805
- const pct = Math.round((currentStep / totalSteps) * 100);
806
- progressBar.style.width = `${pct}%`;
807
- stepText.textContent = data.message;
808
- }
809
- }
810
-
811
- // ── Voice preview event ─────────────────────────────
812
- if (data.type === 'voice_preview') {
813
- currentStep = data.step || 4;
814
- const pct = Math.round((currentStep / totalSteps) * 100);
815
- progressBar.style.width = `${pct}%`;
816
- stepText.textContent = '🎧 Previews ready — choose your voice model';
817
-
818
- const spinner = $('.processing-spinner');
819
- if (spinner) spinner.style.display = 'none';
820
- const header = $('.processing-header h3');
821
- if (header) header.textContent = 'Choose your voice';
822
-
823
- if (previewPanel) {
824
- previewPanel.style.display = 'block';
825
- requestAnimationFrame(() => previewPanel.classList.add('visible'));
826
-
827
- const previews = data.previews || {};
828
- Object.entries(previews).forEach(([model, url]) => {
829
- const audio = $(`#preview-audio-${model}`);
830
- const card = $(`#voice-card-${model}`);
831
- if (audio && url) {
832
- audio.src = `${API_BASE}${url}`;
833
- audio.preload = 'auto';
834
- audio.load();
835
- if (card) card.classList.remove('unavailable');
836
- } else if (card) {
837
- card.classList.add('unavailable');
838
- }
839
- });
840
-
841
- if (!previewBound) {
842
- previewBound = true;
843
- $$('.voice-select-btn').forEach(btn => {
844
- btn.addEventListener('click', (e) => {
845
- e.preventDefault();
846
- const model = btn.dataset.model;
847
- selectModel(jobId, model, previewPanel);
848
- }, { once: true });
849
- });
850
- initPlayPauseButtons();
851
- }
852
- }
853
- }
854
-
855
- if (data.type === 'complete') {
856
- stopped = true;
857
- progressBar.style.width = '100%';
858
-
859
- const spinner = $('.processing-spinner');
860
- if (spinner) spinner.style.display = 'none';
861
- const header = $('.processing-header h3');
862
- if (header) header.textContent = 'Translation complete';
863
- stepText.textContent = `Done in ${data.elapsed || '—'}s`;
864
-
865
- const video = $('#result-video');
866
- video.src = `${API_BASE}/api/jobs/${jobId}/result`;
867
- $('#result-meta').textContent = `Translated to ${targetLang} · Processed in ${data.elapsed || '—'}s`;
868
- $('#download-btn').href = `${API_BASE}/api/jobs/${jobId}/result`;
869
-
870
- document.querySelector('.result-skeleton').style.display = 'none';
871
- document.querySelector('.result-content-wrap').style.display = 'block';
872
-
873
- setTimeout(() => setState('result'), 600);
874
- }
875
-
876
- if (data.type === 'error') {
877
- stopped = true;
878
- showToast(data.message || 'Pipeline error occurred', 'error');
879
- setState('input');
880
- }
881
- }
882
-
883
- // Start polling
884
- poll();
885
- }
886
-
887
- async function selectModel(jobId, modelName, previewPanel) {
888
- try {
889
- // Visually mark selected card
890
- $$('.voice-card').forEach(c => c.classList.remove('selected'));
891
- $(`#voice-card-${modelName}`)?.classList.add('selected');
892
-
893
- // Disable all select buttons
894
- $$('.voice-select-btn').forEach(btn => {
895
- btn.disabled = true;
896
- if (btn.dataset.model === modelName) {
897
- btn.innerHTML = `
898
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
899
- <path d="M20 6L9 17l-5-5" />
900
- </svg>
901
- Selected ✓`;
902
- }
903
- });
904
-
905
- // Stop any playing audio and reset play/pause buttons
906
- $$('.preview-audio').forEach(a => { a.pause(); a.currentTime = 0; });
907
- $$('.play-pause-btn').forEach(b => {
908
- b.querySelector('.icon-play').style.display = '';
909
- b.querySelector('.icon-pause').style.display = 'none';
910
- });
911
-
912
- // POST selection to server
913
- const res = await fetch(`${API_BASE}/api/jobs/${jobId}/select-model`, {
914
- method: 'POST',
915
- headers: { 'Content-Type': 'application/json' },
916
- body: JSON.stringify({ model: modelName }),
917
- });
918
-
919
- if (!res.ok) throw new Error('Failed to select model');
920
-
921
- // Collapse preview panel after a short delay
922
- setTimeout(() => {
923
- if (previewPanel) {
924
- previewPanel.classList.remove('visible');
925
- previewPanel.classList.add('collapsed');
926
- }
927
- // Restore spinner
928
- const spinner = $('.processing-spinner');
929
- if (spinner) spinner.style.display = '';
930
- const header = $('.processing-header h3');
931
- if (header) header.textContent = 'Synthesising with ' + modelName + '...';
932
- }, 800);
933
-
934
- showToast(`Selected ${modelName} — continuing synthesis...`, 'info');
935
-
936
- } catch (err) {
937
- showToast(err.message, 'error');
938
- }
939
- }
940
-
941
- function setState(state) {
942
- const container = document.querySelector('.app-container');
943
- container.classList.remove('state-input', 'state-processing', 'state-result');
944
- container.classList.add(`state-${state}`);
945
-
946
- if (state !== 'input') {
947
- container.style.maxWidth = '100%';
948
- } else {
949
- container.style.maxWidth = '640px';
950
- // If going back to input, reset skeleton visibility
951
- const skeleton = document.querySelector('.result-skeleton');
952
- const contentWrap = document.querySelector('.result-content-wrap');
953
- if (skeleton && contentWrap) {
954
- skeleton.style.display = '';
955
- contentWrap.style.display = 'none';
956
- }
957
- // Reset voice preview panel
958
- const previewPanel = $('#voice-preview-panel');
959
- if (previewPanel) {
960
- previewPanel.style.display = 'none';
961
- previewPanel.classList.remove('visible', 'collapsed');
962
- }
963
- // Reset spinner
964
- const spinner = $('.processing-spinner');
965
- if (spinner) spinner.style.display = '';
966
- const procHeader = $('.processing-header h3');
967
- if (procHeader) procHeader.textContent = 'Translating your video...';
968
- }
969
-
970
- // Disable input controls during processing/result
971
- const isNotInput = state !== 'input';
972
- $('#file-input').disabled = isNotInput;
973
- $('#video-url').disabled = isNotInput;
974
- $('#translate-btn').disabled = isNotInput;
975
- $('#source-lang').disabled = isNotInput;
976
- $('#target-lang').disabled = isNotInput;
977
- $('#captions-toggle').disabled = isNotInput;
978
- $('#music-toggle').disabled = isNotInput;
979
- }
980
-
981
- // New video button
982
- const newVideoBtn = $('#new-video-btn');
983
- if (newVideoBtn) {
984
- newVideoBtn.addEventListener('click', () => {
985
- clearFile();
986
- $('#video-url').value = '';
987
- hideUrlPreview();
988
- setState('input');
989
- });
990
- }
991
-
992
- // ════════════════════════════════════════════════════════
993
- // TOAST NOTIFICATIONS
994
- // ════════════════════════════════════════════════════════
995
- function showToast(message, type = 'info') {
996
- const existing = $('.toast');
997
- if (existing) existing.remove();
998
-
999
- const toast = document.createElement('div');
1000
- toast.className = `toast toast-${type}`;
1001
- toast.innerHTML = `
1002
- <span>${message}</span>
1003
- <button onclick="this.parentElement.remove()" aria-label="Dismiss">&times;</button>
1004
- `;
1005
- document.body.appendChild(toast);
1006
-
1007
- // Add styles if not present
1008
- if (!$('#toast-styles')) {
1009
- const style = document.createElement('style');
1010
- style.id = 'toast-styles';
1011
- style.textContent = `
1012
- .toast {
1013
- position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
1014
- background: var(--surface); border: 1px solid var(--border);
1015
- border-radius: 12px; padding: 12px 20px;
1016
- display: flex; align-items: center; gap: 12px;
1017
- font-size: .875rem; color: var(--text);
1018
- box-shadow: 0 12px 40px rgba(0,0,0,.5);
1019
- z-index: 10000; animation: toast-in .3s ease;
1020
- }
1021
- .toast-error { border-color: var(--red); }
1022
- .toast button {
1023
- background: none; border: none; color: var(--text-3);
1024
- font-size: 1.2rem; cursor: pointer; padding: 0 4px;
1025
- }
1026
- @keyframes toast-in {
1027
- from { opacity: 0; transform: translateX(-50%) translateY(12px); }
1028
- to { opacity: 1; transform: translateX(-50%) translateY(0); }
1029
- }
1030
- `;
1031
- document.head.appendChild(style);
1032
- }
1033
-
1034
- setTimeout(() => { if (toast.parentElement) toast.remove(); }, 5000);
1035
- }
1036
-
1037
- // ════════════════════════════════════════════════════════
1038
- // CUSTOM PLAY / PAUSE FOR VOICE PREVIEW
1039
- // ════════════════════════════════════════════════════════
1040
- function _setPlaying(btn, playing) {
1041
- btn.querySelector('.icon-play').style.display = playing ? 'none' : 'block';
1042
- btn.querySelector('.icon-pause').style.display = playing ? 'block' : 'none';
1043
- }
1044
-
1045
- function initPlayPauseButtons() {
1046
- // Remove old listeners by cloning buttons
1047
- $$('.play-pause-btn').forEach(oldBtn => {
1048
- const btn = oldBtn.cloneNode(true);
1049
- oldBtn.replaceWith(btn);
1050
-
1051
- btn.addEventListener('click', async () => {
1052
- const audio = document.getElementById(btn.dataset.target);
1053
- if (!audio || !audio.src) return;
1054
-
1055
- if (audio.paused) {
1056
- // Pause all other previews first
1057
- $$('.preview-audio').forEach(a => { if (a !== audio && !a.paused) { a.pause(); a.currentTime = 0; } });
1058
- $$('.play-pause-btn').forEach(b => { if (b !== btn) _setPlaying(b, false); });
1059
-
1060
- btn.classList.add('loading');
1061
- try {
1062
- await audio.play();
1063
- } catch (e) {
1064
- console.warn('Audio play failed:', e);
1065
- }
1066
- btn.classList.remove('loading');
1067
- _setPlaying(btn, !audio.paused);
1068
- } else {
1069
- audio.pause();
1070
- _setPlaying(btn, false);
1071
- }
1072
- });
1073
- });
1074
-
1075
- // Progress + time updates
1076
- $$('.preview-audio').forEach(audio => {
1077
- const player = audio.closest('.voice-card-player');
1078
- if (!player) return;
1079
- const bar = player.querySelector('.audio-progress-bar');
1080
- const timeEl = player.querySelector('.audio-time');
1081
- const progressTrack = player.querySelector('.audio-progress');
1082
-
1083
- function findBtn() { return player.querySelector('.play-pause-btn'); }
1084
-
1085
- audio.addEventListener('timeupdate', () => {
1086
- if (!audio.duration) return;
1087
- const pct = (audio.currentTime / audio.duration) * 100;
1088
- if (bar) bar.style.width = pct + '%';
1089
- if (timeEl) {
1090
- const rem = Math.max(0, Math.ceil(audio.duration - audio.currentTime));
1091
- const m = Math.floor(rem / 60);
1092
- const s = String(rem % 60).padStart(2, '0');
1093
- timeEl.textContent = m + ':' + s;
1094
- }
1095
- });
1096
-
1097
- audio.addEventListener('loadedmetadata', () => {
1098
- if (timeEl && audio.duration && isFinite(audio.duration)) {
1099
- const d = Math.ceil(audio.duration);
1100
- const m = Math.floor(d / 60);
1101
- const s = String(d % 60).padStart(2, '0');
1102
- timeEl.textContent = m + ':' + s;
1103
- }
1104
- });
1105
-
1106
- audio.addEventListener('ended', () => {
1107
- if (bar) bar.style.width = '0%';
1108
- if (timeEl) timeEl.textContent = '0:00';
1109
- const btn = findBtn();
1110
- if (btn) _setPlaying(btn, false);
1111
- });
1112
-
1113
- audio.addEventListener('error', () => {
1114
- console.warn('Audio error for', audio.id, audio.error);
1115
- const btn = findBtn();
1116
- if (btn) { btn.classList.remove('loading'); _setPlaying(btn, false); }
1117
- });
1118
-
1119
- // Click on progress bar to seek
1120
- if (progressTrack) {
1121
- progressTrack.addEventListener('click', (e) => {
1122
- if (!audio.duration) return;
1123
- const rect = progressTrack.getBoundingClientRect();
1124
- const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
1125
- audio.currentTime = pct * audio.duration;
1126
- });
1127
- }
1128
- });
1129
- }
1130
-
1131
- // ════════════════════════════════════════════════════════
1132
- // COMMAND PALETTE (⌘K / Ctrl+K)
1133
- // ════════════════════════════════════════════════════════
1134
- const cmdOverlay = $('#cmd-palette-overlay');
1135
- const cmdSearch = $('#cmd-search');
1136
- const cmdResultsContainer = $('#cmd-results');
1137
-
1138
- const cmdActions = [
1139
- {
1140
- group: 'Navigation', items: [
1141
- { label: 'Go to Home', icon: '🏠', action: () => scrollToSection('#hero') },
1142
- { label: 'How it Works', icon: '⚡', action: () => scrollToSection('#how-it-works') },
1143
- { label: 'See Demos', icon: '🎬', action: () => scrollToSection('#demos') },
1144
- { label: 'Try Now — Free', icon: '🚀', action: () => scrollToSection('#app') },
1145
- { label: 'View Pricing', icon: '💰', action: () => scrollToSection('#pricing') },
1146
- ]
1147
- },
1148
- {
1149
- group: 'Actions', items: [
1150
- { label: 'Upload Video', icon: '📤', action: () => { scrollToSection('#app'); setTimeout(() => { $('#tab-upload')?.click(); }, 400); } },
1151
- { label: 'Paste URL', icon: '🔗', action: () => { scrollToSection('#app'); setTimeout(() => { $('#tab-url')?.click(); }, 400); } },
1152
- { label: 'Toggle Dark Mode', icon: '🌙', hint: 'Already dark ✓', action: () => { } },
1153
- ]
1154
- },
1155
- {
1156
- group: 'Languages', items: [
1157
- { label: 'Translate to English', icon: '🇬🇧', action: () => setLang('English') },
1158
- { label: 'Translate to Spanish', icon: '🇪🇸', action: () => setLang('Spanish') },
1159
- { label: 'Translate to French', icon: '🇫🇷', action: () => setLang('French') },
1160
- { label: 'Translate to German', icon: '🇩🇪', action: () => setLang('German') },
1161
- { label: 'Translate to Hindi', icon: '🇮🇳', action: () => setLang('Hindi') },
1162
- { label: 'Translate to Japanese', icon: '🇯🇵', action: () => setLang('Japanese') },
1163
- { label: 'Translate to Chinese', icon: '🇨🇳', action: () => setLang('Chinese') },
1164
- { label: 'Translate to Arabic', icon: '🇸🇦', action: () => setLang('Arabic') },
1165
- { label: 'Translate to Korean', icon: '🇰🇷', action: () => setLang('Korean') },
1166
- { label: 'Translate to Portuguese', icon: '🇧🇷', action: () => setLang('Portuguese') },
1167
- { label: 'Translate to Italian', icon: '🇮🇹', action: () => setLang('Italian') },
1168
- ]
1169
- },
1170
- ];
1171
-
1172
- function setLang(lang) {
1173
- const target = $('#target-lang');
1174
- if (target) target.value = lang;
1175
- scrollToSection('#app');
1176
- }
1177
-
1178
- function scrollToSection(selector) {
1179
- const el = $(selector);
1180
- if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
1181
- }
1182
-
1183
- // Open / close
1184
- function openCmdPalette() {
1185
- cmdOverlay.classList.add('open');
1186
- cmdSearch.value = '';
1187
- renderCmdResults('');
1188
- setTimeout(() => cmdSearch.focus(), 100);
1189
- }
1190
- function closeCmdPalette() {
1191
- cmdOverlay.classList.remove('open');
1192
- }
1193
-
1194
- // Keyboard shortcut
1195
- document.addEventListener('keydown', (e) => {
1196
- if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
1197
- e.preventDefault();
1198
- if (cmdOverlay.classList.contains('open')) {
1199
- closeCmdPalette();
1200
- } else {
1201
- openCmdPalette();
1202
- }
1203
- }
1204
- if (e.key === 'Escape' && cmdOverlay.classList.contains('open')) {
1205
- closeCmdPalette();
1206
- }
1207
- });
1208
-
1209
- // Close on overlay click
1210
- if (cmdOverlay) {
1211
- cmdOverlay.addEventListener('click', (e) => {
1212
- if (e.target === cmdOverlay) closeCmdPalette();
1213
- });
1214
- }
1215
-
1216
- // Search filtering
1217
- let activeIndex = 0;
1218
- if (cmdSearch) {
1219
- cmdSearch.addEventListener('input', () => {
1220
- renderCmdResults(cmdSearch.value);
1221
- });
1222
-
1223
- // Arrow key navigation
1224
- cmdSearch.addEventListener('keydown', (e) => {
1225
- const items = $$('.cmd-item');
1226
- if (e.key === 'ArrowDown') {
1227
- e.preventDefault();
1228
- activeIndex = Math.min(activeIndex + 1, items.length - 1);
1229
- updateActiveItem(items);
1230
- } else if (e.key === 'ArrowUp') {
1231
- e.preventDefault();
1232
- activeIndex = Math.max(activeIndex - 1, 0);
1233
- updateActiveItem(items);
1234
- } else if (e.key === 'Enter') {
1235
- e.preventDefault();
1236
- if (items[activeIndex]) items[activeIndex].click();
1237
- }
1238
- });
1239
- }
1240
-
1241
- function updateActiveItem(items) {
1242
- items.forEach((item, i) => {
1243
- item.classList.toggle('active', i === activeIndex);
1244
- if (i === activeIndex) item.scrollIntoView({ block: 'nearest' });
1245
- });
1246
- }
1247
-
1248
- function renderCmdResults(query) {
1249
- if (!cmdResultsContainer) return;
1250
- const q = query.toLowerCase().trim();
1251
- activeIndex = 0;
1252
- let html = '';
1253
-
1254
- cmdActions.forEach(group => {
1255
- const filtered = group.items.filter(item =>
1256
- !q || item.label.toLowerCase().includes(q)
1257
- );
1258
- if (filtered.length === 0) return;
1259
-
1260
- html += `<div class="cmd-group-label">${group.group}</div>`;
1261
- filtered.forEach((item, idx) => {
1262
- html += `
1263
- <div class="cmd-item" data-group="${group.group}" data-label="${item.label}">
1264
- <div class="cmd-item-icon">${item.icon}</div>
1265
- <span class="cmd-item-text">${item.label}</span>
1266
- ${item.hint ? `<span class="cmd-item-hint">${item.hint}</span>` : ''}
1267
- </div>
1268
- `;
1269
- });
1270
- });
1271
-
1272
- if (!html) {
1273
- html = `<div style="padding: 32px; text-align: center; color: var(--text-3); font-size: .875rem;">No results for "${query}"</div>`;
1274
- }
1275
-
1276
- cmdResultsContainer.innerHTML = html;
1277
-
1278
- // Bind click handlers
1279
- $$('.cmd-item').forEach((el, idx) => {
1280
- el.addEventListener('click', () => {
1281
- const label = el.dataset.label;
1282
- const group = el.dataset.group;
1283
- const groupData = cmdActions.find(g => g.group === group);
1284
- const itemData = groupData?.items.find(i => i.label === label);
1285
- if (itemData) {
1286
- closeCmdPalette();
1287
- itemData.action();
1288
- }
1289
- });
1290
- el.addEventListener('mouseenter', () => {
1291
- activeIndex = idx;
1292
- updateActiveItem($$('.cmd-item'));
1293
- });
1294
- });
1295
-
1296
- // Highlight first
1297
- const items = $$('.cmd-item');
1298
- if (items[0]) items[0].classList.add('active');
1299
- }
1300
-
1301
- // ── CMD palette trigger button in nav ───────────────────
1302
- const cmdTrigger = $('#cmd-trigger');
1303
- if (cmdTrigger) {
1304
- cmdTrigger.addEventListener('click', (e) => {
1305
- e.preventDefault();
1306
- openCmdPalette();
1307
- });
1308
- }
1309
-
1310
- // ════════════════════════════════════════════════════════
1311
- // FREE QUOTA TRACKING (localStorage)
1312
- // ════════════════════════════════════════════════════════
1313
- function getUsedVideos() {
1314
- return parseInt(localStorage.getItem('vv_used') || '0', 10);
1315
- }
1316
- function incrementUsedVideos() {
1317
- localStorage.setItem('vv_used', (getUsedVideos() + 1).toString());
1318
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/index.html DELETED
@@ -1,1089 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
-
4
- <head>
5
- <meta charset="UTF-8" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>VideoVoice — Translate Any Short Video in the Creator's Own Voice</title>
8
- <meta name="description"
9
- content="Paste a link or upload a short video. VideoVoice translates it into 23+ languages using AI voice cloning — preserving the original creator's voice." />
10
- <meta name="theme-color" content="#000000" />
11
- <link rel="preconnect" href="https://fonts.googleapis.com" />
12
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
13
- <link
14
- href="https://fonts.googleapis.com/css2?family=Syne:wght@600;700;800&family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,300&display=swap"
15
- rel="stylesheet" />
16
- <link rel="stylesheet" href="style.css?v=20260402" />
17
- </head>
18
-
19
- <body>
20
- <!-- ── Navigation ────────────────────────────────────── -->
21
- <nav class="nav" id="navbar">
22
- <div class="nav-inner">
23
- <a href="#" class="nav-logo">
24
- <span class="logo-icon">▶</span>
25
- <span class="logo-text">VideoVoice</span>
26
- </a>
27
- <div class="nav-links">
28
- <a href="#how-it-works">How it works</a>
29
- <a href="#demos">Demos</a>
30
- <a href="#pricing">Pricing</a>
31
- <a href="#" class="cmd-trigger-btn" id="cmd-trigger" title="Search (⌘K)">
32
- <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
33
- <circle cx="11" cy="11" r="8" />
34
- <path d="M21 21l-4.35-4.35" />
35
- </svg>
36
- <span class="cmd-trigger-kbd">⌘K</span>
37
- </a>
38
- <a href="#app" class="btn btn-primary btn-sm">Try Now — Free</a>
39
- </div>
40
- <button class="mobile-menu-btn" id="mobile-menu-btn" aria-label="Toggle menu">
41
- <span></span><span></span><span></span>
42
- </button>
43
- </div>
44
- <div class="mobile-nav" id="mobile-nav">
45
- <a href="#how-it-works">How it works</a>
46
- <a href="#demos">Demos</a>
47
- <a href="#pricing">Pricing</a>
48
- <a href="#app" class="btn btn-primary">Try Now — Free</a>
49
- </div>
50
- </nav>
51
-
52
- <!-- ── Hero ──────────────────────────────────────────── -->
53
- <section class="hero" id="hero">
54
-
55
- <!-- Phone tracks background -->
56
- <div class="vid-tracks" aria-hidden="true">
57
- <div class="track track-1">
58
- <div class="rail" id="vt-r1a"></div>
59
- <div class="rail" id="vt-r1b"></div>
60
- </div>
61
- <div class="track track-2">
62
- <div class="rail" id="vt-r2a"></div>
63
- <div class="rail" id="vt-r2b"></div>
64
- </div>
65
- <div class="track track-3">
66
- <div class="rail" id="vt-r3a"></div>
67
- <div class="rail" id="vt-r3b"></div>
68
- </div>
69
- </div>
70
- <div class="vid-vignette" aria-hidden="true"></div>
71
- <div class="vid-grain" aria-hidden="true"></div>
72
-
73
- <!-- Hero content -->
74
- <div class="hero-inner">
75
- <div class="live-badge">Now live — 23+ languages</div>
76
-
77
- <h1>Any short video.<br>In the creator's <em>own voice.</em></h1>
78
-
79
- <p class="hero-sub">
80
- Paste a Reel, Short, or TikTok link — our AI transcribes, translates,
81
- and re-voices it using a zero-shot clone of the original speaker.
82
- Same person. New language. Seconds.
83
- </p>
84
-
85
- <div class="hero-cta">
86
- <a href="#app" class="btn btn-primary btn-lg" id="hero-cta-btn">
87
- Try free
88
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
89
- <path d="M5 12h14M12 5l7 7-7 7" />
90
- </svg>
91
- </a>
92
- <a href="#how-it-works" class="btn btn-ghost btn-lg">
93
- See how it works
94
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
95
- <circle cx="12" cy="12" r="10" />
96
- <polygon points="10 8 16 12 10 16 10 8" fill="currentColor" stroke="none" />
97
- </svg>
98
- </a>
99
- </div>
100
-
101
- <div class="hero-stats">
102
- <div class="hero-stat">
103
- <div class="hero-stat-n">23+</div>
104
- <div class="hero-stat-l">Languages</div>
105
- </div>
106
- <div class="hero-stat-div"></div>
107
- <div class="hero-stat">
108
- <div class="hero-stat-n">&lt;2 min</div>
109
- <div class="hero-stat-l">Processing</div>
110
- </div>
111
- <div class="hero-stat-div"></div>
112
- <div class="hero-stat">
113
- <div class="hero-stat-n">Zero-Shot</div>
114
- <div class="hero-stat-l">Voice Clone</div>
115
- </div>
116
- <div class="hero-stat-div"></div>
117
- <div class="hero-stat">
118
- <div class="hero-stat-n">Free</div>
119
- <div class="hero-stat-l">First video</div>
120
- </div>
121
- </div>
122
- </div>
123
-
124
- <!-- Scroll hint -->
125
- <div class="hero-scroll-hint" aria-hidden="true">
126
- <span>Scroll</span>
127
- <div class="hero-scroll-arr"></div>
128
- </div>
129
-
130
- </section>
131
-
132
- <!-- ── How it works ──────────────────────────────────── -->
133
- <section id="how-it-works" class="section">
134
- <div class="container">
135
- <div class="section-label reveal">
136
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
137
- <path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
138
- </svg>
139
- HOW IT WORKS
140
- </div>
141
- <h2 class="section-title reveal">Three steps. Fully automated.</h2>
142
- <p class="section-sub reveal">Upload your video, pick a language, and let our AI handle the rest.</p>
143
-
144
- <div class="steps-grid">
145
- <div class="step-card reveal">
146
- <div class="step-number">01</div>
147
- <div class="step-icon-wrap">
148
- <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
149
- <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12" />
150
- </svg>
151
- </div>
152
- <h3>Upload or Paste Link</h3>
153
- <p>Drop a video file or paste an Instagram Reel / YouTube Short URL. We accept any video up to 60 seconds.</p>
154
- </div>
155
-
156
- <div class="step-connector reveal">
157
- <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"
158
- opacity="0.3">
159
- <path d="M5 12h14M12 5l7 7-7 7" />
160
- </svg>
161
- </div>
162
-
163
- <div class="step-card reveal">
164
- <div class="step-number">02</div>
165
- <div class="step-icon-wrap">
166
- <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
167
- <circle cx="12" cy="12" r="10" />
168
- <path d="M12 6v6l4 2" />
169
- </svg>
170
- </div>
171
- <h3>AI Translates & Clones</h3>
172
- <p>Our pipeline transcribes, translates, and synthesizes new speech using a voice clone of the original
173
- speaker.</p>
174
- </div>
175
-
176
- <div class="step-connector reveal">
177
- <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"
178
- opacity="0.3">
179
- <path d="M5 12h14M12 5l7 7-7 7" />
180
- </svg>
181
- </div>
182
-
183
- <div class="step-card reveal">
184
- <div class="step-number">03</div>
185
- <div class="step-icon-wrap">
186
- <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
187
- <polygon points="5 3 19 12 5 21 5 3" />
188
- </svg>
189
- </div>
190
- <h3>Preview & Download</h3>
191
- <p>Watch your translated video right here. Download it in full quality and share it with the world.</p>
192
- </div>
193
- </div>
194
- </div>
195
- </section>
196
-
197
- <!-- ── Why VideoVoice ─────────────────────────────────── -->
198
- <section id="why-us" class="section">
199
- <div class="container">
200
- <div class="section-label reveal">
201
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
202
- <path d="M9 18l6-6-6-6" />
203
- </svg>
204
- WHY VIDEOVOICE
205
- </div>
206
- <h2 class="section-title reveal">Auto-dub loses your voice. We keep it.</h2>
207
- <p class="section-sub reveal">YouTube and Instagram translate with generic TTS. Your audience can tell.
208
- VideoVoice clones your actual voice so your content sounds like <em>you</em> in every language.</p>
209
-
210
- <!-- Contrast cards -->
211
- <div class="contrast-grid reveal">
212
- <div class="contrast-card contrast-them">
213
- <div class="contrast-header">
214
- <span class="contrast-badge them-badge">Platform auto-dub</span>
215
- </div>
216
- <ul class="contrast-list">
217
- <li>
218
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M18 6L6 18M6 6l12 12"/></svg>
219
- Generic robot voice
220
- </li>
221
- <li>
222
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M18 6L6 18M6 6l12 12"/></svg>
223
- Locked to one platform
224
- </li>
225
- <li>
226
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M18 6L6 18M6 6l12 12"/></svg>
227
- No control over output
228
- </li>
229
- <li>
230
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M18 6L6 18M6 6l12 12"/></svg>
231
- Captions only (no voice dub)
232
- </li>
233
- </ul>
234
- </div>
235
-
236
- <div class="contrast-vs reveal">VS</div>
237
-
238
- <div class="contrast-card contrast-us">
239
- <div class="contrast-header">
240
- <span class="contrast-badge us-badge">VideoVoice</span>
241
- </div>
242
- <ul class="contrast-list">
243
- <li>
244
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>
245
- Your voice, cloned with AI
246
- </li>
247
- <li>
248
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>
249
- Download & post anywhere
250
- </li>
251
- <li>
252
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>
253
- Preview & choose voice model
254
- </li>
255
- <li>
256
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>
257
- Burn-in captions + voice dub
258
- </li>
259
- </ul>
260
- </div>
261
- </div>
262
-
263
- <!-- Market comparison table -->
264
- <div class="compare-wrap reveal">
265
- <h3 class="compare-title">How we stack up</h3>
266
- <p class="compare-sub">Honest pricing comparison with leading AI dubbing services.</p>
267
- <div class="compare-table-scroll">
268
- <table class="compare-table">
269
- <thead>
270
- <tr>
271
- <th>Service</th>
272
- <th>Starting price</th>
273
- <th>Voice clone</th>
274
- <th>Lip sync</th>
275
- <th>Cross-platform</th>
276
- <th>Captions</th>
277
- </tr>
278
- </thead>
279
- <tbody>
280
- <tr>
281
- <td><strong>HeyGen</strong></td>
282
- <td>$24/mo <span class="compare-note">15 min</span></td>
283
- <td class="c-yes">Yes</td>
284
- <td class="c-yes">Yes</td>
285
- <td class="c-yes">Yes</td>
286
- <td class="c-no">No</td>
287
- </tr>
288
- <tr>
289
- <td><strong>Rask.ai</strong></td>
290
- <td>$60/mo <span class="compare-note">25 min</span></td>
291
- <td class="c-yes">Yes</td>
292
- <td class="c-mid">Add-on</td>
293
- <td class="c-yes">Yes</td>
294
- <td class="c-yes">SRT export</td>
295
- </tr>
296
- <tr>
297
- <td><strong>ElevenLabs</strong></td>
298
- <td>$22/mo <span class="compare-note">char-based</span></td>
299
- <td class="c-yes">Yes</td>
300
- <td class="c-no">No</td>
301
- <td class="c-yes">Yes</td>
302
- <td class="c-no">No</td>
303
- </tr>
304
- <tr>
305
- <td><strong>Synthesia</strong></td>
306
- <td>$22/mo <span class="compare-note">10 min</span></td>
307
- <td class="c-mid">Limited</td>
308
- <td class="c-mid">Avatar only</td>
309
- <td class="c-mid">Partial</td>
310
- <td class="c-no">No</td>
311
- </tr>
312
- <tr>
313
- <td><strong>YouTube</strong></td>
314
- <td>Free</td>
315
- <td class="c-no">No</td>
316
- <td class="c-no">No</td>
317
- <td class="c-no">YouTube only</td>
318
- <td class="c-yes">Auto</td>
319
- </tr>
320
- <tr>
321
- <td><strong>IG / TikTok</strong></td>
322
- <td>Free</td>
323
- <td class="c-no">No</td>
324
- <td class="c-no">No</td>
325
- <td class="c-no">In-app only</td>
326
- <td class="c-mid">Captions only</td>
327
- </tr>
328
- <tr class="compare-highlight">
329
- <td><strong>VideoVoice</strong></td>
330
- <td>Free <span class="compare-note">1st video free</span></td>
331
- <td class="c-yes">Yes</td>
332
- <td class="c-no">No</td>
333
- <td class="c-yes">Yes</td>
334
- <td class="c-yes">Burn-in</td>
335
- </tr>
336
- </tbody>
337
- </table>
338
- </div>
339
- <p class="compare-footnote">Prices as of early 2025. VideoVoice is free during beta — no account needed.</p>
340
- </div>
341
-
342
- </div>
343
- </section>
344
-
345
- <!-- ── Features ──────────────────────────────────────── -->
346
- <section class="section section-dark">
347
- <div class="container">
348
- <div class="section-label reveal">
349
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
350
- <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
351
- </svg>
352
- POWERED BY
353
- </div>
354
- <h2 class="section-title reveal">State-of-the-art AI pipeline</h2>
355
- <p class="section-sub reveal">Six ML models working in harmony — from transcription to voice cloning.</p>
356
-
357
- <div class="features-grid">
358
- <div class="feature-card reveal">
359
- <div class="feature-icon">🎙</div>
360
- <h3>Whisper Large V3</h3>
361
- <p>Precise transcription with word-level timestamps via OpenAI's most accurate speech model.</p>
362
- </div>
363
- <div class="feature-card reveal">
364
- <div class="feature-icon">🌍</div>
365
- <h3>LLM Translation</h3>
366
- <p>Context-aware subtitle translation that preserves meaning, idioms, and technical terms.</p>
367
- </div>
368
- <div class="feature-card reveal">
369
- <div class="feature-icon">🗣️</div>
370
- <h3>Chatterbox Voice Clone</h3>
371
- <p>Zero-shot multilingual voice cloning — the translated speech sounds like the original speaker.</p>
372
- </div>
373
- <div class="feature-card reveal">
374
- <div class="feature-icon">⏱️</div>
375
- <h3>Frame-Accurate Sync</h3>
376
- <p>Dynamic time-stretching ensures the new audio perfectly aligns with the original video timing.</p>
377
- </div>
378
- <div class="feature-card reveal">
379
- <div class="feature-icon">🔗</div>
380
- <h3>URL Import</h3>
381
- <p>Paste any Instagram Reel or YouTube Short link — we'll download and process it automatically.</p>
382
- </div>
383
- <div class="feature-card reveal">
384
- <div class="feature-icon">📦</div>
385
- <h3>Lossless Export</h3>
386
- <p>Original video frames are never re-encoded. Only the audio track is replaced for maximum quality.</p>
387
- </div>
388
- </div>
389
- </div>
390
- </section>
391
-
392
- <!-- ── Demo Showcase ──────────────────────────────────── -->
393
- <section id="demos" class="section">
394
- <div class="container">
395
- <div class="section-label reveal">
396
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
397
- <polygon points="23 7 16 12 23 17 23 7" />
398
- <rect x="1" y="5" width="15" height="14" rx="2" ry="2" />
399
- </svg>
400
- DEMO SHOWCASE
401
- </div>
402
- <h2 class="section-title reveal">Hear the difference</h2>
403
- <p class="section-sub reveal">Original videos side-by-side with AI-dubbed translations. Same voice. New language.</p>
404
-
405
- <div id="showcase-container" aria-live="polite">
406
- <div class="demos-state demos-state-loading" id="showcase-loading">
407
- Loading showcase...
408
- </div>
409
- <div class="demos-state demos-state-error" id="showcase-error" hidden>
410
- Could not load showcase right now.
411
- </div>
412
- <div id="showcase-rows" hidden></div>
413
- </div>
414
-
415
- <!-- Fallback: old masonry grid if showcase is empty -->
416
- <div class="demos-wall" id="demo-videos-wall" aria-live="polite" hidden>
417
- <div class="demos-state demos-state-loading" id="demo-videos-loading" hidden>
418
- Loading demo videos...
419
- </div>
420
- <div class="demos-state demos-state-error" id="demo-videos-error" hidden>
421
- Could not load demo videos right now.
422
- </div>
423
- <div class="demos-state demos-state-empty" id="demo-videos-empty" hidden>
424
- No demo videos are available yet.
425
- </div>
426
- <div class="demos-lanes" id="demo-videos-lanes" hidden>
427
- <div class="demos-lane" data-lane="0"></div>
428
- <div class="demos-lane" data-lane="1"></div>
429
- <div class="demos-lane" data-lane="2"></div>
430
- </div>
431
- </div>
432
- </div>
433
- </section>
434
-
435
- <!-- ── App / Try Now ─────────────────────────────────── -->
436
- <section id="app" class="section section-dark">
437
- <div class="container">
438
- <div class="section-label reveal">
439
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
440
- <path
441
- d="M14.7 6.3a1 1 0 000 1.4l1.6 1.6a1 1 0 001.4 0l3.77-3.77a6 6 0 01-7.94 7.94l-6.91 6.91a2.12 2.12 0 01-3-3l6.91-6.91a6 6 0 017.94-7.94l-3.76 3.76z" />
442
- </svg>
443
- TRY IT NOW
444
- </div>
445
- <h2 class="section-title reveal">Translate your first video — free</h2>
446
- <p class="section-sub reveal">No account needed. Paste a link or upload a file. Get your translated video in under
447
- 2 minutes.</p>
448
-
449
- <div class="app-container state-input reveal">
450
- <!-- Input state -->
451
- <div class="app-panel" id="app-input">
452
- <!-- Tab switcher -->
453
- <div class="input-tabs">
454
- <button class="tab-btn active" data-tab="url" id="tab-url">
455
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
456
- <path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71" />
457
- <path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71" />
458
- </svg>
459
- Paste URL
460
- </button>
461
- <button class="tab-btn" data-tab="upload" id="tab-upload">
462
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
463
- <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12" />
464
- </svg>
465
- Upload File
466
- </button>
467
- </div>
468
-
469
- <!-- URL input -->
470
- <div class="tab-content active" id="content-url">
471
- <div class="url-input-wrap">
472
- <input type="url" id="video-url" class="url-input"
473
- placeholder="Paste Instagram Reel or YouTube Short URL..." autocomplete="off" />
474
- <div class="url-platforms">
475
- <span class="platform-tag">
476
- <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
477
- <path
478
- d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073z" />
479
- </svg>
480
- Instagram Reels
481
- </span>
482
- <span class="platform-tag">
483
- <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
484
- <path
485
- d="M23.498 6.186a3.016 3.016 0 00-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 00.502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 002.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 002.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
486
- </svg>
487
- YouTube Shorts
488
- </span>
489
- <span class="platform-tag">
490
- <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
491
- <path d="M19.59 6.69a4.83 4.83 0 01-3.77-4.25V2h-3.45v13.67a2.89 2.89 0 01-2.88 2.5 2.89 2.89 0 01-2.89-2.89 2.89 2.89 0 012.89-2.89c.28 0 .54.04.79.1v-3.5a6.37 6.37 0 00-.79-.05A6.34 6.34 0 003.15 15.2a6.34 6.34 0 0010.86 4.43V13.1a8.78 8.78 0 005.58 2v-3.44a4.85 4.85 0 01-3.77-1.2v-3.77z"/>
492
- </svg>
493
- TikTok
494
- </span>
495
- </div>
496
- </div>
497
- <!-- URL preview card -->
498
- <div class="url-preview-card" id="url-preview" style="display:none">
499
- <div class="url-preview-icon" id="url-preview-icon"></div>
500
- <div class="url-preview-details">
501
- <span class="url-preview-platform" id="url-preview-platform"></span>
502
- <span class="url-preview-link" id="url-preview-link"></span>
503
- </div>
504
- <button class="url-preview-remove" id="url-preview-remove" aria-label="Clear URL">✕</button>
505
- </div>
506
- </div>
507
-
508
- <!-- File upload -->
509
- <div class="tab-content" id="content-upload">
510
- <div class="drop-zone" id="drop-zone">
511
- <div class="drop-zone-content">
512
- <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"
513
- class="drop-icon">
514
- <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12" />
515
- </svg>
516
- <p class="drop-text">Drag & drop your video here</p>
517
- <p class="drop-hint">or click to browse · MP4, MOV, WebM · Under 60s</p>
518
- <input type="file" id="file-input" accept="video/mp4,video/quicktime,video/webm" hidden />
519
- </div>
520
- <div class="drop-zone-file" id="file-preview" style="display:none">
521
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
522
- <polygon points="23 7 16 12 23 17 23 7" />
523
- <rect x="1" y="5" width="15" height="14" rx="2" ry="2" />
524
- </svg>
525
- <span id="file-name">video.mp4</span>
526
- <button class="file-remove" id="file-remove" aria-label="Remove file">✕</button>
527
- </div>
528
- </div>
529
- <!-- Upload video preview -->
530
- <div class="video-preview" id="upload-preview" style="display:none">
531
- <div class="video-preview-inner">
532
- <video id="upload-preview-video" controls playsinline loop></video>
533
- <button class="video-preview-remove" id="upload-preview-remove" aria-label="Remove video">✕</button>
534
- </div>
535
- </div>
536
- </div>
537
-
538
- <!-- Language selector -->
539
- <div class="language-row">
540
- <div class="lang-select-wrap">
541
- <label for="source-lang">From</label>
542
- <select id="source-lang" class="lang-select">
543
- <option value="ar">Arabic</option>
544
- <option value="zh">Chinese</option>
545
- <option value="da">Danish</option>
546
- <option value="nl">Dutch</option>
547
- <option value="en" selected>English</option>
548
- <option value="fi">Finnish</option>
549
- <option value="fr">French</option>
550
- <option value="de">German</option>
551
- <option value="el">Greek</option>
552
- <option value="he">Hebrew</option>
553
- <option value="hi">Hindi</option>
554
- <option value="it">Italian</option>
555
- <option value="ja">Japanese</option>
556
- <option value="ko">Korean</option>
557
- <option value="ms">Malay</option>
558
- <option value="no">Norwegian</option>
559
- <option value="pl">Polish</option>
560
- <option value="pt">Portuguese</option>
561
- <option value="ru">Russian</option>
562
- <option value="es">Spanish</option>
563
- <option value="sw">Swahili</option>
564
- <option value="sv">Swedish</option>
565
- <option value="tr">Turkish</option>
566
- <option value="ur">Urdu</option>
567
- </select>
568
- </div>
569
- <div class="lang-swap" id="lang-swap" title="Swap languages">
570
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
571
- <path d="M7 16V4m0 12l-4-4m4 4l4-4M17 8v12m0-12l4 4m-4-4l-4 4" />
572
- </svg>
573
- </div>
574
- <div class="lang-select-wrap">
575
- <label for="target-lang">To</label>
576
- <select id="target-lang" class="lang-select">
577
- <option value="Arabic">Arabic</option>
578
- <option value="Chinese">Chinese</option>
579
- <option value="Danish">Danish</option>
580
- <option value="Dutch">Dutch</option>
581
- <option value="English">English</option>
582
- <option value="Finnish">Finnish</option>
583
- <option value="French">French</option>
584
- <option value="German">German</option>
585
- <option value="Greek">Greek</option>
586
- <option value="Hebrew">Hebrew</option>
587
- <option value="Hindi">Hindi</option>
588
- <option value="Italian">Italian</option>
589
- <option value="Japanese">Japanese</option>
590
- <option value="Korean">Korean</option>
591
- <option value="Malay">Malay</option>
592
- <option value="Norwegian">Norwegian</option>
593
- <option value="Polish">Polish</option>
594
- <option value="Portuguese">Portuguese</option>
595
- <option value="Russian">Russian</option>
596
- <option value="Spanish" selected>Spanish</option>
597
- <option value="Swahili">Swahili</option>
598
- <option value="Swedish">Swedish</option>
599
- <option value="Turkish">Turkish</option>
600
- <option value="Urdu">Urdu</option>
601
- </select>
602
- </div>
603
- </div>
604
-
605
- <!-- Voice model selector -->
606
- <div class="voice-mode-group">
607
- <label class="voice-mode-label">Voice Model</label>
608
- <div class="voice-mode-options">
609
- <label class="voice-mode-option" for="vm-preview">
610
- <input type="radio" name="voice_mode" id="vm-preview" value="preview_both" checked />
611
- <div class="voice-mode-card">
612
- <div class="voice-mode-card-inner">
613
- <span class="voice-mode-icon">🎧</span>
614
- <div>
615
- <strong>Preview both</strong>
616
- <span class="voice-mode-hint">Recommended — compare voices before full synthesis</span>
617
- </div>
618
- </div>
619
- </div>
620
- </label>
621
- <label class="voice-mode-option" for="vm-chatterbox">
622
- <input type="radio" name="voice_mode" id="vm-chatterbox" value="chatterbox" />
623
- <div class="voice-mode-card">
624
- <div class="voice-mode-card-inner">
625
- <span class="voice-mode-icon">🗣️</span>
626
- <div>
627
- <strong>Chatterbox only</strong>
628
- <span class="voice-mode-hint">Resemble AI voice clone</span>
629
- </div>
630
- </div>
631
- </div>
632
- </label>
633
- </div>
634
- </div>
635
-
636
- <!-- Captions toggle -->
637
- <div class="option-row">
638
- <label class="toggle-label" for="captions-toggle">
639
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
640
- <rect x="1" y="4" width="22" height="16" rx="2"/>
641
- <path d="M7 15h4M15 15h2M7 11h2M13 11h4"/>
642
- </svg>
643
- Burn-in captions
644
- </label>
645
- <label class="toggle-switch">
646
- <input type="checkbox" id="captions-toggle" checked />
647
- <span class="toggle-track"></span>
648
- </label>
649
- </div>
650
-
651
- <!-- Preserve background music toggle -->
652
- <div class="option-row">
653
- <label class="toggle-label" for="music-toggle">
654
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
655
- <path d="M9 18V5l12-2v13"/>
656
- <circle cx="6" cy="18" r="3"/>
657
- <circle cx="18" cy="16" r="3"/>
658
- </svg>
659
- Preserve background music
660
- </label>
661
- <label class="toggle-switch">
662
- <input type="checkbox" id="music-toggle" />
663
- <span class="toggle-track"></span>
664
- </label>
665
- </div>
666
-
667
- <!-- Submit -->
668
- <button class="btn btn-primary btn-xl btn-full" id="translate-btn">
669
- <span class="btn-text">Translate Video</span>
670
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"
671
- stroke-linecap="round" stroke-linejoin="round">
672
- <path d="M5 12h14M12 5l7 7-7 7" />
673
- </svg>
674
- </button>
675
- <p class="free-note" id="free-note">
676
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
677
- <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
678
- </svg>
679
- Your first video is <strong>completely free</strong> — no account required
680
- </p>
681
- </div>
682
-
683
- <!-- Processing state -->
684
- <div class="app-panel" id="app-processing">
685
- <div class="processing-header">
686
- <div class="processing-spinner"></div>
687
- <h3>Translating your video...</h3>
688
- <p id="processing-step">Initializing pipeline</p>
689
- </div>
690
-
691
- <div class="progress-bar-wrap">
692
- <div class="progress-bar" id="progress-bar"></div>
693
- </div>
694
-
695
- <div class="terminal-mini">
696
- <div class="terminal-bar">
697
- <div class="terminal-dots">
698
- <span class="dot red"></span><span class="dot yellow"></span><span class="dot green"></span>
699
- </div>
700
- <span class="terminal-title">pipeline output</span>
701
- </div>
702
- <div class="terminal-body terminal-scroll" id="processing-log"></div>
703
- </div>
704
-
705
- <!-- Voice Preview Panel (shown when previews are ready) -->
706
- <div id="voice-preview-panel" class="voice-preview-panel" style="display:none">
707
- <div class="voice-preview-header">
708
- <div class="voice-preview-pulse"></div>
709
- <h4>Choose your voice</h4>
710
- <p>Listen to a 30-second preview from each model, then select your favourite</p>
711
- </div>
712
- <div class="voice-cards">
713
- <div class="voice-card" data-model="chatterbox" id="voice-card-chatterbox">
714
- <span class="voice-card-badge">🗣️ Chatterbox</span>
715
- <div class="voice-card-player">
716
- <audio preload="none" id="preview-audio-chatterbox" class="preview-audio"></audio>
717
- <button class="play-pause-btn" data-target="preview-audio-chatterbox" aria-label="Play preview">
718
- <svg class="icon-play" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><polygon points="6,3 20,12 6,21"/></svg>
719
- <svg class="icon-pause" width="20" height="20" viewBox="0 0 24 24" fill="currentColor" style="display:none"><rect x="5" y="3" width="4" height="18"/><rect x="15" y="3" width="4" height="18"/></svg>
720
- </button>
721
- <div class="audio-progress">
722
- <div class="audio-progress-bar"></div>
723
- </div>
724
- <span class="audio-time">0:00</span>
725
- </div>
726
- <button class="btn btn-primary btn-sm voice-select-btn" data-model="chatterbox" id="select-chatterbox">
727
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
728
- <path d="M20 6L9 17l-5-5" />
729
- </svg>
730
- Select this voice
731
- </button>
732
- </div>
733
- </div>
734
- </div>
735
- </div>
736
-
737
- <!-- Result state -->
738
- <div class="app-panel" id="app-result">
739
- <!-- Skeleton placeholder -->
740
- <div class="result-skeleton">
741
- <div class="skel-bar" style="height: 64px; border-radius: 12px; margin-bottom: 24px;"></div>
742
- <div class="skel-bar" style="height: 380px; border-radius: 16px; margin-bottom: 24px;"></div>
743
- <div class="skel-bar" style="height: 56px; border-radius: 12px;"></div>
744
- </div>
745
-
746
- <div class="result-content-wrap" style="display:none">
747
- <div class="result-header">
748
- <div class="result-check">
749
- <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
750
- <path d="M22 11.08V12a10 10 0 11-5.93-9.14" />
751
- <path d="M22 4L12 14.01l-3-3" />
752
- </svg>
753
- </div>
754
- <h3>Translation complete!</h3>
755
- <p id="result-meta">English → Spanish · Processed in 47s</p>
756
- </div>
757
-
758
- <div class="video-player-wrap">
759
- <video id="result-video" controls playsinline class="result-video">
760
- Your browser does not support the video tag.
761
- </video>
762
- </div>
763
-
764
- <div class="result-actions">
765
- <a href="#" class="btn btn-primary btn-lg" id="download-btn" download>
766
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
767
- <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3" />
768
- </svg>
769
- Download Video
770
- </a>
771
- <button class="btn btn-ghost btn-lg" id="new-video-btn">
772
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
773
- <path d="M12 5v14M5 12h14" />
774
- </svg>
775
- Translate Another
776
- </button>
777
- </div>
778
- </div>
779
- </div>
780
- </div>
781
- </div>
782
- </section>
783
-
784
- <!-- ── Pricing ───────────────────────────────────────── -->
785
- <section id="pricing" class="section">
786
- <div class="container">
787
- <div class="section-label reveal">
788
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
789
- <line x1="12" y1="1" x2="12" y2="23" />
790
- <path d="M17 5H9.5a3.5 3.5 0 000 7h5a3.5 3.5 0 010 7H6" />
791
- </svg>
792
- PRICING
793
- </div>
794
- <h2 class="section-title reveal">Simple, creator-friendly pricing</h2>
795
- <p class="section-sub reveal">Start free. Scale when you're ready. Built for creators, not enterprises.</p>
796
-
797
- <div class="pricing-grid">
798
- <!-- Free -->
799
- <div class="pricing-card reveal">
800
- <div class="pricing-badge">Try it</div>
801
- <h3 class="pricing-name">Free</h3>
802
- <div class="pricing-price">
803
- <span class="price-amount">$0</span>
804
- </div>
805
- <p class="pricing-desc">Perfect for trying out VideoVoice with your first video.</p>
806
- <ul class="pricing-features">
807
- <li>
808
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
809
- <path d="M20 6L9 17l-5-5" />
810
- </svg>
811
- 1 free video translation
812
- </li>
813
- <li>
814
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
815
- <path d="M20 6L9 17l-5-5" />
816
- </svg>
817
- Up to 60 seconds
818
- </li>
819
- <li>
820
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
821
- <path d="M20 6L9 17l-5-5" />
822
- </svg>
823
- All 23+ languages
824
- </li>
825
- <li>
826
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
827
- <path d="M20 6L9 17l-5-5" />
828
- </svg>
829
- Full quality download
830
- </li>
831
- </ul>
832
- <a href="#app" class="btn btn-ghost btn-full">Get started</a>
833
- </div>
834
-
835
- <!-- Starter -->
836
- <div class="pricing-card pricing-popular reveal">
837
- <div class="pricing-badge popular-badge">Most popular</div>
838
- <h3 class="pricing-name">Starter</h3>
839
- <div class="pricing-price">
840
- <span class="price-amount">$4.99</span>
841
- <span class="price-period">/month</span>
842
- </div>
843
- <p class="pricing-desc">For creators who need regular translations for their content.</p>
844
- <ul class="pricing-features">
845
- <li>
846
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
847
- <path d="M20 6L9 17l-5-5" />
848
- </svg>
849
- 10 videos per month
850
- </li>
851
- <li>
852
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
853
- <path d="M20 6L9 17l-5-5" />
854
- </svg>
855
- Up to 60 seconds each
856
- </li>
857
- <li>
858
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
859
- <path d="M20 6L9 17l-5-5" />
860
- </svg>
861
- Priority processing queue
862
- </li>
863
- <li>
864
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
865
- <path d="M20 6L9 17l-5-5" />
866
- </svg>
867
- HD quality export
868
- </li>
869
- <li>
870
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
871
- <path d="M20 6L9 17l-5-5" />
872
- </svg>
873
- Email support
874
- </li>
875
- </ul>
876
- <a href="#" class="btn btn-primary btn-full" id="buy-starter">Get Starter</a>
877
- </div>
878
-
879
- <!-- Creator -->
880
- <div class="pricing-card reveal">
881
- <div class="pricing-badge">Best value</div>
882
- <h3 class="pricing-name">Creator</h3>
883
- <div class="pricing-price">
884
- <span class="price-amount">$14.99</span>
885
- <span class="price-period">/month</span>
886
- </div>
887
- <p class="pricing-desc">For professional creators and agencies scaling multilingual content.</p>
888
- <ul class="pricing-features">
889
- <li>
890
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
891
- <path d="M20 6L9 17l-5-5" />
892
- </svg>
893
- 50 videos per month
894
- </li>
895
- <li>
896
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
897
- <path d="M20 6L9 17l-5-5" />
898
- </svg>
899
- Up to 60 seconds each
900
- </li>
901
- <li>
902
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
903
- <path d="M20 6L9 17l-5-5" />
904
- </svg>
905
- Fastest processing
906
- </li>
907
- <li>
908
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
909
- <path d="M20 6L9 17l-5-5" />
910
- </svg>
911
- All 23+ languages
912
- </li>
913
- <li>
914
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
915
- <path d="M20 6L9 17l-5-5" />
916
- </svg>
917
- Priority support
918
- </li>
919
- <li>
920
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
921
- <path d="M20 6L9 17l-5-5" />
922
- </svg>
923
- API access (coming soon)
924
- </li>
925
- </ul>
926
- <a href="#" class="btn btn-ghost btn-full" id="buy-creator">Get Creator</a>
927
- </div>
928
- </div>
929
- </div>
930
- </section>
931
-
932
- <!-- ── CTA ───────────────────────────────────────────── -->
933
- <section class="cta-section">
934
- <div class="container">
935
- <div class="cta-card reveal">
936
- <h2>Ready to break the language barrier?</h2>
937
- <p>Translate your first video for free. No account needed. Takes under 2 minutes.</p>
938
- <a href="#app" class="btn btn-primary btn-lg">
939
- Try VideoVoice Now
940
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
941
- <path d="M5 12h14M12 5l7 7-7 7" />
942
- </svg>
943
- </a>
944
- </div>
945
- </div>
946
- </section>
947
-
948
- <!-- ── Footer ────────────────────────────────────────── -->
949
- <footer class="footer">
950
- <div class="container footer-inner">
951
- <div class="footer-brand">
952
- <a href="#" class="nav-logo">
953
- <span class="logo-icon">▶</span>
954
- <span class="logo-text">VideoVoice</span>
955
- </a>
956
- <p>AI-powered video translation with voice cloning.<br />Same creator. New language. Seconds.</p>
957
- </div>
958
- <div class="footer-links">
959
- <div class="footer-col">
960
- <h4>Product</h4>
961
- <a href="#how-it-works">How it works</a>
962
- <a href="#demos">Demos</a>
963
- <a href="#app">Try now</a>
964
- <a href="#pricing">Pricing</a>
965
- </div>
966
- <div class="footer-col">
967
- <h4>Legal</h4>
968
- <a href="#">Privacy Policy</a>
969
- <a href="#">Terms of Service</a>
970
- </div>
971
- <div class="footer-col">
972
- <h4>Contact</h4>
973
- <a href="mailto:hello@videovoice.ai">hello@videovoice.ai</a>
974
- </div>
975
- </div>
976
- </div>
977
- <div class="container footer-bottom">
978
- <p>&copy; 2026 VideoVoice. All rights reserved.</p>
979
- </div>
980
- </footer>
981
-
982
- <!-- ── Command Palette (⌘K) ──────────────────────────── -->
983
- <div class="cmd-palette-overlay" id="cmd-palette-overlay">
984
- <div class="cmd-palette">
985
- <div class="cmd-search-wrap">
986
- <svg class="cmd-search-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
987
- stroke-width="2">
988
- <circle cx="11" cy="11" r="8" />
989
- <path d="M21 21l-4.35-4.35" />
990
- </svg>
991
- <input type="text" class="cmd-search" id="cmd-search" placeholder="Search actions, navigate, set language..."
992
- autocomplete="off" />
993
- <span class="cmd-kbd">ESC</span>
994
- </div>
995
- <div class="cmd-results" id="cmd-results"></div>
996
- <div class="cmd-footer">
997
- <div class="cmd-footer-keys">
998
- <kbd>↑</kbd><kbd>↓</kbd> <span>to navigate</span>
999
- </div>
1000
- <div class="cmd-footer-keys">
1001
- <kbd>↵</kbd> <span>to select</span>
1002
- </div>
1003
- <div class="cmd-footer-keys">
1004
- <kbd>esc</kbd> <span>to close</span>
1005
- </div>
1006
- </div>
1007
- </div>
1008
- </div>
1009
-
1010
- <!-- ── Video phone tracks script ── -->
1011
- <script>
1012
- (function () {
1013
- const vids = [
1014
- { src: 'https://videos.pexels.com/video-files/3571264/3571264-hd_1280_720_30fps.mp4', platform: 'tiktok', handle: '@miachef', desc: 'Homemade pasta from scratch 🍝', lang: 'ES → EN' },
1015
- { src: 'https://videos.pexels.com/video-files/4064155/4064155-hd_1280_720_25fps.mp4', platform: 'reels', handle: '@seoulfashion', desc: 'K-fashion haul 2025 🛍️', lang: 'KR → FR' },
1016
- { src: 'https://videos.pexels.com/video-files/5329430/5329430-hd_1280_720_30fps.mp4', platform: 'shorts', handle: '@priya.fit', desc: '5-min morning routine 🔥', lang: 'HI → EN' },
1017
- { src: 'https://videos.pexels.com/video-files/3209828/3209828-hd_1280_720_25fps.mp4', platform: 'tiktok', handle: '@food.tales', desc: 'Street food in Mexico City 🌮', lang: 'PT → EN' },
1018
- { src: 'https://videos.pexels.com/video-files/6804077/6804077-hd_1280_720_30fps.mp4', platform: 'reels', handle: '@linabeauty', desc: 'Skincare routine that changed my life ✨', lang: 'FR → EN' },
1019
- { src: 'https://videos.pexels.com/video-files/5698697/5698697-hd_1280_720_25fps.mp4', platform: 'shorts', handle: '@maya.style', desc: 'Thrift flip challenge 💫', lang: 'DE → EN' },
1020
- { src: 'https://videos.pexels.com/video-files/3195394/3195394-hd_1280_720_25fps.mp4', platform: 'tiktok', handle: '@chefmarco', desc: 'Risotto trick no one tells you 🍚', lang: 'IT → EN' },
1021
- { src: 'https://videos.pexels.com/video-files/4126542/4126542-hd_1280_720_30fps.mp4', platform: 'reels', handle: '@lara.beats', desc: 'New single just dropped 🎵', lang: 'AR → EN' },
1022
- { src: 'https://videos.pexels.com/video-files/5713888/5713888-hd_1280_720_25fps.mp4', platform: 'shorts', handle: '@dancestudio', desc: 'Viral choreography tutorial 💃', lang: 'JP → EN' },
1023
- { src: 'https://videos.pexels.com/video-files/3048527/3048527-hd_1280_720_25fps.mp4', platform: 'tiktok', handle: '@zenwithyoga', desc: 'Morning flow for beginners 🧘', lang: 'ZH → EN' },
1024
- { src: 'https://videos.pexels.com/video-files/4101665/4101665-hd_1280_720_30fps.mp4', platform: 'reels', handle: '@alexlifts', desc: 'Clean bulk meal prep 💪', lang: 'RU → EN' },
1025
- { src: 'https://videos.pexels.com/video-files/856797/856797-hd_1280_720_30fps.mp4', platform: 'shorts', handle: '@wanderlust', desc: 'Bali hidden beaches 🌊', lang: 'ID → EN' },
1026
- ];
1027
-
1028
- const svgHeart = `<svg viewBox="0 0 24 24" fill="white" opacity="0.9"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>`;
1029
- const svgShare = `<svg viewBox="0 0 24 24" fill="white" opacity="0.9"><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z"/></svg>`;
1030
- const svgMute = `<svg viewBox="0 0 24 24" fill="rgba(255,255,255,0.7)"><path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/></svg>`;
1031
-
1032
- const platClass = { tiktok: 'vid-p-tiktok', reels: 'vid-p-reels', shorts: 'vid-p-shorts' };
1033
- const platLabel = { tiktok: 'TikTok', reels: 'Reels', shorts: 'Shorts' };
1034
-
1035
- function makePhone(data) {
1036
- const el = document.createElement('div');
1037
- el.className = 'vid-phone';
1038
- el.innerHTML = `
1039
- <video src="${data.src}" autoplay muted loop playsinline preload="none"></video>
1040
- <div class="vid-phone-scrim"></div>
1041
- <span class="vid-badge-plat ${platClass[data.platform]}">${platLabel[data.platform]}</span>
1042
- <div class="vid-muted-icon">${svgMute}</div>
1043
- <div class="vid-xlate">
1044
- <div class="vid-xlate-dot"></div>
1045
- ${data.lang}
1046
- <div class="vid-wave"><b></b><b></b><b></b><b></b><b></b></div>
1047
- </div>
1048
- <div class="vid-phone-actions">
1049
- <div class="vid-act-btn">${svgHeart}</div>
1050
- <div class="vid-act-btn">${svgShare}</div>
1051
- </div>
1052
- <div class="vid-info">
1053
- <div class="vid-handle">${data.handle}</div>
1054
- <div class="vid-desc">${data.desc}</div>
1055
- </div>`;
1056
- el.querySelector('video').addEventListener('error', () => { el.style.display = 'none'; });
1057
- return el;
1058
- }
1059
-
1060
- function fillTrack(railAId, railBId, items) {
1061
- const a = document.getElementById(railAId);
1062
- const b = document.getElementById(railBId);
1063
- [...items, ...items].forEach(d => { a.appendChild(makePhone(d)); b.appendChild(makePhone(d)); });
1064
- }
1065
-
1066
- fillTrack('vt-r1a', 'vt-r1b', vids.slice(0, 4));
1067
- fillTrack('vt-r2a', 'vt-r2b', vids.slice(4, 8));
1068
- fillTrack('vt-r3a', 'vt-r3b', vids.slice(8, 12));
1069
-
1070
- // Stagger translation pill animations
1071
- document.querySelectorAll('.vid-xlate').forEach((x, i) => {
1072
- const onDelay = (i * 1300 + Math.random() * 800) % 7000;
1073
- function show() { x.classList.add('on'); setTimeout(hide, 2800 + Math.random() * 1000); }
1074
- function hide() { x.classList.remove('on'); setTimeout(show, 3000 + Math.random() * 2000); }
1075
- setTimeout(show, onDelay);
1076
- });
1077
-
1078
- // Pause scroll on hover
1079
- document.querySelectorAll('.vid-phone').forEach(p => {
1080
- p.addEventListener('mouseenter', () => document.querySelectorAll('.vid-tracks .rail').forEach(r => r.style.animationPlayState = 'paused'));
1081
- p.addEventListener('mouseleave', () => document.querySelectorAll('.vid-tracks .rail').forEach(r => r.style.animationPlayState = 'running'));
1082
- });
1083
- })();
1084
- </script>
1085
-
1086
- <script src="app.js?v=20260402d"></script>
1087
- </body>
1088
-
1089
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/style.css DELETED
@@ -1,3469 +0,0 @@
1
- /* ════════════════════════════════════════════════════════════
2
- VideoVoice — Production Design System
3
- Inspired by Sploink.ai: deep black, orange accent, terminal UI
4
- ════════════════════════════════════════════════════════════ */
5
-
6
- *,
7
- *::before,
8
- *::after {
9
- box-sizing: border-box;
10
- margin: 0;
11
- padding: 0;
12
- }
13
-
14
- :root {
15
- /* ── Palette ─────────────────── */
16
- --bg: #07090f;
17
- --bg-elevated: #0d1020;
18
- --surface: #121526;
19
- --surface-2: #171c32;
20
- --border: rgba(255, 255, 255, 0.08);
21
- --border-hover: rgba(255, 255, 255, 0.18);
22
-
23
- /* Accent */
24
- --accent: #5b6ef5;
25
- --accent2: #a78bfa;
26
- --accent-hover: #7b8df7;
27
- --accent-glow: rgba(91, 110, 245, 0.38);
28
- --accent-dim: rgba(91, 110, 245, 0.12);
29
-
30
- /* Semantic */
31
- --green: #5bf0a0;
32
- --green-dim: rgba(91, 240, 160, 0.12);
33
- --blue: #5b6ef5;
34
- --red: #ef4444;
35
- --yellow: #eab308;
36
-
37
- /* Text */
38
- --text: #f0f0f6;
39
- --text-2: rgba(240, 240, 246, 0.55);
40
- --text-3: rgba(240, 240, 246, 0.28);
41
-
42
- /* Geometry */
43
- --radius: 12px;
44
- --radius-lg: 16px;
45
- --radius-xl: 20px;
46
- --container: 1120px;
47
-
48
- /* Type */
49
- --font: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
50
- --font-display: 'Syne', var(--font);
51
- --font-mono: 'JetBrains Mono', 'Fira Code', 'SF Mono', monospace;
52
- }
53
-
54
- html {
55
- scroll-behavior: smooth;
56
- -webkit-font-smoothing: antialiased;
57
- -moz-osx-font-smoothing: grayscale;
58
- }
59
-
60
- body {
61
- background: var(--bg);
62
- color: var(--text);
63
- font-family: var(--font);
64
- font-size: 15px;
65
- line-height: 1.6;
66
- overflow-x: hidden;
67
- }
68
-
69
- /* ── Selection ─────────────────────────────────────────── */
70
- ::selection {
71
- background: var(--accent);
72
- color: #fff;
73
- }
74
-
75
- /* ════════════════════════════════════════════════════════
76
- LAYOUT
77
- ════════════════════════════════════════════════════════ */
78
- .container {
79
- max-width: var(--container);
80
- margin: 0 auto;
81
- padding: 0 24px;
82
- }
83
-
84
- /* ════════════════════════════════════════════════════════
85
- DOT GRID BACKGROUND
86
- ════════════════════════════════════════════════════════ */
87
- .dot-grid {
88
- position: fixed;
89
- inset: 0;
90
- z-index: 0;
91
- pointer-events: none;
92
- background-image: radial-gradient(circle, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
93
- background-size: 32px 32px;
94
- }
95
-
96
- /* ════════════════════════════════════════════════════════
97
- NAVIGATION
98
- ════════════════════════════════════════════════════════ */
99
- .nav {
100
- position: fixed;
101
- top: 0;
102
- left: 0;
103
- right: 0;
104
- z-index: 1000;
105
- background: rgba(7, 9, 15, 0.85);
106
- backdrop-filter: blur(20px) saturate(1.5);
107
- -webkit-backdrop-filter: blur(20px) saturate(1.5);
108
- border-bottom: 1px solid var(--border);
109
- transition: background .3s, box-shadow .3s;
110
- }
111
-
112
- .nav.scrolled {
113
- background: rgba(7, 9, 15, 0.97);
114
- box-shadow: 0 4px 30px rgba(0, 0, 0, .4);
115
- }
116
-
117
- .nav-inner {
118
- max-width: var(--container);
119
- margin: 0 auto;
120
- padding: 0 24px;
121
- height: 64px;
122
- display: flex;
123
- align-items: center;
124
- justify-content: space-between;
125
- }
126
-
127
- /* Logo */
128
- .nav-logo {
129
- display: flex;
130
- align-items: center;
131
- gap: 10px;
132
- text-decoration: none;
133
- font-weight: 700;
134
- font-size: 1.15rem;
135
- color: var(--text);
136
- transition: opacity .2s;
137
- }
138
-
139
- .nav-logo:hover {
140
- opacity: .8;
141
- }
142
-
143
- .logo-icon {
144
- display: flex;
145
- align-items: center;
146
- justify-content: center;
147
- width: 32px;
148
- height: 32px;
149
- background: linear-gradient(135deg, var(--accent), var(--accent-hover));
150
- border-radius: 8px;
151
- font-size: .85rem;
152
- color: #fff;
153
- }
154
-
155
- .logo-text {
156
- font-family: var(--font-display);
157
- letter-spacing: -0.02em;
158
- }
159
-
160
- /* Links */
161
- .nav-links {
162
- display: flex;
163
- align-items: center;
164
- gap: 8px;
165
- }
166
-
167
- .nav-links a {
168
- color: var(--text-2);
169
- text-decoration: none;
170
- font-size: .875rem;
171
- font-weight: 500;
172
- padding: 6px 14px;
173
- border-radius: 8px;
174
- transition: color .2s, background .2s;
175
- }
176
-
177
- .nav-links a:hover {
178
- color: var(--text);
179
- background: rgba(255, 255, 255, 0.05);
180
- }
181
-
182
- /* Mobile menu */
183
- .mobile-menu-btn {
184
- display: none;
185
- flex-direction: column;
186
- gap: 5px;
187
- background: none;
188
- border: none;
189
- cursor: pointer;
190
- padding: 4px;
191
- }
192
-
193
- .mobile-menu-btn span {
194
- display: block;
195
- width: 22px;
196
- height: 2px;
197
- background: var(--text-2);
198
- border-radius: 2px;
199
- transition: all .3s;
200
- }
201
-
202
- .mobile-menu-btn.active span:nth-child(1) {
203
- transform: translateY(7px) rotate(45deg);
204
- }
205
-
206
- .mobile-menu-btn.active span:nth-child(2) {
207
- opacity: 0;
208
- }
209
-
210
- .mobile-menu-btn.active span:nth-child(3) {
211
- transform: translateY(-7px) rotate(-45deg);
212
- }
213
-
214
- .mobile-nav {
215
- display: none;
216
- flex-direction: column;
217
- gap: 4px;
218
- padding: 12px 24px 20px;
219
- background: rgba(10, 10, 10, 0.98);
220
- border-bottom: 1px solid var(--border);
221
- }
222
-
223
- .mobile-nav.open {
224
- display: flex;
225
- }
226
-
227
- .mobile-nav a {
228
- color: var(--text-2);
229
- text-decoration: none;
230
- padding: 12px 0;
231
- font-size: .95rem;
232
- border-bottom: 1px solid var(--border);
233
- }
234
-
235
- .mobile-nav a:last-child {
236
- border: none;
237
- }
238
-
239
- /* ⌘K Trigger */
240
- .cmd-trigger-btn {
241
- display: flex !important;
242
- align-items: center;
243
- gap: 6px;
244
- padding: 5px 10px !important;
245
- background: var(--surface) !important;
246
- border: 1px solid var(--border) !important;
247
- border-radius: 8px !important;
248
- font-size: .75rem !important;
249
- color: var(--text-3) !important;
250
- cursor: pointer;
251
- transition: all .2s !important;
252
- }
253
-
254
- .cmd-trigger-btn:hover {
255
- border-color: var(--border-hover) !important;
256
- color: var(--text-2) !important;
257
- background: var(--surface-2) !important;
258
- }
259
-
260
- .cmd-trigger-kbd {
261
- font-family: var(--font-mono);
262
- font-size: .65rem;
263
- opacity: .6;
264
- }
265
-
266
- /* ════════════════════════════════════════════════════════
267
- BUTTONS
268
- ════════════════════════════════════════════════════════ */
269
- .btn {
270
- display: inline-flex;
271
- align-items: center;
272
- gap: 8px;
273
- padding: 10px 24px;
274
- border-radius: 50px;
275
- font-size: .875rem;
276
- font-weight: 500;
277
- font-family: var(--font);
278
- text-decoration: none;
279
- cursor: pointer;
280
- border: none;
281
- transition: transform .15s, box-shadow .15s, color .15s, border-color .15s;
282
- }
283
-
284
- .btn-primary {
285
- background: var(--accent);
286
- color: #fff;
287
- box-shadow: 0 0 32px var(--accent-glow);
288
- }
289
-
290
- .btn-primary:hover {
291
- transform: translateY(-2px);
292
- box-shadow: 0 0 50px rgba(91, 110, 245, 0.65);
293
- }
294
-
295
- .btn-ghost {
296
- background: transparent;
297
- color: var(--text-2);
298
- border: 1px solid rgba(255, 255, 255, 0.12);
299
- }
300
-
301
- .btn-ghost:hover {
302
- color: var(--text);
303
- border-color: rgba(255, 255, 255, 0.25);
304
- }
305
-
306
- /* Size variants */
307
- .btn-sm {
308
- padding: 7px 16px;
309
- font-size: .8rem;
310
- }
311
-
312
- .btn-lg {
313
- padding: 12px 28px;
314
- font-size: 15px;
315
- }
316
-
317
- .btn-xl {
318
- padding: 18px 40px;
319
- font-size: 1.05rem;
320
- }
321
-
322
- .btn-full {
323
- width: 100%;
324
- justify-content: center;
325
- }
326
-
327
- /* ════════════════════════════════════════════════════════
328
- HERO SECTION
329
- ════════════════════════════════════════════════════════ */
330
- .hero {
331
- position: relative;
332
- width: 100vw;
333
- height: 100vh;
334
- overflow: hidden;
335
- display: flex;
336
- align-items: center;
337
- justify-content: center;
338
- }
339
-
340
- /* Hero inner — centered content */
341
- .hero-inner {
342
- position: relative;
343
- z-index: 5;
344
- text-align: center;
345
- padding: 0 20px;
346
- max-width: 740px;
347
- animation: hero-rise 1s cubic-bezier(0.22, 1, 0.36, 1) 0.2s both;
348
- }
349
-
350
- @keyframes hero-rise {
351
- from {
352
- opacity: 0;
353
- transform: translateY(28px);
354
- }
355
-
356
- to {
357
- opacity: 1;
358
- transform: translateY(0);
359
- }
360
- }
361
-
362
- /* Live badge */
363
- .live-badge {
364
- display: inline-flex;
365
- align-items: center;
366
- gap: 7px;
367
- font-size: 11px;
368
- font-weight: 500;
369
- letter-spacing: 0.7px;
370
- text-transform: uppercase;
371
- color: var(--accent2);
372
- border: 1px solid rgba(167, 139, 250, 0.28);
373
- padding: 5px 14px;
374
- border-radius: 20px;
375
- background: rgba(167, 139, 250, 0.07);
376
- margin-bottom: 26px;
377
- }
378
-
379
- .live-badge::before {
380
- content: '';
381
- width: 6px;
382
- height: 6px;
383
- border-radius: 50%;
384
- background: var(--green);
385
- box-shadow: 0 0 6px var(--green);
386
- animation: live-pulse 2s ease infinite;
387
- }
388
-
389
- @keyframes live-pulse {
390
-
391
- 0%,
392
- 100% {
393
- opacity: 1;
394
- }
395
-
396
- 50% {
397
- opacity: 0.35;
398
- }
399
- }
400
-
401
- /* Heading */
402
- .hero-inner h1 {
403
- font-family: var(--font-display);
404
- font-weight: 800;
405
- font-size: clamp(34px, 5.8vw, 70px);
406
- line-height: 1.05;
407
- color: var(--text);
408
- letter-spacing: -0.03em;
409
- margin-bottom: 20px;
410
- }
411
-
412
- .hero-inner h1 em {
413
- font-style: normal;
414
- background: linear-gradient(110deg, #5b6ef5 0%, #a78bfa 45%, #f472b6 100%);
415
- -webkit-background-clip: text;
416
- -webkit-text-fill-color: transparent;
417
- background-clip: text;
418
- }
419
-
420
- /* Sub */
421
- .hero-sub {
422
- font-size: clamp(13px, 1.5vw, 17px);
423
- color: var(--text-2);
424
- line-height: 1.7;
425
- max-width: 500px;
426
- margin: 0 auto 36px;
427
- font-weight: 300;
428
- }
429
-
430
- /* CTAs */
431
- .hero-cta {
432
- display: flex;
433
- align-items: center;
434
- justify-content: center;
435
- gap: 12px;
436
- flex-wrap: wrap;
437
- }
438
-
439
- /* Stats row */
440
- .hero-stats {
441
- display: flex;
442
- align-items: center;
443
- justify-content: center;
444
- gap: 28px;
445
- margin-top: 44px;
446
- flex-wrap: wrap;
447
- }
448
-
449
- .hero-stat {
450
- text-align: center;
451
- }
452
-
453
- .hero-stat-n {
454
- font-family: var(--font-display);
455
- font-size: 22px;
456
- font-weight: 700;
457
- color: var(--text);
458
- }
459
-
460
- .hero-stat-l {
461
- font-size: 10px;
462
- color: var(--text-2);
463
- text-transform: uppercase;
464
- letter-spacing: 0.6px;
465
- margin-top: 2px;
466
- }
467
-
468
- .hero-stat-div {
469
- width: 1px;
470
- height: 30px;
471
- background: rgba(255, 255, 255, 0.1);
472
- }
473
-
474
- /* Scroll hint */
475
- .hero-scroll-hint {
476
- position: absolute;
477
- bottom: 24px;
478
- left: 50%;
479
- transform: translateX(-50%);
480
- z-index: 6;
481
- display: flex;
482
- flex-direction: column;
483
- align-items: center;
484
- gap: 5px;
485
- opacity: 0.3;
486
- }
487
-
488
- .hero-scroll-hint span {
489
- font-size: 9px;
490
- color: #fff;
491
- text-transform: uppercase;
492
- letter-spacing: 1.2px;
493
- }
494
-
495
- .hero-scroll-arr {
496
- width: 18px;
497
- height: 18px;
498
- border-right: 1.5px solid #fff;
499
- border-bottom: 1.5px solid #fff;
500
- transform: rotate(45deg);
501
- animation: scroll-bounce 1.4s ease infinite;
502
- }
503
-
504
- @keyframes scroll-bounce {
505
-
506
- 0%,
507
- 100% {
508
- transform: rotate(45deg) translateY(0);
509
- }
510
-
511
- 50% {
512
- transform: rotate(45deg) translateY(5px);
513
- }
514
- }
515
-
516
- /* ── Video Phone Tracks (hero background) ──────────────── */
517
- .vid-tracks {
518
- position: absolute;
519
- inset: 0;
520
- display: flex;
521
- flex-direction: column;
522
- justify-content: center;
523
- gap: 28px;
524
- transform: rotate(-6deg) scale(1.18);
525
- transform-origin: center center;
526
- z-index: 0;
527
- pointer-events: none;
528
- overflow: hidden;
529
- }
530
-
531
- .vid-tracks .track {
532
- display: flex;
533
- width: 100%;
534
- overflow: hidden;
535
- gap: 20px;
536
- }
537
-
538
- .vid-tracks .rail {
539
- display: flex;
540
- gap: 20px;
541
- flex-shrink: 0;
542
- }
543
-
544
- .vid-tracks .track-1 .rail {
545
- animation: vt-scroll-left 28s linear infinite;
546
- }
547
-
548
- .vid-tracks .track-2 .rail {
549
- animation: vt-scroll-right 22s linear infinite;
550
- }
551
-
552
- .vid-tracks .track-3 .rail {
553
- animation: vt-scroll-left 32s linear infinite;
554
- animation-delay: -8s;
555
- }
556
-
557
- @keyframes vt-scroll-left {
558
- from {
559
- transform: translateX(0);
560
- }
561
-
562
- to {
563
- transform: translateX(-50%);
564
- }
565
- }
566
-
567
- @keyframes vt-scroll-right {
568
- from {
569
- transform: translateX(-50%);
570
- }
571
-
572
- to {
573
- transform: translateX(0);
574
- }
575
- }
576
-
577
- .vid-phone {
578
- position: relative;
579
- flex-shrink: 0;
580
- width: 155px;
581
- height: 275px;
582
- border-radius: 20px;
583
- background: #0d1020;
584
- border: 1.5px solid rgba(255, 255, 255, 0.10);
585
- overflow: hidden;
586
- box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.55), 0 24px 50px rgba(0, 0, 0, 0.55);
587
- }
588
-
589
- .vid-phone::after {
590
- content: '';
591
- position: absolute;
592
- top: 9px;
593
- left: 50%;
594
- transform: translateX(-50%);
595
- width: 46px;
596
- height: 5px;
597
- background: rgba(0, 0, 0, 0.6);
598
- border-radius: 10px;
599
- z-index: 20;
600
- }
601
-
602
- .vid-phone video {
603
- position: absolute;
604
- inset: 0;
605
- width: 100%;
606
- height: 100%;
607
- object-fit: cover;
608
- object-position: center top;
609
- border-radius: 20px;
610
- }
611
-
612
- .vid-phone-scrim {
613
- position: absolute;
614
- bottom: 0;
615
- left: 0;
616
- right: 0;
617
- height: 55%;
618
- background: linear-gradient(transparent, rgba(0, 0, 0, 0.78));
619
- z-index: 5;
620
- pointer-events: none;
621
- }
622
-
623
- .vid-badge-plat {
624
- position: absolute;
625
- top: 16px;
626
- left: 10px;
627
- font-size: 7.5px;
628
- font-weight: 600;
629
- letter-spacing: 0.5px;
630
- padding: 2px 7px;
631
- border-radius: 4px;
632
- text-transform: uppercase;
633
- z-index: 10;
634
- backdrop-filter: blur(6px);
635
- }
636
-
637
- .vid-p-tiktok {
638
- background: rgba(0, 0, 0, 0.50);
639
- color: #fff;
640
- border: 1px solid rgba(255, 255, 255, 0.2);
641
- }
642
-
643
- .vid-p-reels {
644
- background: rgba(200, 50, 150, 0.4);
645
- color: #ffb0df;
646
- border: 1px solid rgba(255, 100, 200, 0.25);
647
- }
648
-
649
- .vid-p-shorts {
650
- background: rgba(200, 30, 30, 0.4);
651
- color: #ffaaaa;
652
- border: 1px solid rgba(255, 80, 80, 0.25);
653
- }
654
-
655
- .vid-xlate {
656
- position: absolute;
657
- top: 30px;
658
- left: 8px;
659
- right: 8px;
660
- background: rgba(91, 110, 245, 0.82);
661
- backdrop-filter: blur(5px);
662
- border-radius: 7px;
663
- padding: 4px 8px;
664
- font-size: 8px;
665
- color: #fff;
666
- display: flex;
667
- align-items: center;
668
- gap: 5px;
669
- z-index: 11;
670
- opacity: 0;
671
- transform: translateY(-3px);
672
- transition: opacity 0.35s ease, transform 0.35s ease;
673
- }
674
-
675
- .vid-xlate.on {
676
- opacity: 1;
677
- transform: translateY(0);
678
- }
679
-
680
- .vid-xlate-dot {
681
- width: 5px;
682
- height: 5px;
683
- border-radius: 50%;
684
- background: #5bf0a0;
685
- flex-shrink: 0;
686
- animation: vt-blink 1s ease infinite;
687
- }
688
-
689
- @keyframes vt-blink {
690
-
691
- 0%,
692
- 100% {
693
- opacity: 1;
694
- }
695
-
696
- 50% {
697
- opacity: 0.3;
698
- }
699
- }
700
-
701
- .vid-wave {
702
- display: flex;
703
- align-items: center;
704
- gap: 1.5px;
705
- margin-left: auto;
706
- }
707
-
708
- .vid-wave b {
709
- display: block;
710
- width: 2px;
711
- background: rgba(255, 255, 255, 0.9);
712
- border-radius: 1px;
713
- animation: vt-wave 0.7s ease-in-out infinite alternate;
714
- }
715
-
716
- .vid-wave b:nth-child(1) {
717
- height: 3px;
718
- animation-delay: 0s;
719
- }
720
-
721
- .vid-wave b:nth-child(2) {
722
- height: 7px;
723
- animation-delay: 0.12s;
724
- }
725
-
726
- .vid-wave b:nth-child(3) {
727
- height: 4px;
728
- animation-delay: 0.24s;
729
- }
730
-
731
- .vid-wave b:nth-child(4) {
732
- height: 8px;
733
- animation-delay: 0.08s;
734
- }
735
-
736
- .vid-wave b:nth-child(5) {
737
- height: 3px;
738
- animation-delay: 0.20s;
739
- }
740
-
741
- @keyframes vt-wave {
742
- from {
743
- transform: scaleY(0.35);
744
- }
745
-
746
- to {
747
- transform: scaleY(1);
748
- }
749
- }
750
-
751
- .vid-phone-actions {
752
- position: absolute;
753
- right: 8px;
754
- bottom: 64px;
755
- display: flex;
756
- flex-direction: column;
757
- align-items: center;
758
- gap: 12px;
759
- z-index: 10;
760
- }
761
-
762
- .vid-act-btn {
763
- width: 30px;
764
- height: 30px;
765
- border-radius: 50%;
766
- background: rgba(255, 255, 255, 0.18);
767
- display: flex;
768
- align-items: center;
769
- justify-content: center;
770
- }
771
-
772
- .vid-act-btn svg {
773
- width: 13px;
774
- height: 13px;
775
- }
776
-
777
- .vid-info {
778
- position: absolute;
779
- bottom: 0;
780
- left: 0;
781
- right: 0;
782
- padding: 0 10px 10px;
783
- z-index: 10;
784
- }
785
-
786
- .vid-handle {
787
- font-size: 9.5px;
788
- font-weight: 600;
789
- color: #fff;
790
- margin-bottom: 3px;
791
- }
792
-
793
- .vid-desc {
794
- font-size: 8px;
795
- color: rgba(255, 255, 255, 0.72);
796
- line-height: 1.3;
797
- }
798
-
799
- .vid-muted-icon {
800
- position: absolute;
801
- top: 16px;
802
- right: 10px;
803
- width: 18px;
804
- height: 18px;
805
- background: rgba(0, 0, 0, 0.45);
806
- border-radius: 50%;
807
- display: flex;
808
- align-items: center;
809
- justify-content: center;
810
- z-index: 12;
811
- }
812
-
813
- .vid-muted-icon svg {
814
- width: 10px;
815
- height: 10px;
816
- fill: rgba(255, 255, 255, 0.7);
817
- }
818
-
819
- .vid-vignette {
820
- position: absolute;
821
- inset: 0;
822
- z-index: 1;
823
- background:
824
- radial-gradient(ellipse 50% 60% at 50% 50%, rgba(7, 9, 15, 0.88) 20%, transparent 72%),
825
- radial-gradient(ellipse 100% 30% at 50% 0%, rgba(7, 9, 15, 0.95) 0%, transparent 100%),
826
- radial-gradient(ellipse 100% 30% at 50% 100%, rgba(7, 9, 15, 0.95) 0%, transparent 100%);
827
- pointer-events: none;
828
- }
829
-
830
- .vid-grain {
831
- position: absolute;
832
- inset: 0;
833
- z-index: 2;
834
- opacity: 0.04;
835
- background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
836
- background-repeat: repeat;
837
- background-size: 200px;
838
- pointer-events: none;
839
- }
840
-
841
- /* ── Terminal Preview ─────────────────────────────────── */
842
- .terminal-preview {
843
- background: var(--surface);
844
- border: 1px solid var(--border);
845
- border-radius: var(--radius-lg);
846
- overflow: hidden;
847
- max-width: 720px;
848
- margin: 0 auto;
849
- box-shadow:
850
- 0 0 0 1px rgba(255, 255, 255, 0.03),
851
- 0 20px 60px rgba(0, 0, 0, 0.5),
852
- 0 0 80px rgba(91, 110, 245, 0.04);
853
- }
854
-
855
- .terminal-bar {
856
- display: flex;
857
- align-items: center;
858
- justify-content: space-between;
859
- padding: 12px 16px;
860
- background: var(--bg-elevated);
861
- border-bottom: 1px solid var(--border);
862
- }
863
-
864
- .terminal-dots {
865
- display: flex;
866
- gap: 7px;
867
- }
868
-
869
- .terminal-dots .dot {
870
- width: 12px;
871
- height: 12px;
872
- border-radius: 50%;
873
- }
874
-
875
- .dot.red {
876
- background: #ff5f57;
877
- }
878
-
879
- .dot.yellow {
880
- background: #febc2e;
881
- }
882
-
883
- .dot.green {
884
- background: #28c840;
885
- }
886
-
887
- .terminal-title {
888
- font-family: var(--font-mono);
889
- font-size: .75rem;
890
- color: var(--text-3);
891
- letter-spacing: .03em;
892
- }
893
-
894
- .terminal-status {
895
- display: flex;
896
- align-items: center;
897
- gap: 6px;
898
- font-size: .75rem;
899
- color: var(--green);
900
- font-weight: 600;
901
- }
902
-
903
- .pulse-dot {
904
- width: 6px;
905
- height: 6px;
906
- background: var(--green);
907
- border-radius: 50%;
908
- animation: pulse-dot 2s ease-in-out infinite;
909
- }
910
-
911
- .terminal-body {
912
- padding: 16px 20px;
913
- font-family: var(--font-mono);
914
- font-size: .8rem;
915
- line-height: 1.9;
916
- max-height: 320px;
917
- overflow-y: auto;
918
- }
919
-
920
- .terminal-line {
921
- color: var(--text-2);
922
- opacity: 0;
923
- animation: terminal-fade-in .4s ease forwards;
924
- }
925
-
926
- .terminal-line:nth-child(1) {
927
- animation-delay: .3s;
928
- }
929
-
930
- .terminal-line:nth-child(2) {
931
- animation-delay: .6s;
932
- }
933
-
934
- .terminal-line:nth-child(3) {
935
- animation-delay: 1s;
936
- }
937
-
938
- .terminal-line:nth-child(4) {
939
- animation-delay: 1.3s;
940
- }
941
-
942
- .terminal-line:nth-child(5) {
943
- animation-delay: 1.7s;
944
- }
945
-
946
- .terminal-line:nth-child(6) {
947
- animation-delay: 2s;
948
- }
949
-
950
- .terminal-line:nth-child(7) {
951
- animation-delay: 2.4s;
952
- }
953
-
954
- .terminal-line:nth-child(8) {
955
- animation-delay: 2.7s;
956
- }
957
-
958
- .terminal-line:nth-child(9) {
959
- animation-delay: 3.1s;
960
- }
961
-
962
- .terminal-line:nth-child(10) {
963
- animation-delay: 3.4s;
964
- }
965
-
966
- .terminal-line:nth-child(11) {
967
- animation-delay: 3.8s;
968
- }
969
-
970
- .terminal-line:nth-child(12) {
971
- animation-delay: 4.2s;
972
- }
973
-
974
- @keyframes terminal-fade-in {
975
- from {
976
- opacity: 0;
977
- transform: translateY(4px);
978
- }
979
-
980
- to {
981
- opacity: 1;
982
- transform: translateY(0);
983
- }
984
- }
985
-
986
- .t-step {
987
- color: var(--accent);
988
- font-weight: 700;
989
- }
990
-
991
- .t-muted {
992
- color: var(--text-3);
993
- }
994
-
995
- .t-check {
996
- color: var(--green);
997
- font-weight: 700;
998
- }
999
-
1000
- .t-done {}
1001
-
1002
- .terminal-line.complete {
1003
- color: var(--green);
1004
- font-weight: 600;
1005
- }
1006
-
1007
- /* ════════════════════════════════════════════════════════
1008
- STATS BAR
1009
- ════════════════════════════════════════════════════════ */
1010
- .stats-section {
1011
- padding: 64px 0;
1012
- border-top: 1px solid var(--border);
1013
- border-bottom: 1px solid var(--border);
1014
- background: var(--bg-elevated);
1015
- position: relative;
1016
- z-index: 1;
1017
- }
1018
-
1019
- .stats-bar {
1020
- display: flex;
1021
- align-items: center;
1022
- justify-content: center;
1023
- gap: 0;
1024
- flex-wrap: wrap;
1025
- }
1026
-
1027
- .stat-item {
1028
- flex: 1;
1029
- min-width: 160px;
1030
- text-align: center;
1031
- padding: 12px 24px;
1032
- }
1033
-
1034
- .stat-value {
1035
- font-family: var(--font-display);
1036
- font-size: 2.8rem;
1037
- font-weight: 700;
1038
- letter-spacing: -0.03em;
1039
- color: var(--text);
1040
- line-height: 1;
1041
- margin-bottom: 6px;
1042
- }
1043
-
1044
- .stat-label {
1045
- font-size: .8rem;
1046
- color: var(--text-3);
1047
- text-transform: uppercase;
1048
- letter-spacing: .08em;
1049
- font-weight: 500;
1050
- }
1051
-
1052
- .stat-divider {
1053
- width: 1px;
1054
- height: 48px;
1055
- background: var(--border);
1056
- }
1057
-
1058
- /* ════════════════════════════════════════════════════════
1059
- SECTIONS (shared)
1060
- ════════════════════════════════════════════════════════ */
1061
- .section {
1062
- padding: 100px 0;
1063
- position: relative;
1064
- z-index: 1;
1065
- }
1066
-
1067
- .section-dark {
1068
- background: var(--bg);
1069
- padding: 100px 0;
1070
- position: relative;
1071
- z-index: 1;
1072
- }
1073
-
1074
- .section-label {
1075
- display: inline-flex;
1076
- align-items: center;
1077
- gap: 8px;
1078
- font-size: .75rem;
1079
- font-weight: 600;
1080
- color: var(--accent);
1081
- text-transform: uppercase;
1082
- letter-spacing: .08em;
1083
- margin-bottom: 16px;
1084
- font-family: var(--font-mono);
1085
- }
1086
-
1087
- .section-title {
1088
- font-family: var(--font-display);
1089
- font-size: clamp(1.8rem, 4vw, 2.8rem);
1090
- font-weight: 700;
1091
- letter-spacing: -0.03em;
1092
- text-align: left;
1093
- margin-bottom: 12px;
1094
- line-height: 1.15;
1095
- }
1096
-
1097
- .section-sub {
1098
- color: var(--text-2);
1099
- text-align: left;
1100
- margin-bottom: 56px;
1101
- font-size: 1.05rem;
1102
- max-width: 540px;
1103
- }
1104
-
1105
- /* ════════════════════════════════════════════════════════
1106
- HOW IT WORKS — Steps Grid
1107
- ════════════════════════════════════════════════════════ */
1108
- .steps-grid {
1109
- display: flex;
1110
- align-items: center;
1111
- gap: 0;
1112
- flex-wrap: wrap;
1113
- justify-content: center;
1114
- }
1115
-
1116
- .step-card {
1117
- flex: 1;
1118
- min-width: 240px;
1119
- max-width: 320px;
1120
- background: var(--surface);
1121
- border: 1px solid var(--border);
1122
- border-radius: var(--radius-lg);
1123
- padding: 32px 28px;
1124
- position: relative;
1125
- transition: all .35s cubic-bezier(.4, 0, .2, 1);
1126
- }
1127
-
1128
- .step-card:hover {
1129
- transform: translateY(-4px);
1130
- border-color: rgba(91, 110, 245, 0.3);
1131
- box-shadow:
1132
- 0 12px 40px rgba(0, 0, 0, .3),
1133
- 0 0 40px rgba(91, 110, 245, 0.06);
1134
- }
1135
-
1136
- .step-number {
1137
- font-family: var(--font-display);
1138
- font-size: 3.5rem;
1139
- font-weight: 700;
1140
- color: rgba(91, 110, 245, 0.08);
1141
- line-height: 1;
1142
- margin-bottom: 16px;
1143
- }
1144
-
1145
- .step-icon-wrap {
1146
- display: flex;
1147
- align-items: center;
1148
- justify-content: center;
1149
- width: 52px;
1150
- height: 52px;
1151
- background: var(--accent-dim);
1152
- border-radius: var(--radius);
1153
- margin-bottom: 20px;
1154
- color: var(--accent);
1155
- }
1156
-
1157
- .step-card h3 {
1158
- font-family: var(--font-display);
1159
- font-size: 1.1rem;
1160
- font-weight: 600;
1161
- margin-bottom: 8px;
1162
- letter-spacing: -0.01em;
1163
- }
1164
-
1165
- .step-card p {
1166
- color: var(--text-2);
1167
- font-size: .88rem;
1168
- line-height: 1.6;
1169
- }
1170
-
1171
- /* Connector arrows */
1172
- .step-connector {
1173
- display: flex;
1174
- align-items: center;
1175
- justify-content: center;
1176
- padding: 0 8px;
1177
- color: var(--text-3);
1178
- }
1179
-
1180
- /* ════════════════════════════════════════════════════════
1181
- WHY US — CONTRAST + COMPARISON
1182
- ════════════════════════════════════════════════════════ */
1183
-
1184
- /* Contrast grid (them vs us) */
1185
- .contrast-grid {
1186
- display: grid;
1187
- grid-template-columns: 1fr auto 1fr;
1188
- gap: 24px;
1189
- align-items: stretch;
1190
- margin-bottom: 64px;
1191
- }
1192
-
1193
- .contrast-card {
1194
- padding: 28px;
1195
- border-radius: var(--radius-lg);
1196
- border: 1px solid var(--border);
1197
- }
1198
-
1199
- .contrast-them {
1200
- background: rgba(255, 60, 60, 0.04);
1201
- border-color: rgba(255, 80, 80, 0.15);
1202
- }
1203
-
1204
- .contrast-us {
1205
- background: rgba(91, 110, 245, 0.06);
1206
- border-color: rgba(91, 110, 245, 0.2);
1207
- }
1208
-
1209
- .contrast-header {
1210
- margin-bottom: 20px;
1211
- }
1212
-
1213
- .contrast-badge {
1214
- display: inline-block;
1215
- font-size: .75rem;
1216
- font-weight: 700;
1217
- letter-spacing: .06em;
1218
- text-transform: uppercase;
1219
- padding: 5px 12px;
1220
- border-radius: 20px;
1221
- }
1222
-
1223
- .them-badge {
1224
- background: rgba(255, 60, 60, 0.1);
1225
- color: #ff6b6b;
1226
- border: 1px solid rgba(255, 80, 80, 0.2);
1227
- }
1228
-
1229
- .us-badge {
1230
- background: var(--accent-dim);
1231
- color: var(--accent);
1232
- border: 1px solid rgba(91, 110, 245, 0.25);
1233
- }
1234
-
1235
- .contrast-list {
1236
- list-style: none;
1237
- display: flex;
1238
- flex-direction: column;
1239
- gap: 14px;
1240
- }
1241
-
1242
- .contrast-list li {
1243
- display: flex;
1244
- align-items: center;
1245
- gap: 10px;
1246
- font-size: .9rem;
1247
- color: var(--text-2);
1248
- }
1249
-
1250
- .contrast-them .contrast-list svg {
1251
- color: #ff6b6b;
1252
- flex-shrink: 0;
1253
- }
1254
-
1255
- .contrast-us .contrast-list svg {
1256
- color: #5bf0a0;
1257
- flex-shrink: 0;
1258
- }
1259
-
1260
- .contrast-vs {
1261
- display: flex;
1262
- align-items: center;
1263
- justify-content: center;
1264
- font-family: var(--font-heading);
1265
- font-size: 1rem;
1266
- font-weight: 700;
1267
- color: var(--text-3);
1268
- letter-spacing: .1em;
1269
- }
1270
-
1271
- /* Comparison table */
1272
- .compare-wrap {
1273
- background: var(--surface);
1274
- border: 1px solid var(--border);
1275
- border-radius: var(--radius-xl);
1276
- padding: 36px;
1277
- }
1278
-
1279
- .compare-title {
1280
- font-family: var(--font-heading);
1281
- font-size: 1.4rem;
1282
- font-weight: 700;
1283
- color: var(--text);
1284
- margin-bottom: 6px;
1285
- }
1286
-
1287
- .compare-sub {
1288
- font-size: .9rem;
1289
- color: var(--text-3);
1290
- margin-bottom: 28px;
1291
- }
1292
-
1293
- .compare-table-scroll {
1294
- overflow-x: auto;
1295
- -webkit-overflow-scrolling: touch;
1296
- }
1297
-
1298
- .compare-table {
1299
- width: 100%;
1300
- border-collapse: collapse;
1301
- font-size: .85rem;
1302
- }
1303
-
1304
- .compare-table th {
1305
- text-align: left;
1306
- padding: 12px 14px;
1307
- font-size: .7rem;
1308
- font-weight: 700;
1309
- text-transform: uppercase;
1310
- letter-spacing: .06em;
1311
- color: var(--text-3);
1312
- border-bottom: 1px solid var(--border);
1313
- white-space: nowrap;
1314
- }
1315
-
1316
- .compare-table td {
1317
- padding: 14px;
1318
- border-bottom: 1px solid rgba(255, 255, 255, 0.04);
1319
- color: var(--text-2);
1320
- white-space: nowrap;
1321
- }
1322
-
1323
- .compare-table tbody tr:hover {
1324
- background: rgba(255, 255, 255, 0.02);
1325
- }
1326
-
1327
- .compare-highlight {
1328
- background: rgba(91, 110, 245, 0.06) !important;
1329
- border-radius: var(--radius);
1330
- }
1331
-
1332
- .compare-highlight td {
1333
- color: var(--text);
1334
- font-weight: 500;
1335
- border-bottom-color: rgba(91, 110, 245, 0.15);
1336
- }
1337
-
1338
- .compare-highlight td:first-child {
1339
- color: var(--accent);
1340
- }
1341
-
1342
- .compare-note {
1343
- font-size: .7rem;
1344
- color: var(--text-3);
1345
- margin-left: 4px;
1346
- }
1347
-
1348
- .c-yes {
1349
- color: #5bf0a0 !important;
1350
- }
1351
-
1352
- .c-no {
1353
- color: #ff6b6b !important;
1354
- }
1355
-
1356
- .c-mid {
1357
- color: #f5c85b !important;
1358
- }
1359
-
1360
- .compare-footnote {
1361
- margin-top: 16px;
1362
- font-size: .75rem;
1363
- color: var(--text-3);
1364
- text-align: center;
1365
- }
1366
-
1367
- /* ════════════════════════════════════════════════════════
1368
- FEATURES GRID
1369
- ════════════════════════════════════════════════════════ */
1370
- .features-grid {
1371
- display: grid;
1372
- grid-template-columns: repeat(3, 1fr);
1373
- gap: 16px;
1374
- }
1375
-
1376
- .feature-card {
1377
- background: var(--surface);
1378
- border: 1px solid var(--border);
1379
- border-radius: var(--radius);
1380
- padding: 28px 24px;
1381
- transition: all .3s cubic-bezier(.4, 0, .2, 1);
1382
- }
1383
-
1384
- .feature-card:hover {
1385
- border-color: rgba(91, 110, 245, 0.25);
1386
- transform: translateY(-2px);
1387
- box-shadow: 0 8px 30px rgba(0, 0, 0, .3);
1388
- }
1389
-
1390
- .feature-icon {
1391
- font-size: 1.6rem;
1392
- margin-bottom: 14px;
1393
- display: flex;
1394
- align-items: center;
1395
- justify-content: center;
1396
- width: 44px;
1397
- height: 44px;
1398
- background: var(--accent-dim);
1399
- border-radius: 10px;
1400
- }
1401
-
1402
- .feature-card h3 {
1403
- font-family: var(--font-display);
1404
- font-size: 1rem;
1405
- font-weight: 600;
1406
- margin-bottom: 8px;
1407
- letter-spacing: -0.01em;
1408
- }
1409
-
1410
- .feature-card p {
1411
- color: var(--text-2);
1412
- font-size: .85rem;
1413
- line-height: 1.55;
1414
- }
1415
-
1416
- /* ════════════════════════════════════════════════════════
1417
- DEMO VIDEOS
1418
- ════════════════════════════════════════════════════════ */
1419
- .demos-wall {
1420
- margin-top: 24px;
1421
- min-height: 220px;
1422
- }
1423
-
1424
- .demos-state {
1425
- text-align: center;
1426
- font-size: .9rem;
1427
- color: var(--text-2);
1428
- background: var(--surface);
1429
- border: 1px solid var(--border);
1430
- border-radius: var(--radius-lg);
1431
- padding: 20px;
1432
- }
1433
-
1434
- .demos-state-error {
1435
- color: #ffb4b4;
1436
- border-color: rgba(255, 84, 84, 0.35);
1437
- background: rgba(255, 84, 84, 0.08);
1438
- }
1439
-
1440
- .demos-lanes {
1441
- display: grid;
1442
- grid-template-columns: repeat(3, minmax(0, 1fr));
1443
- gap: 20px;
1444
- align-items: start;
1445
- }
1446
-
1447
- .demos-lane {
1448
- display: flex;
1449
- flex-direction: column;
1450
- gap: 20px;
1451
- transform: translate3d(0, 0, 0);
1452
- will-change: transform;
1453
- }
1454
-
1455
- .demos-wall.parallax-enabled .demos-lane {
1456
- transition: transform .06s linear;
1457
- }
1458
-
1459
- .demo-card {
1460
- background: var(--surface);
1461
- border: 1px solid var(--border);
1462
- border-radius: var(--radius-lg);
1463
- overflow: hidden;
1464
- transition: border-color .25s ease, box-shadow .25s ease, transform .25s ease;
1465
- }
1466
-
1467
- .demo-card:hover {
1468
- transform: translateY(-3px);
1469
- border-color: rgba(91, 110, 245, 0.25);
1470
- box-shadow: 0 16px 50px rgba(0, 0, 0, .4);
1471
- }
1472
-
1473
- .demo-video-frame {
1474
- aspect-ratio: 9/16;
1475
- background: linear-gradient(135deg, var(--surface-2), var(--bg-elevated));
1476
- position: relative;
1477
- overflow: hidden;
1478
- }
1479
-
1480
- .demo-video {
1481
- width: 100%;
1482
- height: 100%;
1483
- display: block;
1484
- object-fit: cover;
1485
- background: #000;
1486
- }
1487
-
1488
- .demo-video:focus-visible {
1489
- outline: 2px solid var(--accent);
1490
- outline-offset: -2px;
1491
- }
1492
-
1493
- .demo-badge {
1494
- position: absolute;
1495
- top: 12px;
1496
- left: 12px;
1497
- background: rgba(0, 0, 0, 0.7);
1498
- backdrop-filter: blur(8px);
1499
- padding: 4px 10px;
1500
- border-radius: 6px;
1501
- font-size: .75rem;
1502
- font-weight: 700;
1503
- letter-spacing: .04em;
1504
- text-transform: uppercase;
1505
- color: var(--text);
1506
- z-index: 1;
1507
- }
1508
-
1509
- .demo-info {
1510
- padding: 16px 20px;
1511
- }
1512
-
1513
- .demo-info h4 {
1514
- font-size: .92rem;
1515
- font-weight: 600;
1516
- margin-bottom: 6px;
1517
- line-height: 1.3;
1518
- word-break: break-word;
1519
- }
1520
-
1521
- .demo-meta {
1522
- font-size: .8rem;
1523
- color: var(--text-2);
1524
- line-height: 1.45;
1525
- }
1526
-
1527
- @media (prefers-reduced-motion: reduce) {
1528
- .demos-lane {
1529
- transform: none !important;
1530
- transition: none !important;
1531
- }
1532
- }
1533
-
1534
- /* ════════════════════════════════════════════════════════
1535
- SHOWCASE (before / after comparison rows)
1536
- ════════════════════════════════════════════════════════ */
1537
- #showcase-container {
1538
- margin-top: 24px;
1539
- }
1540
-
1541
- .showcase-row {
1542
- margin-bottom: 56px;
1543
- padding-bottom: 56px;
1544
- border-bottom: 1px solid var(--border);
1545
- }
1546
-
1547
- .showcase-row:last-child {
1548
- border-bottom: none;
1549
- margin-bottom: 0;
1550
- padding-bottom: 0;
1551
- }
1552
-
1553
- /* ── Row header ────────────────────────────── */
1554
- .showcase-row-header {
1555
- display: flex;
1556
- align-items: center;
1557
- gap: 12px;
1558
- margin-bottom: 24px;
1559
- }
1560
-
1561
- .showcase-platform-icon {
1562
- width: 36px;
1563
- height: 36px;
1564
- border-radius: 10px;
1565
- display: flex;
1566
- align-items: center;
1567
- justify-content: center;
1568
- flex-shrink: 0;
1569
- }
1570
-
1571
- .showcase-platform-icon--youtube {
1572
- background: rgba(255, 0, 0, 0.12);
1573
- color: #ff4444;
1574
- }
1575
-
1576
- .showcase-platform-icon--instagram {
1577
- background: linear-gradient(135deg, rgba(131, 58, 180, 0.15), rgba(253, 29, 29, 0.15), rgba(252, 176, 69, 0.15));
1578
- color: #e1306c;
1579
- }
1580
-
1581
- .showcase-platform-icon svg {
1582
- width: 20px;
1583
- height: 20px;
1584
- }
1585
-
1586
- .showcase-row-info {
1587
- flex: 1;
1588
- min-width: 0;
1589
- }
1590
-
1591
- .showcase-row-title {
1592
- font-family: var(--font-display);
1593
- font-size: 1.15rem;
1594
- font-weight: 600;
1595
- color: var(--text);
1596
- line-height: 1.3;
1597
- }
1598
-
1599
- .showcase-row-desc {
1600
- font-size: 0.85rem;
1601
- color: var(--text-2);
1602
- margin-top: 2px;
1603
- }
1604
-
1605
- /* ── Cards flow ────────────────────────────── */
1606
- .showcase-flow {
1607
- display: grid;
1608
- grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
1609
- gap: 20px;
1610
- align-items: start;
1611
- }
1612
-
1613
- .showcase-card {
1614
- background: var(--surface);
1615
- border: 1px solid var(--border);
1616
- border-radius: var(--radius-lg);
1617
- overflow: hidden;
1618
- transition: transform .25s ease, box-shadow .25s ease, border-color .25s ease;
1619
- }
1620
-
1621
- .showcase-card:hover {
1622
- transform: translateY(-3px);
1623
- box-shadow: 0 16px 50px rgba(0, 0, 0, .4);
1624
- }
1625
-
1626
- /* Card style variants: original / their-dub / our-dub */
1627
- .showcase-card--original {
1628
- border-color: rgba(255, 255, 255, 0.15);
1629
- }
1630
-
1631
- .showcase-card--original:hover {
1632
- border-color: rgba(255, 255, 255, 0.3);
1633
- }
1634
-
1635
- .showcase-card--their-dub {
1636
- border-color: rgba(239, 68, 68, 0.15);
1637
- }
1638
-
1639
- .showcase-card--their-dub:hover {
1640
- border-color: rgba(239, 68, 68, 0.3);
1641
- }
1642
-
1643
- .showcase-card--our-dub {
1644
- border-color: var(--green-dim);
1645
- }
1646
-
1647
- .showcase-card--our-dub:hover {
1648
- border-color: rgba(91, 240, 160, 0.3);
1649
- box-shadow: 0 16px 50px rgba(0, 0, 0, .4), 0 0 20px rgba(91, 240, 160, 0.08);
1650
- }
1651
-
1652
- /* ── Language label pill ───────────────────── */
1653
- .showcase-label {
1654
- display: inline-flex;
1655
- align-items: center;
1656
- gap: 6px;
1657
- padding: 6px 12px;
1658
- margin: 12px 12px 0;
1659
- font-size: 0.7rem;
1660
- font-weight: 700;
1661
- letter-spacing: 0.08em;
1662
- text-transform: uppercase;
1663
- border-radius: 20px;
1664
- line-height: 1;
1665
- }
1666
-
1667
- .showcase-label--original {
1668
- background: rgba(255, 255, 255, 0.08);
1669
- color: var(--text-2);
1670
- }
1671
-
1672
- .showcase-label--their-dub {
1673
- background: rgba(239, 68, 68, 0.1);
1674
- color: #f87171;
1675
- }
1676
-
1677
- .showcase-label--our-dub {
1678
- background: var(--green-dim);
1679
- color: var(--green);
1680
- }
1681
-
1682
- /* ── Video / embed frame ───────────────────── */
1683
- .showcase-video-frame {
1684
- position: relative;
1685
- aspect-ratio: 9 / 16;
1686
- background: linear-gradient(135deg, var(--surface-2), var(--bg-elevated));
1687
- overflow: hidden;
1688
- }
1689
-
1690
- .showcase-video-frame video {
1691
- width: 100%;
1692
- height: 100%;
1693
- object-fit: cover;
1694
- background: #000;
1695
- }
1696
-
1697
- .showcase-video-frame video:focus-visible {
1698
- outline: 2px solid var(--accent);
1699
- outline-offset: -2px;
1700
- }
1701
-
1702
- .showcase-video-frame iframe {
1703
- width: 100%;
1704
- height: 100%;
1705
- border: none;
1706
- background: #000;
1707
- }
1708
-
1709
- /* ── Card info ─────────────────────────────── */
1710
- .showcase-card-info {
1711
- padding: 12px 16px 16px;
1712
- }
1713
-
1714
- .showcase-card-title {
1715
- font-size: 0.9rem;
1716
- font-weight: 600;
1717
- color: var(--text);
1718
- line-height: 1.3;
1719
- }
1720
-
1721
- /* ── Flow arrow (desktop only) ─────────────── */
1722
- .showcase-arrow {
1723
- display: flex;
1724
- align-items: center;
1725
- justify-content: center;
1726
- color: var(--text-3);
1727
- font-size: 1.4rem;
1728
- padding: 0 4px;
1729
- align-self: center;
1730
- }
1731
-
1732
- /* ── Mobile tabs ───────────────────────────── */
1733
- .showcase-tabs {
1734
- display: none;
1735
- gap: 4px;
1736
- margin-bottom: 16px;
1737
- padding: 4px;
1738
- background: var(--surface);
1739
- border-radius: var(--radius);
1740
- border: 1px solid var(--border);
1741
- overflow-x: auto;
1742
- }
1743
-
1744
- .showcase-tab {
1745
- flex: 1;
1746
- min-width: 0;
1747
- padding: 8px 12px;
1748
- font-size: 0.75rem;
1749
- font-weight: 600;
1750
- text-transform: uppercase;
1751
- letter-spacing: 0.05em;
1752
- color: var(--text-2);
1753
- background: transparent;
1754
- border: none;
1755
- border-radius: 8px;
1756
- cursor: pointer;
1757
- transition: background .2s, color .2s;
1758
- white-space: nowrap;
1759
- text-align: center;
1760
- }
1761
-
1762
- .showcase-tab:hover {
1763
- color: var(--text);
1764
- background: rgba(255, 255, 255, 0.05);
1765
- }
1766
-
1767
- .showcase-tab--active {
1768
- color: var(--text);
1769
- background: var(--accent-dim);
1770
- }
1771
-
1772
- /* ── Responsive ────────────────────────────── */
1773
- @media (max-width: 900px) {
1774
- .showcase-flow {
1775
- grid-template-columns: repeat(2, minmax(0, 1fr));
1776
- }
1777
-
1778
- .showcase-arrow {
1779
- display: none;
1780
- }
1781
- }
1782
-
1783
- @media (max-width: 640px) {
1784
- .showcase-tabs {
1785
- display: flex;
1786
- }
1787
-
1788
- .showcase-flow {
1789
- grid-template-columns: 1fr;
1790
- max-width: 340px;
1791
- margin: 0 auto;
1792
- }
1793
-
1794
- .showcase-card {
1795
- display: none;
1796
- }
1797
-
1798
- .showcase-card--active {
1799
- display: block;
1800
- }
1801
-
1802
- .showcase-arrow {
1803
- display: none;
1804
- }
1805
- }
1806
-
1807
- /* ════════════════════════════════════════════════════════
1808
- APP / TRY NOW
1809
- ════════════════════════════════════════════════════════ */
1810
- .app-container {
1811
- max-width: 640px;
1812
- /* expanded via JS to 100% on state change */
1813
- margin: 0 auto;
1814
- background: var(--surface);
1815
- border: 1px solid rgba(91, 110, 245, 0.25);
1816
- border-radius: var(--radius-xl);
1817
- overflow: hidden;
1818
- box-shadow:
1819
- 0 0 0 1px rgba(91, 110, 245, 0.05),
1820
- 0 0 20px rgba(91, 110, 245, 0.08),
1821
- 0 20px 60px rgba(0, 0, 0, .4);
1822
- display: flex;
1823
- flex-direction: row;
1824
- width: 100%;
1825
- gap: 0;
1826
- transition: max-width 0s, box-shadow 0.6s ease, border-color 0.6s ease;
1827
- }
1828
-
1829
- /* Glow intensifies when pipeline is processing */
1830
- .app-container.state-processing {
1831
- border-color: rgba(91, 110, 245, 0.5);
1832
- box-shadow:
1833
- 0 0 0 1px rgba(91, 110, 245, 0.1),
1834
- 0 0 30px rgba(91, 110, 245, 0.2),
1835
- 0 0 60px rgba(91, 110, 245, 0.1),
1836
- 0 20px 60px rgba(0, 0, 0, .4);
1837
- animation: pulse-glow 2s ease-in-out infinite;
1838
- }
1839
-
1840
- @keyframes pulse-glow {
1841
-
1842
- 0%,
1843
- 100% {
1844
- box-shadow:
1845
- 0 0 0 1px rgba(91, 110, 245, 0.1),
1846
- 0 0 30px rgba(91, 110, 245, 0.2),
1847
- 0 0 60px rgba(91, 110, 245, 0.1),
1848
- 0 20px 60px rgba(0, 0, 0, .4);
1849
- }
1850
-
1851
- 50% {
1852
- box-shadow:
1853
- 0 0 0 1px rgba(91, 110, 245, 0.2),
1854
- 0 0 40px rgba(91, 110, 245, 0.3),
1855
- 0 0 80px rgba(91, 110, 245, 0.15),
1856
- 0 20px 60px rgba(0, 0, 0, .4);
1857
- }
1858
- }
1859
-
1860
-
1861
- /* Tab switcher */
1862
- .input-tabs {
1863
- display: flex;
1864
- border-bottom: 1px solid var(--border);
1865
- }
1866
-
1867
- .tab-btn {
1868
- flex: 1;
1869
- padding: 14px;
1870
- background: none;
1871
- border: none;
1872
- color: var(--text-3);
1873
- font-family: var(--font);
1874
- font-size: .875rem;
1875
- font-weight: 500;
1876
- cursor: pointer;
1877
- display: flex;
1878
- align-items: center;
1879
- justify-content: center;
1880
- gap: 8px;
1881
- transition: all .2s;
1882
- position: relative;
1883
- }
1884
-
1885
- .tab-btn:hover {
1886
- color: var(--text-2);
1887
- background: rgba(255, 255, 255, 0.02);
1888
- }
1889
-
1890
- .tab-btn.active {
1891
- color: var(--accent);
1892
- }
1893
-
1894
- .tab-btn.active::after {
1895
- content: '';
1896
- position: absolute;
1897
- bottom: -1px;
1898
- left: 0;
1899
- right: 0;
1900
- height: 2px;
1901
- background: var(--accent);
1902
- }
1903
-
1904
- /* Tab content */
1905
- .tab-content {
1906
- display: none;
1907
- }
1908
-
1909
- .tab-content.active {
1910
- display: block;
1911
- }
1912
-
1913
- /* App panel inner */
1914
- .app-panel {
1915
- padding: 28px;
1916
- flex: 0 0 0%;
1917
- width: 0;
1918
- opacity: 0;
1919
- overflow: hidden;
1920
- transform: translateX(20px);
1921
- transition:
1922
- flex 0.45s cubic-bezier(0.4, 0, 0.2, 1),
1923
- opacity 0.4s ease,
1924
- transform 0.4s ease,
1925
- padding 0.45s ease;
1926
- pointer-events: none;
1927
- }
1928
-
1929
- /* Left panel always visible from start */
1930
- #app-input {
1931
- flex: 0 0 100%;
1932
- width: 100%;
1933
- opacity: 1;
1934
- transform: none;
1935
- pointer-events: auto;
1936
- overflow-y: auto;
1937
- overflow-x: hidden;
1938
- }
1939
-
1940
- /* STATE: processing — left shrinks, center appears, right skeleton appears */
1941
- .state-processing #app-input {
1942
- flex: 0 0 32%;
1943
- opacity: 0.55;
1944
- transform: none;
1945
- }
1946
-
1947
- /* Block form controls but keep video previews interactive */
1948
- .state-processing #app-input .input-tabs,
1949
- .state-processing #app-input .tab-content> :not(.video-preview):not(.url-preview-card),
1950
- .state-processing #app-input .language-row,
1951
- .state-processing #app-input .voice-mode-group,
1952
- .state-processing #app-input .option-row,
1953
- .state-processing #app-input .btn,
1954
- .state-processing #app-input .free-note {
1955
- pointer-events: none;
1956
- }
1957
-
1958
- .state-processing #app-input .video-preview,
1959
- .state-processing #app-input .url-preview-card {
1960
- pointer-events: auto;
1961
- opacity: 1;
1962
- }
1963
-
1964
- .state-processing #app-processing {
1965
- flex: 0 0 36%;
1966
- opacity: 1;
1967
- transform: none;
1968
- pointer-events: auto;
1969
- transition-delay: 0.1s;
1970
- /* stagger: center arrives second */
1971
- }
1972
-
1973
- .state-processing #app-result {
1974
- flex: 0 0 32%;
1975
- opacity: 1;
1976
- transform: none;
1977
- pointer-events: none;
1978
- transition-delay: 0.2s;
1979
- /* stagger: right arrives third */
1980
- }
1981
-
1982
- /* STATE: result — right panel gets focus */
1983
- .state-result #app-input {
1984
- flex: 0 0 28%;
1985
- opacity: 0.45;
1986
- }
1987
-
1988
- .state-result #app-input .input-tabs,
1989
- .state-result #app-input .tab-content> :not(.video-preview):not(.url-preview-card),
1990
- .state-result #app-input .language-row,
1991
- .state-result #app-input .voice-mode-group,
1992
- .state-result #app-input .option-row,
1993
- .state-result #app-input .btn,
1994
- .state-result #app-input .free-note {
1995
- pointer-events: none;
1996
- }
1997
-
1998
- .state-result #app-input .video-preview,
1999
- .state-result #app-input .url-preview-card {
2000
- pointer-events: auto;
2001
- opacity: 1;
2002
- }
2003
-
2004
- .state-result #app-processing {
2005
- flex: 0 0 28%;
2006
- opacity: 0.55;
2007
- pointer-events: none;
2008
- }
2009
-
2010
- .state-result #app-result {
2011
- flex: 0 0 44%;
2012
- /* right panel gets extra weight */
2013
- opacity: 1;
2014
- pointer-events: auto;
2015
- transition-delay: 0s;
2016
- }
2017
-
2018
- /* Skeleton placeholder */
2019
- .result-skeleton {
2020
- display: flex;
2021
- flex-direction: column;
2022
- gap: 12px;
2023
- }
2024
-
2025
- .result-skeleton .skel-bar {
2026
- background: linear-gradient(90deg, #111520 25%, #1a2038 50%, #111520 75%);
2027
- background-size: 200% 100%;
2028
- animation: shimmer 1.4s infinite;
2029
- border-radius: 6px;
2030
- }
2031
-
2032
- @keyframes shimmer {
2033
- 0% {
2034
- background-position: 200% 0;
2035
- }
2036
-
2037
- 100% {
2038
- background-position: -200% 0;
2039
- }
2040
- }
2041
-
2042
- /* URL input */
2043
- .url-input-wrap {
2044
- margin-bottom: 16px;
2045
- }
2046
-
2047
- .url-input {
2048
- width: 100%;
2049
- padding: 14px 16px;
2050
- background: var(--bg);
2051
- border: 1px solid var(--border);
2052
- border-radius: var(--radius);
2053
- color: var(--text);
2054
- font-family: var(--font);
2055
- font-size: .9rem;
2056
- outline: none;
2057
- transition: border-color .2s, box-shadow .2s;
2058
- }
2059
-
2060
- .url-input:focus {
2061
- border-color: var(--accent);
2062
- box-shadow: 0 0 0 3px var(--accent-dim);
2063
- }
2064
-
2065
- .url-input::placeholder {
2066
- color: var(--text-3);
2067
- }
2068
-
2069
- .url-platforms {
2070
- display: flex;
2071
- gap: 12px;
2072
- margin-top: 10px;
2073
- }
2074
-
2075
- .platform-tag {
2076
- display: flex;
2077
- align-items: center;
2078
- gap: 6px;
2079
- font-size: .75rem;
2080
- color: var(--text-3);
2081
- }
2082
-
2083
- .platform-tag svg {
2084
- opacity: .5;
2085
- }
2086
-
2087
- /* Drop zone */
2088
- .drop-zone {
2089
- border: 2px dashed var(--border);
2090
- border-radius: var(--radius);
2091
- padding: 40px 24px;
2092
- text-align: center;
2093
- cursor: pointer;
2094
- transition: all .3s;
2095
- margin-bottom: 16px;
2096
- }
2097
-
2098
- .drop-zone:hover,
2099
- .drop-zone.drag-over {
2100
- border-color: var(--accent);
2101
- background: var(--accent-dim);
2102
- }
2103
-
2104
- .drop-icon {
2105
- color: var(--text-3);
2106
- margin-bottom: 12px;
2107
- transition: color .3s;
2108
- }
2109
-
2110
- .drop-zone:hover .drop-icon {
2111
- color: var(--accent);
2112
- }
2113
-
2114
- .drop-text {
2115
- font-weight: 600;
2116
- margin-bottom: 6px;
2117
- }
2118
-
2119
- .drop-hint {
2120
- color: var(--text-3);
2121
- font-size: .8rem;
2122
- }
2123
-
2124
- /* File preview */
2125
- .drop-zone-file {
2126
- display: flex;
2127
- align-items: center;
2128
- gap: 10px;
2129
- padding: 14px 16px;
2130
- background: var(--accent-dim);
2131
- border: 1px solid rgba(91, 110, 245, 0.2);
2132
- border-radius: var(--radius);
2133
- color: var(--accent);
2134
- font-size: .875rem;
2135
- }
2136
-
2137
- .file-remove {
2138
- margin-left: auto;
2139
- background: none;
2140
- border: none;
2141
- color: var(--text-3);
2142
- cursor: pointer;
2143
- font-size: 1rem;
2144
- padding: 4px;
2145
- transition: color .2s;
2146
- }
2147
-
2148
- .file-remove:hover {
2149
- color: var(--red);
2150
- }
2151
-
2152
- /* Video preview */
2153
- .video-preview {
2154
- margin-top: 12px;
2155
- animation: fade-in .3s ease;
2156
- }
2157
-
2158
- @keyframes fade-in {
2159
- from {
2160
- opacity: 0;
2161
- transform: translateY(6px);
2162
- }
2163
-
2164
- to {
2165
- opacity: 1;
2166
- transform: translateY(0);
2167
- }
2168
- }
2169
-
2170
- .video-preview-inner {
2171
- position: relative;
2172
- border-radius: var(--radius);
2173
- overflow: hidden;
2174
- border: 1px solid var(--border);
2175
- background: #000;
2176
- max-height: 280px;
2177
- display: flex;
2178
- align-items: center;
2179
- justify-content: center;
2180
- }
2181
-
2182
- .video-preview-inner video {
2183
- width: 100%;
2184
- max-height: 280px;
2185
- object-fit: contain;
2186
- display: block;
2187
- }
2188
-
2189
- .video-preview-remove {
2190
- position: absolute;
2191
- top: 8px;
2192
- right: 8px;
2193
- width: 28px;
2194
- height: 28px;
2195
- border-radius: 50%;
2196
- background: rgba(0, 0, 0, 0.7);
2197
- border: 1px solid rgba(255, 255, 255, 0.15);
2198
- color: #fff;
2199
- font-size: .85rem;
2200
- cursor: pointer;
2201
- display: flex;
2202
- align-items: center;
2203
- justify-content: center;
2204
- transition: background .2s;
2205
- z-index: 2;
2206
- }
2207
-
2208
- .video-preview-remove:hover {
2209
- background: rgba(220, 50, 50, 0.8);
2210
- }
2211
-
2212
- .video-preview-info {
2213
- margin-top: 6px;
2214
- font-size: .75rem;
2215
- color: var(--text-3);
2216
- display: flex;
2217
- align-items: center;
2218
- gap: 6px;
2219
- }
2220
-
2221
- /* URL preview card */
2222
- .url-preview-card {
2223
- display: flex;
2224
- align-items: center;
2225
- gap: 12px;
2226
- margin-top: 12px;
2227
- padding: 12px 14px;
2228
- background: var(--surface-2);
2229
- border: 1px solid var(--accent);
2230
- border-radius: var(--radius);
2231
- animation: fade-in .3s ease;
2232
- }
2233
-
2234
- .url-preview-icon {
2235
- width: 40px;
2236
- height: 40px;
2237
- border-radius: 10px;
2238
- display: flex;
2239
- align-items: center;
2240
- justify-content: center;
2241
- flex-shrink: 0;
2242
- font-size: 1.2rem;
2243
- }
2244
-
2245
- .url-preview-icon.ig {
2246
- background: linear-gradient(135deg, #f09433, #e6683c, #dc2743, #cc2366, #bc1888);
2247
- }
2248
-
2249
- .url-preview-icon.yt {
2250
- background: #cc0000;
2251
- }
2252
-
2253
- .url-preview-icon.tt {
2254
- background: #010101;
2255
- border: 1px solid rgba(255, 255, 255, 0.15);
2256
- }
2257
-
2258
- .url-preview-icon.generic {
2259
- background: var(--accent-dim);
2260
- }
2261
-
2262
- .url-preview-details {
2263
- flex: 1;
2264
- min-width: 0;
2265
- display: flex;
2266
- flex-direction: column;
2267
- gap: 2px;
2268
- }
2269
-
2270
- .url-preview-platform {
2271
- font-size: .85rem;
2272
- font-weight: 600;
2273
- color: var(--text);
2274
- }
2275
-
2276
- .url-preview-link {
2277
- font-size: .72rem;
2278
- color: var(--text-3);
2279
- white-space: nowrap;
2280
- overflow: hidden;
2281
- text-overflow: ellipsis;
2282
- }
2283
-
2284
- .url-preview-remove {
2285
- width: 28px;
2286
- height: 28px;
2287
- border-radius: 50%;
2288
- background: rgba(255, 255, 255, 0.06);
2289
- border: 1px solid var(--border);
2290
- color: var(--text-3);
2291
- font-size: .85rem;
2292
- cursor: pointer;
2293
- display: flex;
2294
- align-items: center;
2295
- justify-content: center;
2296
- flex-shrink: 0;
2297
- transition: background .2s, color .2s;
2298
- }
2299
-
2300
- .url-preview-remove:hover {
2301
- background: rgba(220, 50, 50, 0.3);
2302
- color: #fff;
2303
- }
2304
-
2305
- /* Language row */
2306
- .language-row {
2307
- display: flex;
2308
- align-items: flex-end;
2309
- gap: 12px;
2310
- margin-bottom: 20px;
2311
- }
2312
-
2313
- .lang-select-wrap {
2314
- flex: 1;
2315
- }
2316
-
2317
- .lang-select-wrap label {
2318
- display: block;
2319
- font-size: .75rem;
2320
- font-weight: 600;
2321
- color: var(--text-3);
2322
- text-transform: uppercase;
2323
- letter-spacing: .06em;
2324
- margin-bottom: 6px;
2325
- }
2326
-
2327
- .lang-select {
2328
- width: 100%;
2329
- padding: 12px 14px;
2330
- background: var(--bg);
2331
- border: 1px solid var(--border);
2332
- border-radius: var(--radius);
2333
- color: var(--text);
2334
- font-family: var(--font);
2335
- font-size: .875rem;
2336
- outline: none;
2337
- cursor: pointer;
2338
- appearance: none;
2339
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
2340
- background-repeat: no-repeat;
2341
- background-position: right 12px center;
2342
- transition: border-color .2s;
2343
- }
2344
-
2345
- .lang-select:focus {
2346
- border-color: var(--accent);
2347
- }
2348
-
2349
- .lang-swap {
2350
- display: flex;
2351
- align-items: center;
2352
- justify-content: center;
2353
- width: 44px;
2354
- height: 44px;
2355
- background: var(--surface-2);
2356
- border: 1px solid var(--border);
2357
- border-radius: var(--radius);
2358
- color: var(--text-3);
2359
- cursor: pointer;
2360
- transition: all .2s;
2361
- flex-shrink: 0;
2362
- }
2363
-
2364
- .lang-swap:hover {
2365
- border-color: var(--accent);
2366
- color: var(--accent);
2367
- }
2368
-
2369
- /* Captions toggle option row */
2370
- .option-row {
2371
- display: flex;
2372
- align-items: center;
2373
- justify-content: space-between;
2374
- padding: 10px 14px;
2375
- margin-bottom: 16px;
2376
- background: var(--surface-2);
2377
- border: 1px solid var(--border);
2378
- border-radius: var(--radius);
2379
- }
2380
-
2381
- .toggle-label {
2382
- display: flex;
2383
- align-items: center;
2384
- gap: 8px;
2385
- font-size: .85rem;
2386
- font-weight: 500;
2387
- color: var(--text-2);
2388
- cursor: pointer;
2389
- user-select: none;
2390
- }
2391
-
2392
- .toggle-switch {
2393
- position: relative;
2394
- display: inline-block;
2395
- width: 40px;
2396
- height: 22px;
2397
- cursor: pointer;
2398
- }
2399
-
2400
- .toggle-switch input {
2401
- display: none;
2402
- }
2403
-
2404
- .toggle-track {
2405
- position: absolute;
2406
- inset: 0;
2407
- background: var(--surface);
2408
- border: 1px solid var(--border);
2409
- border-radius: 12px;
2410
- transition: background .2s, border-color .2s;
2411
- }
2412
-
2413
- .toggle-track::after {
2414
- content: '';
2415
- position: absolute;
2416
- top: 2px;
2417
- left: 2px;
2418
- width: 16px;
2419
- height: 16px;
2420
- background: var(--text-3);
2421
- border-radius: 50%;
2422
- transition: transform .2s, background .2s;
2423
- }
2424
-
2425
- .toggle-switch input:checked+.toggle-track {
2426
- background: var(--accent);
2427
- border-color: var(--accent);
2428
- }
2429
-
2430
- .toggle-switch input:checked+.toggle-track::after {
2431
- transform: translateX(18px);
2432
- background: #fff;
2433
- }
2434
-
2435
- /* Free note */
2436
- .free-note {
2437
- display: flex;
2438
- align-items: center;
2439
- justify-content: center;
2440
- gap: 6px;
2441
- font-size: .8rem;
2442
- color: var(--text-3);
2443
- margin-top: 14px;
2444
- }
2445
-
2446
- /* ── Processing state ──────────────────────────────────── */
2447
- .processing-header {
2448
- text-align: center;
2449
- margin-bottom: 24px;
2450
- }
2451
-
2452
- .processing-spinner {
2453
- width: 36px;
2454
- height: 36px;
2455
- border: 3px solid var(--border);
2456
- border-top-color: var(--accent);
2457
- border-radius: 50%;
2458
- animation: spin 1s linear infinite;
2459
- margin: 0 auto 14px;
2460
- }
2461
-
2462
- @keyframes spin {
2463
- to {
2464
- transform: rotate(360deg);
2465
- }
2466
- }
2467
-
2468
- .processing-header h3 {
2469
- font-size: 1.1rem;
2470
- margin-bottom: 4px;
2471
- }
2472
-
2473
- #processing-step {
2474
- color: var(--text-2);
2475
- font-size: .85rem;
2476
- }
2477
-
2478
- .progress-bar-wrap {
2479
- height: 4px;
2480
- background: var(--border);
2481
- border-radius: 4px;
2482
- overflow: hidden;
2483
- margin-bottom: 20px;
2484
- }
2485
-
2486
- .progress-bar {
2487
- height: 100%;
2488
- width: 0%;
2489
- background: linear-gradient(90deg, var(--accent), var(--accent-hover));
2490
- border-radius: 4px;
2491
- transition: width .4s ease;
2492
- }
2493
-
2494
- .terminal-mini {
2495
- background: var(--bg);
2496
- border: 1px solid var(--border);
2497
- border-radius: var(--radius);
2498
- overflow: hidden;
2499
- }
2500
-
2501
- .terminal-scroll {
2502
- max-height: 220px;
2503
- overflow-y: auto;
2504
- }
2505
-
2506
- /* ═══════════════════════════════════════════════════════
2507
- VOICE MODEL SELECTOR (input form)
2508
- ═══════════════════════════════════════════════════════ */
2509
- .voice-mode-group {
2510
- margin-bottom: 20px;
2511
- }
2512
-
2513
- .voice-mode-label {
2514
- display: block;
2515
- font-size: .75rem;
2516
- font-weight: 600;
2517
- color: var(--text-3);
2518
- text-transform: uppercase;
2519
- letter-spacing: .06em;
2520
- margin-bottom: 10px;
2521
- }
2522
-
2523
- .voice-mode-options {
2524
- display: flex;
2525
- flex-direction: column;
2526
- gap: 8px;
2527
- }
2528
-
2529
- .voice-mode-option {
2530
- cursor: pointer;
2531
- display: block;
2532
- }
2533
-
2534
- .voice-mode-option input[type="radio"] {
2535
- position: absolute;
2536
- opacity: 0;
2537
- pointer-events: none;
2538
- }
2539
-
2540
- .voice-mode-card {
2541
- background: var(--bg);
2542
- border: 1.5px solid var(--border);
2543
- border-radius: var(--radius);
2544
- padding: 12px 16px;
2545
- transition: all .25s cubic-bezier(.4, 0, .2, 1);
2546
- }
2547
-
2548
- .voice-mode-card:hover {
2549
- border-color: rgba(91, 110, 245, 0.25);
2550
- background: rgba(91, 110, 245, 0.03);
2551
- }
2552
-
2553
- .voice-mode-option input:checked+.voice-mode-card {
2554
- border-color: var(--accent);
2555
- background: var(--accent-dim);
2556
- box-shadow: 0 0 0 1px var(--accent), 0 4px 16px rgba(91, 110, 245, 0.1);
2557
- }
2558
-
2559
- .voice-mode-card-inner {
2560
- display: flex;
2561
- align-items: center;
2562
- gap: 12px;
2563
- }
2564
-
2565
- .voice-mode-icon {
2566
- font-size: 1.3rem;
2567
- flex-shrink: 0;
2568
- width: 36px;
2569
- height: 36px;
2570
- display: flex;
2571
- align-items: center;
2572
- justify-content: center;
2573
- background: rgba(255, 255, 255, 0.04);
2574
- border-radius: 8px;
2575
- }
2576
-
2577
- .voice-mode-card strong {
2578
- display: block;
2579
- font-size: .875rem;
2580
- font-weight: 600;
2581
- color: var(--text);
2582
- margin-bottom: 2px;
2583
- }
2584
-
2585
- .voice-mode-hint {
2586
- display: block;
2587
- font-size: .75rem;
2588
- color: var(--text-3);
2589
- line-height: 1.3;
2590
- }
2591
-
2592
- /* ═══════════════════════════════════════════════════════
2593
- VOICE PREVIEW PANEL (processing section)
2594
- ═══════════════════════════════════════════════════════ */
2595
- .voice-preview-panel {
2596
- margin-top: 20px;
2597
- background: linear-gradient(135deg,
2598
- rgba(91, 110, 245, 0.05) 0%,
2599
- rgba(167, 139, 250, 0.05) 100%);
2600
- border: 1px solid rgba(91, 110, 245, 0.18);
2601
- border-radius: var(--radius-lg);
2602
- padding: 24px;
2603
- opacity: 0;
2604
- transform: translateY(16px);
2605
- transition: all .5s cubic-bezier(.4, 0, .2, 1);
2606
- }
2607
-
2608
- .voice-preview-panel.visible {
2609
- opacity: 1;
2610
- transform: translateY(0);
2611
- }
2612
-
2613
- .voice-preview-panel.collapsed {
2614
- opacity: 0;
2615
- transform: translateY(-8px) scale(0.98);
2616
- max-height: 0;
2617
- padding: 0 24px;
2618
- margin-top: 0;
2619
- overflow: hidden;
2620
- border-color: transparent;
2621
- transition: all .4s cubic-bezier(.4, 0, .2, 1);
2622
- }
2623
-
2624
- .voice-preview-header {
2625
- text-align: center;
2626
- margin-bottom: 20px;
2627
- position: relative;
2628
- }
2629
-
2630
- .voice-preview-pulse {
2631
- width: 10px;
2632
- height: 10px;
2633
- background: var(--accent);
2634
- border-radius: 50%;
2635
- margin: 0 auto 12px;
2636
- animation: preview-pulse 1.5s ease infinite;
2637
- box-shadow: 0 0 12px rgba(91, 110, 245, 0.4);
2638
- }
2639
-
2640
- @keyframes preview-pulse {
2641
-
2642
- 0%,
2643
- 100% {
2644
- transform: scale(1);
2645
- opacity: 1;
2646
- }
2647
-
2648
- 50% {
2649
- transform: scale(1.4);
2650
- opacity: 0.6;
2651
- }
2652
- }
2653
-
2654
- .voice-preview-header h4 {
2655
- font-family: var(--font-display);
2656
- font-size: 1.15rem;
2657
- font-weight: 700;
2658
- margin-bottom: 6px;
2659
- letter-spacing: -0.01em;
2660
- background: linear-gradient(135deg, var(--text), var(--accent));
2661
- -webkit-background-clip: text;
2662
- -webkit-text-fill-color: transparent;
2663
- background-clip: text;
2664
- }
2665
-
2666
- .voice-preview-header p {
2667
- color: var(--text-3);
2668
- font-size: .8rem;
2669
- }
2670
-
2671
- .voice-cards {
2672
- display: flex;
2673
- flex-direction: column;
2674
- gap: 10px;
2675
- }
2676
-
2677
- .voice-card {
2678
- background: var(--surface);
2679
- border: 1.5px solid var(--border);
2680
- border-radius: var(--radius);
2681
- padding: 12px 16px;
2682
- display: flex;
2683
- flex-direction: row;
2684
- flex-wrap: wrap;
2685
- align-items: center;
2686
- gap: 14px;
2687
- transition: all .3s cubic-bezier(.4, 0, .2, 1);
2688
- position: relative;
2689
- overflow: hidden;
2690
- }
2691
-
2692
- .voice-card::before {
2693
- content: '';
2694
- position: absolute;
2695
- inset: 0;
2696
- background: radial-gradient(ellipse at top left, rgba(91, 110, 245, 0.07) 0%, transparent 60%);
2697
- pointer-events: none;
2698
- opacity: 0;
2699
- transition: opacity .3s;
2700
- }
2701
-
2702
- .voice-card:hover {
2703
- border-color: rgba(91, 110, 245, 0.35);
2704
- box-shadow: 0 8px 30px rgba(0, 0, 0, .3);
2705
- }
2706
-
2707
- .voice-card:hover::before {
2708
- opacity: 1;
2709
- }
2710
-
2711
- .voice-card.selected {
2712
- border-color: var(--accent);
2713
- box-shadow:
2714
- 0 0 0 1px var(--accent),
2715
- 0 0 40px rgba(91, 110, 245, 0.12),
2716
- 0 8px 30px rgba(0, 0, 0, .3);
2717
- }
2718
-
2719
- .voice-card.selected::before {
2720
- opacity: 1;
2721
- }
2722
-
2723
- .voice-card.unavailable {
2724
- opacity: 0.35;
2725
- pointer-events: none;
2726
- }
2727
-
2728
- .voice-card-badge {
2729
- font-weight: 700;
2730
- font-size: .85rem;
2731
- color: var(--text);
2732
- letter-spacing: -0.01em;
2733
- white-space: nowrap;
2734
- flex-shrink: 0;
2735
- min-width: 110px;
2736
- position: relative;
2737
- z-index: 1;
2738
- }
2739
-
2740
- .voice-card-player {
2741
- position: relative;
2742
- z-index: 1;
2743
- display: flex;
2744
- align-items: center;
2745
- gap: 10px;
2746
- flex: 1;
2747
- min-width: 120px;
2748
- }
2749
-
2750
- .voice-card-player audio {
2751
- display: none;
2752
- }
2753
-
2754
- .play-pause-btn {
2755
- flex-shrink: 0;
2756
- width: 40px;
2757
- height: 40px;
2758
- border-radius: 50%;
2759
- border: 2px solid var(--accent);
2760
- background: var(--accent);
2761
- color: #fff;
2762
- cursor: pointer;
2763
- display: flex;
2764
- align-items: center;
2765
- justify-content: center;
2766
- transition: transform .15s, background .2s, opacity .2s;
2767
- position: relative;
2768
- z-index: 2;
2769
- }
2770
-
2771
- .play-pause-btn svg {
2772
- display: block;
2773
- pointer-events: none;
2774
- }
2775
-
2776
- .play-pause-btn:hover {
2777
- transform: scale(1.1);
2778
- background: var(--accent-hover);
2779
- }
2780
-
2781
- .play-pause-btn:active {
2782
- transform: scale(.95);
2783
- }
2784
-
2785
- .play-pause-btn.loading {
2786
- opacity: .5;
2787
- pointer-events: none;
2788
- }
2789
-
2790
- .audio-progress {
2791
- flex: 1;
2792
- height: 6px;
2793
- background: rgba(255, 255, 255, .1);
2794
- border-radius: 4px;
2795
- overflow: hidden;
2796
- cursor: pointer;
2797
- position: relative;
2798
- z-index: 1;
2799
- }
2800
-
2801
- .audio-progress-bar {
2802
- height: 100%;
2803
- width: 0%;
2804
- background: var(--accent);
2805
- border-radius: 4px;
2806
- transition: width .1s linear;
2807
- }
2808
-
2809
- .audio-time {
2810
- font-size: .75rem;
2811
- color: var(--text-2);
2812
- min-width: 32px;
2813
- text-align: right;
2814
- font-variant-numeric: tabular-nums;
2815
- }
2816
-
2817
- .voice-select-btn {
2818
- flex-shrink: 0;
2819
- display: flex;
2820
- align-items: center;
2821
- justify-content: center;
2822
- gap: 6px;
2823
- padding: 8px 16px;
2824
- font-size: .78rem;
2825
- font-weight: 600;
2826
- transition: all .25s;
2827
- white-space: nowrap;
2828
- position: relative;
2829
- z-index: 1;
2830
- margin-left: auto;
2831
- }
2832
-
2833
- .voice-select-btn:disabled {
2834
- opacity: 0.7;
2835
- }
2836
-
2837
- /* ── Result state ──────────────────────────────────────── */
2838
- .result-header {
2839
- text-align: center;
2840
- margin-bottom: 20px;
2841
- }
2842
-
2843
- .result-check {
2844
- color: var(--green);
2845
- margin-bottom: 10px;
2846
- }
2847
-
2848
- .result-header h3 {
2849
- font-size: 1.2rem;
2850
- margin-bottom: 4px;
2851
- }
2852
-
2853
- #result-meta {
2854
- color: var(--text-2);
2855
- font-size: .85rem;
2856
- }
2857
-
2858
- .video-player-wrap {
2859
- border-radius: var(--radius);
2860
- overflow: hidden;
2861
- margin-bottom: 20px;
2862
- background: #000;
2863
- }
2864
-
2865
- .result-video {
2866
- width: 100%;
2867
- display: block;
2868
- max-height: 400px;
2869
- }
2870
-
2871
- .result-actions {
2872
- display: flex;
2873
- gap: 12px;
2874
- }
2875
-
2876
- .result-actions .btn {
2877
- flex: 1;
2878
- justify-content: center;
2879
- }
2880
-
2881
- /* ════════════════════════════════════════════════════════
2882
- PRICING
2883
- ════════════════════════════════════════════════════════ */
2884
- .pricing-grid {
2885
- display: grid;
2886
- grid-template-columns: repeat(3, 1fr);
2887
- gap: 20px;
2888
- align-items: start;
2889
- }
2890
-
2891
- .pricing-card {
2892
- background: var(--surface);
2893
- border: 1px solid var(--border);
2894
- border-radius: var(--radius-xl);
2895
- padding: 36px 28px;
2896
- position: relative;
2897
- transition: all .35s cubic-bezier(.4, 0, .2, 1);
2898
- }
2899
-
2900
- .pricing-card:hover {
2901
- transform: translateY(-4px);
2902
- box-shadow: 0 12px 40px rgba(0, 0, 0, .3);
2903
- }
2904
-
2905
- /* Popular card */
2906
- .pricing-popular {
2907
- border-color: var(--accent);
2908
- box-shadow:
2909
- 0 0 0 1px var(--accent),
2910
- 0 0 60px rgba(91, 110, 245, 0.08);
2911
- }
2912
-
2913
- .pricing-popular:hover {
2914
- box-shadow:
2915
- 0 0 0 1px var(--accent),
2916
- 0 12px 60px rgba(91, 110, 245, 0.15);
2917
- }
2918
-
2919
- .pricing-badge {
2920
- font-size: .7rem;
2921
- font-weight: 600;
2922
- text-transform: uppercase;
2923
- letter-spacing: .08em;
2924
- color: var(--text-3);
2925
- margin-bottom: 8px;
2926
- }
2927
-
2928
- .popular-badge {
2929
- color: var(--accent);
2930
- }
2931
-
2932
- .pricing-name {
2933
- font-family: var(--font-display);
2934
- font-size: 1.3rem;
2935
- font-weight: 700;
2936
- margin-bottom: 12px;
2937
- }
2938
-
2939
- .pricing-price {
2940
- margin-bottom: 12px;
2941
- display: flex;
2942
- align-items: baseline;
2943
- gap: 2px;
2944
- }
2945
-
2946
- .price-amount {
2947
- font-family: var(--font-display);
2948
- font-size: 2.8rem;
2949
- font-weight: 700;
2950
- letter-spacing: -0.03em;
2951
- }
2952
-
2953
- .price-period {
2954
- color: var(--text-3);
2955
- font-size: .9rem;
2956
- }
2957
-
2958
- .pricing-desc {
2959
- color: var(--text-2);
2960
- font-size: .85rem;
2961
- margin-bottom: 24px;
2962
- line-height: 1.5;
2963
- }
2964
-
2965
- .pricing-features {
2966
- list-style: none;
2967
- margin-bottom: 28px;
2968
- display: flex;
2969
- flex-direction: column;
2970
- gap: 10px;
2971
- }
2972
-
2973
- .pricing-features li {
2974
- display: flex;
2975
- align-items: center;
2976
- gap: 10px;
2977
- font-size: .875rem;
2978
- color: var(--text-2);
2979
- }
2980
-
2981
- .pricing-features li svg {
2982
- color: var(--green);
2983
- flex-shrink: 0;
2984
- }
2985
-
2986
- /* ════════════════════════════════════════════════════════
2987
- CTA SECTION
2988
- ════════════════════════════════════════════════════════ */
2989
- .cta-section {
2990
- padding: 80px 0;
2991
- position: relative;
2992
- z-index: 1;
2993
- }
2994
-
2995
- .cta-card {
2996
- background: linear-gradient(135deg, var(--surface), var(--surface-2));
2997
- border: 1px solid rgba(91, 110, 245, 0.15);
2998
- border-radius: var(--radius-xl);
2999
- padding: 64px 48px;
3000
- text-align: center;
3001
- position: relative;
3002
- overflow: hidden;
3003
- }
3004
-
3005
- .cta-card::before {
3006
- content: '';
3007
- position: absolute;
3008
- inset: 0;
3009
- background: radial-gradient(ellipse at top, rgba(91, 110, 245, 0.06) 0%, transparent 60%);
3010
- pointer-events: none;
3011
- }
3012
-
3013
- .cta-card h2 {
3014
- font-family: var(--font-display);
3015
- font-size: clamp(1.6rem, 3vw, 2.2rem);
3016
- font-weight: 700;
3017
- margin-bottom: 12px;
3018
- letter-spacing: -0.02em;
3019
- position: relative;
3020
- }
3021
-
3022
- .cta-card p {
3023
- color: var(--text-2);
3024
- font-size: 1rem;
3025
- margin-bottom: 28px;
3026
- position: relative;
3027
- }
3028
-
3029
- .cta-card .btn {
3030
- position: relative;
3031
- }
3032
-
3033
- /* ════════════════════════════════════════════════════════
3034
- FOOTER
3035
- ════════════════════════════════════════════════════════ */
3036
- .footer {
3037
- padding: 64px 0 0;
3038
- border-top: 1px solid var(--border);
3039
- position: relative;
3040
- z-index: 1;
3041
- }
3042
-
3043
- .footer-inner {
3044
- display: flex;
3045
- justify-content: space-between;
3046
- gap: 48px;
3047
- flex-wrap: wrap;
3048
- }
3049
-
3050
- .footer-brand {
3051
- max-width: 280px;
3052
- }
3053
-
3054
- .footer-brand p {
3055
- color: var(--text-3);
3056
- font-size: .85rem;
3057
- margin-top: 12px;
3058
- line-height: 1.6;
3059
- }
3060
-
3061
- .footer-links {
3062
- display: flex;
3063
- gap: 64px;
3064
- }
3065
-
3066
- .footer-col h4 {
3067
- font-size: .75rem;
3068
- font-weight: 600;
3069
- text-transform: uppercase;
3070
- letter-spacing: .08em;
3071
- color: var(--text-3);
3072
- margin-bottom: 16px;
3073
- }
3074
-
3075
- .footer-col a {
3076
- display: block;
3077
- color: var(--text-2);
3078
- text-decoration: none;
3079
- font-size: .875rem;
3080
- padding: 4px 0;
3081
- transition: color .2s;
3082
- }
3083
-
3084
- .footer-col a:hover {
3085
- color: var(--text);
3086
- }
3087
-
3088
- .footer-bottom {
3089
- margin-top: 40px;
3090
- padding: 20px 0;
3091
- border-top: 1px solid var(--border);
3092
- }
3093
-
3094
- .footer-bottom p {
3095
- color: var(--text-3);
3096
- font-size: .8rem;
3097
- }
3098
-
3099
- /* ════════════════════════════════════════════════════════
3100
- COMMAND PALETTE (⌘K)
3101
- ════════════════════════════════════════════════════════ */
3102
- .cmd-palette-overlay {
3103
- position: fixed;
3104
- inset: 0;
3105
- background: rgba(0, 0, 0, 0.7);
3106
- backdrop-filter: blur(8px);
3107
- -webkit-backdrop-filter: blur(8px);
3108
- z-index: 9999;
3109
- display: flex;
3110
- align-items: flex-start;
3111
- justify-content: center;
3112
- padding-top: 20vh;
3113
- opacity: 0;
3114
- visibility: hidden;
3115
- transition: opacity .2s, visibility .2s;
3116
- }
3117
-
3118
- .cmd-palette-overlay.open {
3119
- opacity: 1;
3120
- visibility: visible;
3121
- }
3122
-
3123
- .cmd-palette {
3124
- width: 560px;
3125
- max-width: calc(100vw - 48px);
3126
- background: var(--surface);
3127
- border: 1px solid var(--border-hover);
3128
- border-radius: var(--radius-lg);
3129
- box-shadow:
3130
- 0 0 0 1px rgba(255, 255, 255, 0.05),
3131
- 0 24px 80px rgba(0, 0, 0, .6);
3132
- overflow: hidden;
3133
- transform: translateY(-12px) scale(.97);
3134
- transition: transform .25s cubic-bezier(.4, 0, .2, 1);
3135
- }
3136
-
3137
- .cmd-palette-overlay.open .cmd-palette {
3138
- transform: translateY(0) scale(1);
3139
- }
3140
-
3141
- .cmd-search-wrap {
3142
- display: flex;
3143
- align-items: center;
3144
- gap: 12px;
3145
- padding: 16px 20px;
3146
- border-bottom: 1px solid var(--border);
3147
- }
3148
-
3149
- .cmd-search-icon {
3150
- color: var(--text-3);
3151
- flex-shrink: 0;
3152
- }
3153
-
3154
- .cmd-search {
3155
- flex: 1;
3156
- background: none;
3157
- border: none;
3158
- color: var(--text);
3159
- font-family: var(--font);
3160
- font-size: .95rem;
3161
- outline: none;
3162
- }
3163
-
3164
- .cmd-search::placeholder {
3165
- color: var(--text-3);
3166
- }
3167
-
3168
- .cmd-kbd {
3169
- font-family: var(--font-mono);
3170
- font-size: .7rem;
3171
- color: var(--text-3);
3172
- background: var(--bg);
3173
- border: 1px solid var(--border);
3174
- padding: 3px 8px;
3175
- border-radius: 5px;
3176
- flex-shrink: 0;
3177
- }
3178
-
3179
- .cmd-results {
3180
- max-height: 320px;
3181
- overflow-y: auto;
3182
- padding: 8px;
3183
- }
3184
-
3185
- .cmd-group-label {
3186
- font-size: .7rem;
3187
- font-weight: 600;
3188
- color: var(--text-3);
3189
- text-transform: uppercase;
3190
- letter-spacing: .08em;
3191
- padding: 8px 12px 4px;
3192
- }
3193
-
3194
- .cmd-item {
3195
- display: flex;
3196
- align-items: center;
3197
- gap: 12px;
3198
- padding: 10px 12px;
3199
- border-radius: 8px;
3200
- cursor: pointer;
3201
- transition: background .15s;
3202
- color: var(--text-2);
3203
- font-size: .875rem;
3204
- text-decoration: none;
3205
- }
3206
-
3207
- .cmd-item:hover,
3208
- .cmd-item.active {
3209
- background: rgba(91, 110, 245, 0.08);
3210
- color: var(--text);
3211
- }
3212
-
3213
- .cmd-item-icon {
3214
- width: 32px;
3215
- height: 32px;
3216
- display: flex;
3217
- align-items: center;
3218
- justify-content: center;
3219
- background: var(--bg);
3220
- border-radius: 8px;
3221
- color: var(--text-3);
3222
- flex-shrink: 0;
3223
- }
3224
-
3225
- .cmd-item:hover .cmd-item-icon,
3226
- .cmd-item.active .cmd-item-icon {
3227
- color: var(--accent);
3228
- background: var(--accent-dim);
3229
- }
3230
-
3231
- .cmd-item-text {
3232
- flex: 1;
3233
- }
3234
-
3235
- .cmd-item-hint {
3236
- font-size: .75rem;
3237
- color: var(--text-3);
3238
- }
3239
-
3240
- .cmd-footer {
3241
- display: flex;
3242
- align-items: center;
3243
- gap: 16px;
3244
- padding: 10px 20px;
3245
- border-top: 1px solid var(--border);
3246
- font-size: .7rem;
3247
- color: var(--text-3);
3248
- }
3249
-
3250
- .cmd-footer-keys {
3251
- display: flex;
3252
- align-items: center;
3253
- gap: 4px;
3254
- }
3255
-
3256
- .cmd-footer-keys kbd {
3257
- font-family: var(--font-mono);
3258
- background: var(--bg);
3259
- border: 1px solid var(--border);
3260
- padding: 1px 6px;
3261
- border-radius: 4px;
3262
- font-size: .65rem;
3263
- }
3264
-
3265
- /* ════════════════════════════════════════════════════════
3266
- REVEAL ANIMATION
3267
- ════════════════════════════════════════════════════════ */
3268
- .reveal {
3269
- opacity: 0;
3270
- transform: translateY(24px);
3271
- transition: opacity .7s cubic-bezier(.4, 0, .2, 1), transform .7s cubic-bezier(.4, 0, .2, 1);
3272
- }
3273
-
3274
- .reveal.visible {
3275
- opacity: 1;
3276
- transform: translateY(0);
3277
- }
3278
-
3279
- /* ════════════════════════════════════════════════════════
3280
- RESPONSIVE
3281
- ════════════════════════════════════════════════════════ */
3282
- @media (max-width: 999px) {
3283
- .app-container {
3284
- flex-direction: column;
3285
- max-width: 100%;
3286
- }
3287
-
3288
- .app-panel {
3289
- flex: 0 0 auto !important;
3290
- width: 100% !important;
3291
- /* swap width animation for vertical slide */
3292
- transform: translateY(16px);
3293
- transition:
3294
- opacity 0.4s ease,
3295
- transform 0.4s ease;
3296
- }
3297
-
3298
- /* On mobile: hide input panel during processing so logs are visible */
3299
- .state-processing #app-input {
3300
- display: none !important;
3301
- }
3302
-
3303
- /* Processing panel: force visible with all overrides */
3304
- .state-processing #app-processing {
3305
- display: block !important;
3306
- opacity: 1 !important;
3307
- overflow: visible !important;
3308
- width: 100% !important;
3309
- height: auto !important;
3310
- transform: none !important;
3311
- pointer-events: auto !important;
3312
- }
3313
-
3314
- .state-processing #app-result {
3315
- transform: translateY(0);
3316
- opacity: 1;
3317
- }
3318
-
3319
- /* On mobile: hide input + processing during result so video is front and center */
3320
- .state-result #app-input,
3321
- .state-result #app-processing {
3322
- display: none !important;
3323
- }
3324
-
3325
- .state-result #app-result {
3326
- display: block !important;
3327
- opacity: 1 !important;
3328
- overflow: visible !important;
3329
- transform: none !important;
3330
- pointer-events: auto !important;
3331
- }
3332
- }
3333
-
3334
- @media (max-width: 900px) {
3335
- .features-grid {
3336
- grid-template-columns: repeat(2, 1fr);
3337
- }
3338
-
3339
- .demos-lanes {
3340
- grid-template-columns: repeat(2, minmax(0, 1fr));
3341
- }
3342
-
3343
- .pricing-grid {
3344
- grid-template-columns: 1fr;
3345
- max-width: 400px;
3346
- margin: 0 auto;
3347
- }
3348
-
3349
- .footer-links {
3350
- gap: 32px;
3351
- }
3352
-
3353
- .contrast-grid {
3354
- grid-template-columns: 1fr;
3355
- gap: 12px;
3356
- }
3357
-
3358
- .contrast-vs {
3359
- padding: 4px 0;
3360
- }
3361
-
3362
- .compare-wrap {
3363
- padding: 20px;
3364
- }
3365
- }
3366
-
3367
- @media (max-width: 640px) {
3368
- .nav-links {
3369
- display: none;
3370
- }
3371
-
3372
- .mobile-menu-btn {
3373
- display: flex;
3374
- }
3375
-
3376
- .hero-stats {
3377
- gap: 16px;
3378
- margin-top: 32px;
3379
- }
3380
-
3381
- .hero-stat-div {
3382
- width: 40px;
3383
- height: 1px;
3384
- }
3385
-
3386
- .steps-grid {
3387
- flex-direction: column;
3388
- }
3389
-
3390
- .step-connector {
3391
- transform: rotate(90deg);
3392
- padding: 8px 0;
3393
- }
3394
-
3395
- .features-grid {
3396
- grid-template-columns: 1fr;
3397
- }
3398
-
3399
- .demos-lanes {
3400
- grid-template-columns: 1fr;
3401
- max-width: 340px;
3402
- margin: 0 auto;
3403
- }
3404
-
3405
- .demos-lane {
3406
- transform: none !important;
3407
- }
3408
-
3409
- .language-row {
3410
- flex-direction: column;
3411
- }
3412
-
3413
- .lang-swap {
3414
- align-self: center;
3415
- transform: rotate(90deg);
3416
- }
3417
-
3418
- .result-actions {
3419
- flex-direction: column;
3420
- }
3421
-
3422
- .voice-card {
3423
- flex-wrap: wrap;
3424
- }
3425
-
3426
- .voice-card-player {
3427
- width: 100%;
3428
- }
3429
-
3430
- .voice-preview-panel {
3431
- padding: 16px;
3432
- }
3433
-
3434
- .terminal-body {
3435
- max-height: 50vh;
3436
- }
3437
-
3438
- .footer-inner {
3439
- flex-direction: column;
3440
- gap: 32px;
3441
- }
3442
-
3443
- .footer-links {
3444
- flex-direction: column;
3445
- gap: 24px;
3446
- }
3447
-
3448
- .cta-card {
3449
- padding: 40px 24px;
3450
- }
3451
- }
3452
-
3453
- /* Scrollbar */
3454
- ::-webkit-scrollbar {
3455
- width: 6px;
3456
- }
3457
-
3458
- ::-webkit-scrollbar-track {
3459
- background: transparent;
3460
- }
3461
-
3462
- ::-webkit-scrollbar-thumb {
3463
- background: var(--border);
3464
- border-radius: 3px;
3465
- }
3466
-
3467
- ::-webkit-scrollbar-thumb:hover {
3468
- background: var(--border-hover);
3469
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
server.py CHANGED
@@ -91,7 +91,8 @@ ALLOWED_ORIGINS = _parse_allowed_origins(
91
 
92
  # ── App ────────────────────────────────────────────────
93
  router = APIRouter()
94
- limiter = Limiter(key_func=get_remote_address)
 
95
  # Note: app.state.limiter, exception handlers, and SlowAPIMiddleware
96
  # are now configured on the main Server instance in app.py.
97
 
@@ -856,7 +857,9 @@ async def startup_event():
856
  (ARTIFACTS_ROOT / "data").mkdir(parents=True, exist_ok=True)
857
  (ARTIFACTS_ROOT / "tmp").mkdir(parents=True, exist_ok=True)
858
 
859
- if artifact_reaper_task is None or artifact_reaper_task.done():
 
 
860
  artifact_reaper_task = asyncio.create_task(_artifact_reaper_loop())
861
 
862
 
 
91
 
92
  # ── App ────────────────────────────────────────────────
93
  router = APIRouter()
94
+ _RATE_LIMIT_ENABLED = os.getenv("DISABLE_RATE_LIMIT", "").lower() not in ("1", "true", "yes")
95
+ limiter = Limiter(key_func=get_remote_address, enabled=_RATE_LIMIT_ENABLED)
96
  # Note: app.state.limiter, exception handlers, and SlowAPIMiddleware
97
  # are now configured on the main Server instance in app.py.
98
 
 
857
  (ARTIFACTS_ROOT / "data").mkdir(parents=True, exist_ok=True)
858
  (ARTIFACTS_ROOT / "tmp").mkdir(parents=True, exist_ok=True)
859
 
860
+ if os.getenv("DISABLE_CLEANUP", "").lower() in ("1", "true", "yes"):
861
+ print("[reaper] DISABLE_CLEANUP is set — artifact reaper will not run")
862
+ elif artifact_reaper_task is None or artifact_reaper_task.done():
863
  artifact_reaper_task = asyncio.create_task(_artifact_reaper_loop())
864
 
865