@mevdragon/vidfarm-devcli 0.18.1 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.agents/skills/editor-capabilities/SKILL.md +166 -0
  2. package/.agents/skills/editor-capabilities/references/re-theme-walkthrough.md +58 -0
  3. package/.agents/skills/vidfarm-media/SKILL.md +2 -0
  4. package/SKILL.director.md +174 -16
  5. package/SKILL.platform.md +7 -3
  6. package/demo/dist/app.css +1 -1
  7. package/demo/dist/app.js +77 -75
  8. package/dist/src/app.js +1513 -145
  9. package/dist/src/cli.js +1365 -105
  10. package/dist/src/config.js +7 -0
  11. package/dist/src/devcli/clips.js +3 -0
  12. package/dist/src/devcli/composition-edit.js +396 -8
  13. package/dist/src/devcli/local-backend.js +123 -0
  14. package/dist/src/devcli/migrate-local.js +140 -0
  15. package/dist/src/devcli/sync.js +311 -0
  16. package/dist/src/devcli/timeline-edit.js +208 -1
  17. package/dist/src/editor-chat.js +33 -17
  18. package/dist/src/frontend/file-directory.js +271 -20
  19. package/dist/src/frontend/homepage-view.js +3 -3
  20. package/dist/src/frontend/template-editor-chat.js +6 -3
  21. package/dist/src/page-shell.js +1 -1
  22. package/dist/src/primitive-registry.js +409 -4
  23. package/dist/src/reskin/chat-page.js +103 -15
  24. package/dist/src/reskin/discover-page.js +247 -10
  25. package/dist/src/reskin/document.js +147 -10
  26. package/dist/src/reskin/inpaint-clipper-page.js +649 -0
  27. package/dist/src/reskin/inpaint-page.js +2459 -452
  28. package/dist/src/reskin/inpaint-video-page.js +1339 -0
  29. package/dist/src/reskin/library-page.js +324 -82
  30. package/dist/src/reskin/theme.js +36 -11
  31. package/dist/src/services/billing.js +4 -0
  32. package/dist/src/services/clip-curation/hunt.js +2 -0
  33. package/dist/src/services/clip-records.js +28 -0
  34. package/dist/src/services/file-directory.js +6 -3
  35. package/dist/src/services/hyperframes.js +283 -3
  36. package/dist/src/services/serverless-records.js +43 -0
  37. package/dist/src/services/storage.js +24 -2
  38. package/package.json +1 -1
  39. package/public/assets/file-directory-app.js +2 -2
  40. package/public/assets/homepage-client-app.js +1 -1
  41. package/public/assets/page-runtime-client-app.js +2 -2
  42. package/public/assets/placeholders/scene-placeholder.png +0 -0
@@ -152,7 +152,12 @@ function projectCard(post, accountId, index) {
152
152
  </div>
153
153
  </div>
154
154
  </div>`;
155
- return `<article class="rk-library-card rk-card-hover" data-status="${rkEscape(post.status)}" data-title="${rkEscape(searchHay)}" data-post-id="${rkEscape(post.post_id)}"
155
+ // Folder identity for the client-side folder view: the real folder_path if the
156
+ // post has one, else its tracer (a synthetic group that materializes a folder
157
+ // when renamed). data-poster fuels the folder-tile mosaic.
158
+ const folder = (post.folder_path || "").replace(/^\/+|\/+$/g, "");
159
+ const posterUrl = (post.media.find((a) => isVideoAsset(a)) || post.media.find((a) => isImageAsset(a)) || primaryAsset(post))?.url || "";
160
+ return `<article class="rk-library-card rk-card-hover" data-status="${rkEscape(post.status)}" data-title="${rkEscape(searchHay)}" data-post-id="${rkEscape(post.post_id)}" data-folder="${rkEscape(folder)}" data-tracer="${rkEscape(post.tracer || "")}" data-poster="${rkEscape(posterUrl)}">
156
161
  <button type="button" class="rk-library-poster" data-rk-view="${index}" aria-label="Play ${rkEscape(title)}">
157
162
  ${posterMedia(post)}
158
163
  <span class="rk-pill ${s.pill} rk-library-pstatus"><span class="rk-library-dot"></span>${rkEscape(s.short)}</span>
@@ -216,6 +221,10 @@ function approvedPanel(input, tabs) {
216
221
  ${LOGS_BTN}
217
222
  </div>
218
223
 
224
+ <div class="rk-raws-bar rk-library-foldbar" data-rk-approved-bar data-rk-account="${rkEscape(accountId)}" hidden>
225
+ <nav class="rk-raws-crumbs" data-rk-approved-crumbs aria-label="Breadcrumb"></nav>
226
+ </div>
227
+
219
228
  <div class="rk-library-grid" data-rk-grid>
220
229
  ${cards}
221
230
  </div>
@@ -278,10 +287,6 @@ function clipsPanel(tabs) {
278
287
  <p class="rk-muted">Click <b>Import source</b> to mine a long video into tagged, searchable short clips.</p>
279
288
  </div>
280
289
 
281
- <div class="rk-raws-hover" id="rkRawsHover" hidden aria-hidden="true">
282
- <video class="rk-raws-hover-vid" muted loop playsinline preload="none"></video>
283
- <div class="rk-raws-hover-cap" id="rkRawsHoverCap"></div>
284
- </div>
285
290
  ${LIBRARY_VIEWER_HTML}`;
286
291
  }
287
292
  /* ──────────────────────────────── Files tab ───────────────────────────────── */
@@ -293,7 +298,13 @@ function clipsPanel(tabs) {
293
298
  // (/assets/file-directory-app.js) already ships on every full-chrome reskin
294
299
  // page and auto-mounts any `[data-rk-directory]`, so no page script is needed;
295
300
  // preview routes through the chrome's `window.__rkDirectoryHost.onPreview`.
296
- function filesPanel(tabs) {
301
+ function filesPanel(tabs, cloudAvailable = false, initialPath) {
302
+ // `?path=` deep-links straight into a folder (e.g. /library/files?path=/approved/swipes).
303
+ // The client's parseDirectoryPath only accepts the known roots, so any junk or
304
+ // traversal falls back to the virtual root "/" — safe to reflect verbatim.
305
+ const initialAttr = initialPath && initialPath.trim()
306
+ ? ` data-initial-path="${rkEscape(initialPath.trim())}"`
307
+ : "";
297
308
  return `
298
309
  <div class="rk-library-topbar">
299
310
  ${tabs}
@@ -301,7 +312,7 @@ function filesPanel(tabs) {
301
312
  </div>
302
313
 
303
314
  <div class="rk-files-explorer">
304
- <div data-rk-directory data-mode="standalone" data-show-upload="true" data-show-multiselect="true" data-primary-click="preview"></div>
315
+ <div data-rk-directory data-mode="standalone" data-show-upload="true" data-show-multiselect="true" data-primary-click="preview"${cloudAvailable ? ` data-cloud-available="true"` : ""}${initialAttr}></div>
305
316
  </div>`;
306
317
  }
307
318
  const CLIPS_DIALOG = `
@@ -310,7 +321,7 @@ const CLIPS_DIALOG = `
310
321
  <div class="rk-card-head">
311
322
  <span class="rk-eyebrow">Import source</span>
312
323
  <h2>Import a video</h2>
313
- <p class="rk-muted">Paste a video URL — TikTok, YouTube, Instagram, X, or a direct link. We'll add it to your raws library.</p>
324
+ <p class="rk-muted">Paste a video URL — TikTok, YouTube, Instagram, X, or a direct link. We'll open the Clipper to preview it and save an exact subrange to your raws.</p>
314
325
  </div>
315
326
  <div class="rk-field">
316
327
  <label class="rk-label" for="rkScanUrl">Video URL</label>
@@ -322,7 +333,7 @@ const CLIPS_DIALOG = `
322
333
  </div>
323
334
  <label class="rk-clips-toggle" for="rkScanClip">
324
335
  <input type="checkbox" id="rkScanClip" name="clip" />
325
- <span><b>Clip raws</b> — mine this video into matching moments with AI, instead of importing the whole thing.</span>
336
+ <span><b>Clip raws with AI</b> — auto-mine this video into matching moments, instead of hand-picking a subrange in the Clipper.</span>
326
337
  </label>
327
338
  <div class="rk-clips-advanced" id="rkClipsAdvanced" hidden>
328
339
  <div class="rk-field">
@@ -343,7 +354,7 @@ const CLIPS_DIALOG = `
343
354
  <p class="rk-hint rk-clips-dialog-note" id="rkScanNote">We'll pull the whole video into your raws library.</p>
344
355
  <div class="rk-clips-dialog-actions">
345
356
  <button type="button" class="rk-btn rk-btn-ghost" id="rkClipsScanCancel">Cancel</button>
346
- <button type="submit" class="rk-btn rk-btn-gold" id="rkClipsScanSubmit">Import video <span class="rk-arrow">→</span></button>
357
+ <button type="submit" class="rk-btn rk-btn-gold" id="rkClipsScanSubmit">Open clipper <span class="rk-arrow">→</span></button>
347
358
  </div>
348
359
  </form>
349
360
  </dialog>`;
@@ -354,24 +365,15 @@ const APPROVED_SCRIPT = `
354
365
  var empty=root.querySelector('[data-rk-empty]');
355
366
  var search=root.querySelector('[data-rk-search]');
356
367
  var grid=root.querySelector('[data-rk-grid]');
368
+ var foldBar=root.querySelector('[data-rk-approved-bar]');
369
+ var crumbsEl=root.querySelector('[data-rk-approved-crumbs]');
370
+ var account=(foldBar&&foldBar.getAttribute('data-rk-account'))||'';
357
371
  var cards=Array.prototype.slice.call(root.querySelectorAll('.rk-library-card'));
358
372
  var total=cards.length;
359
373
  function syncPlaceholder(){
360
374
  if(!search)return;
361
375
  search.setAttribute('placeholder','Search '+total+' approved post'+(total===1?'':'s')+String.fromCharCode(8230));
362
376
  }
363
- function apply(){
364
- var q=(search&&search.value||'').trim().toLowerCase();
365
- var shown=0;
366
- cards.forEach(function(card){
367
- if(card.getAttribute('data-removed'))return;
368
- var visible=!q||(card.getAttribute('data-title')||'').indexOf(q)!==-1;
369
- card.hidden=!visible;
370
- if(visible)shown++;
371
- });
372
- if(empty)empty.classList.toggle('is-visible',shown===0);
373
- }
374
- if(search)search.addEventListener('input',apply);
375
377
  // hover-to-play the real MP4 previews on the grid cards
376
378
  root.querySelectorAll('video.rk-library-video').forEach(function(v){
377
379
  var card=v.closest('.rk-library-card');
@@ -379,6 +381,146 @@ const APPROVED_SCRIPT = `
379
381
  card.addEventListener('mouseenter',function(){ if(v.play){var p=v.play(); if(p&&p.catch)p.catch(function(){});} });
380
382
  card.addEventListener('mouseleave',function(){ if(v.pause)v.pause(); });
381
383
  });
384
+
385
+ /* ── folder view (mirrors the Raws tab): group cards by folder_path, fall back
386
+ to tracer for legacy posts; drill into a folder; search flattens. ─────── */
387
+ function cssUrl(u){ return "'"+String(u||'').replace(/['")\\\\]/g,'').replace(/\\s/g,'%20')+"'"; }
388
+ function activeCards(){ return cards.filter(function(c){ return !c.getAttribute('data-removed'); }); }
389
+ var FOLDERS=[], LOOSE=[], MODE='root', CUR='';
390
+ function metaOf(card){
391
+ var fp=(card.getAttribute('data-folder')||'').replace(/^\\/+|\\/+$/g,'');
392
+ var tr=card.getAttribute('data-tracer')||'';
393
+ var key = fp ? ('fp:'+fp) : (tr ? ('tr:'+tr) : '');
394
+ return { card:card, fp:fp, tr:tr, key:key, poster:card.getAttribute('data-poster')||'', postId:card.getAttribute('data-post-id')||'' };
395
+ }
396
+ function buildFolders(){
397
+ var map={}; FOLDERS=[]; LOOSE=[];
398
+ activeCards().forEach(function(card){
399
+ var m=metaOf(card);
400
+ if(!m.key){ LOOSE.push(m); return; }
401
+ var f=map[m.key];
402
+ if(!f){
403
+ f={ id:m.key, folderPath:m.fp||null, tracer:m.tr, name:(m.fp||m.tr||'Untitled'), items:[] };
404
+ map[m.key]=f; FOLDERS.push(f);
405
+ }
406
+ f.items.push(m);
407
+ });
408
+ }
409
+ function folderById(id){ for(var i=0;i<FOLDERS.length;i++){ if(FOLDERS[i].id===id)return FOLDERS[i]; } return null; }
410
+ function clearTiles(){ Array.prototype.slice.call(grid.querySelectorAll('[data-approved-tile]')).forEach(function(t){ t.parentNode&&t.parentNode.removeChild(t); }); }
411
+ function folderTileEl(f){
412
+ var thumbs=f.items.filter(function(m){ return m.poster; }).slice(0,4);
413
+ var mosaic = thumbs.length
414
+ ? '<span class="rk-raws-folder-mosaic'+(thumbs.length===1?' is-single':'')+'">'+thumbs.map(function(m){ return '<span class="rk-raws-folder-cell" style="background-image:url('+cssUrl(m.poster)+')"></span>'; }).join('')+'</span>'
415
+ : '<span class="rk-raws-folder-mosaic is-empty">'+${JSON.stringify(ICON_FOLDER)}+'</span>';
416
+ var n=f.items.length;
417
+ var canonPath='/approved'+(f.folderPath?'/'+f.folderPath:(f.tracer?'/'+f.tracer:''));
418
+ var html='<div class="rk-raws-folder-wrap" data-approved-tile>'
419
+ + '<button type="button" class="rk-raws-folder rk-card-hover" data-approved-folder="'+attr(f.id)+'">'
420
+ + mosaic
421
+ + '<span class="rk-raws-folder-meta"><span class="rk-raws-folder-ic">'+${JSON.stringify(ICON_FOLDER)}+'</span>'
422
+ + '<span class="rk-raws-folder-txt"><span class="rk-raws-folder-name" title="'+attr(f.name)+'">'+esc(f.name)+'</span>'
423
+ + '<span class="rk-raws-folder-count">'+n+' post'+(n===1?'':'s')+'</span></span></span>'
424
+ + '</button>'
425
+ + '<div class="rk-raws-folder-menu" data-folder-menu>'
426
+ + '<button type="button" class="rk-raws-folder-kebab" data-approved-fkebab aria-label="Folder actions" aria-haspopup="true" aria-expanded="false">'+${JSON.stringify(ICON_MORE)}+'</button>'
427
+ + '<div class="rk-raws-folder-pop" data-folder-pop hidden role="menu">'
428
+ + '<button type="button" role="menuitem" data-approved-frename="'+attr(f.id)+'">Rename folder</button>'
429
+ + '<button type="button" role="menuitem" data-folder-copy="'+attr(canonPath)+'">Copy path</button>'
430
+ + '</div></div>'
431
+ + '</div>';
432
+ var tmp=document.createElement('div'); tmp.innerHTML=html; return tmp.firstChild;
433
+ }
434
+ function renderCrumbs(){
435
+ if(!crumbsEl)return;
436
+ if(MODE==='root'){ crumbsEl.innerHTML='<span class="rk-raws-crumb is-current">All posts</span>'; return; }
437
+ var html='<button type="button" class="rk-raws-crumb rk-raws-crumb-link" data-approved-crumb-root>'+${JSON.stringify(ICON_CHEVLEFT)}+'<span>All posts</span></button>'
438
+ + '<span class="rk-raws-crumb-sep" aria-hidden="true">/</span>';
439
+ if(MODE==='folder'){ var f=folderById(CUR); html+='<span class="rk-raws-crumb is-current">'+esc(f?f.name:'Folder')+' <span class="rk-raws-crumb-n">'+(f?f.items.length:0)+'</span></span>'; }
440
+ else { html+='<span class="rk-raws-crumb is-current">Search</span>'; }
441
+ crumbsEl.innerHTML=html;
442
+ }
443
+ function showBar(show){ if(foldBar)foldBar.hidden=!show; }
444
+ function setVisible(pred){ var shown=0; activeCards().forEach(function(c){ var v=pred(metaOf(c)); c.hidden=!v; if(v)shown++; }); return shown; }
445
+ function paintRoot(){
446
+ MODE='root'; CUR=''; clearTiles(); buildFolders();
447
+ // folder tiles first (in order, before the cards), then loose cards remain.
448
+ var frag=document.createDocumentFragment();
449
+ FOLDERS.forEach(function(f){ frag.appendChild(folderTileEl(f)); });
450
+ grid.insertBefore(frag,grid.firstChild);
451
+ var looseShown=setVisible(function(m){ return !m.key; });
452
+ showBar(FOLDERS.length>0);
453
+ renderCrumbs();
454
+ if(empty)empty.classList.toggle('is-visible',FOLDERS.length===0&&looseShown===0);
455
+ }
456
+ function paintFolder(id){
457
+ var f=folderById(id); if(!f){ paintRoot(); return; }
458
+ MODE='folder'; CUR=id; clearTiles(); showBar(true); renderCrumbs();
459
+ var shown=setVisible(function(m){ return m.key===id; });
460
+ if(empty)empty.classList.toggle('is-visible',shown===0);
461
+ }
462
+ function paintSearch(q){
463
+ MODE='search'; clearTiles(); showBar(false);
464
+ var shown=setVisible(function(m){ return (m.card.getAttribute('data-title')||'').indexOf(q)!==-1; });
465
+ if(empty)empty.classList.toggle('is-visible',shown===0);
466
+ }
467
+ function repaint(){ if(MODE==='folder'&&folderById(CUR))paintFolder(CUR); else paintRoot(); }
468
+ function apply(){
469
+ var q=(search&&search.value||'').trim().toLowerCase();
470
+ if(q)paintSearch(q); else repaint();
471
+ }
472
+ if(search)search.addEventListener('input',apply);
473
+
474
+ /* ── folder tile clicks: open / rename / copy-path + breadcrumb back ──────── */
475
+ function closeFolderMenus(except){
476
+ Array.prototype.slice.call(grid.querySelectorAll('[data-folder-menu]')).forEach(function(mn){
477
+ if(mn===except)return;
478
+ var pop=mn.querySelector('[data-folder-pop]'); if(pop)pop.hidden=true;
479
+ var kb=mn.querySelector('[data-approved-fkebab]'); if(kb)kb.setAttribute('aria-expanded','false');
480
+ });
481
+ }
482
+ function copyText(txt,ok){ try{ if(navigator.clipboard&&navigator.clipboard.writeText){ navigator.clipboard.writeText(txt); } else { var ta=document.createElement('textarea'); ta.value=txt; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); } if(ok)ok(); }catch(e){} }
483
+ function slugFolder(v){ return String(v||'').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'').slice(0,60); }
484
+ function renameApprovedFolder(id){
485
+ var f=folderById(id); if(!f)return;
486
+ var current=f.folderPath?f.folderPath.split('/').pop():(f.tracer||f.name);
487
+ var next=(window.prompt('Rename folder',current||'')||'').trim();
488
+ if(!next||next===current)return;
489
+ if(f.folderPath){
490
+ fetch('/api/v1/user/me/directory/rename',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json',Accept:'application/json'},body:JSON.stringify({path:'/approved/'+f.folderPath,new_name:next})})
491
+ .then(function(r){ return r.ok?r.json():r.json().catch(function(){return{};}).then(function(d){ throw new Error((d&&d.error)||'Rename failed'); }); })
492
+ .then(function(){ window.location.reload(); })
493
+ .catch(function(err){ window.alert((err&&err.message)||'Rename failed'); });
494
+ } else {
495
+ // synthetic tracer group → materialize a real folder by moving each post.
496
+ if(!account){ window.alert('Cannot rename this group here.'); return; }
497
+ var target=slugFolder(next); if(!target){ window.alert('Pick a folder name with letters or numbers.'); return; }
498
+ Promise.all(f.items.map(function(m){
499
+ return fetch('/u/'+encodeURIComponent(account)+'/approved/posts/'+encodeURIComponent(m.postId),{method:'PATCH',credentials:'same-origin',headers:{'Content-Type':'application/json',Accept:'application/json'},body:JSON.stringify({folder_path:target})}).then(function(r){ return r.ok; });
500
+ })).then(function(){ window.location.reload(); }).catch(function(){ window.alert('Rename failed'); });
501
+ }
502
+ }
503
+ if(grid){
504
+ grid.addEventListener('click',function(e){
505
+ var t=e.target;
506
+ var kb=t&&t.closest?t.closest('[data-approved-fkebab]'):null;
507
+ if(kb){ e.preventDefault(); e.stopPropagation(); var mn=kb.closest('[data-folder-menu]'); var pop=mn&&mn.querySelector('[data-folder-pop]'); var willOpen=pop&&pop.hidden; closeFolderMenus(mn); if(pop){ pop.hidden=!willOpen; kb.setAttribute('aria-expanded',willOpen?'true':'false'); } return; }
508
+ var rn=t&&t.closest?t.closest('[data-approved-frename]'):null;
509
+ if(rn){ e.preventDefault(); closeFolderMenus(null); renameApprovedFolder(rn.getAttribute('data-approved-frename')); return; }
510
+ var cp=t&&t.closest?t.closest('[data-folder-copy]'):null;
511
+ if(cp){ e.preventDefault(); closeFolderMenus(null); copyText(cp.getAttribute('data-folder-copy')); return; }
512
+ var fo=t&&t.closest?t.closest('[data-approved-folder]'):null;
513
+ if(fo){ e.preventDefault(); closeFolderMenus(null); if(search)search.value=''; paintFolder(fo.getAttribute('data-approved-folder')); return; }
514
+ });
515
+ }
516
+ if(crumbsEl){
517
+ crumbsEl.addEventListener('click',function(e){
518
+ var r=e.target&&e.target.closest?e.target.closest('[data-approved-crumb-root]'):null;
519
+ if(!r)return; e.preventDefault(); if(search)search.value=''; paintRoot();
520
+ });
521
+ }
522
+ document.addEventListener('click',function(e){ var t=e.target; if(!(t&&t.closest&&t.closest('[data-folder-menu]')))closeFolderMenus(null); });
523
+
382
524
  apply();
383
525
 
384
526
  // ── per-card "⋯" overflow menu: Copy ID + Delete (popconfirm) ────────────
@@ -470,7 +612,7 @@ const APPROVED_SCRIPT = `
470
612
  ? '<video class="rk-library-viewer-media" data-vsrc="'+attr(t.url)+'"'+(t.poster?' poster="'+attr(t.poster)+'"':'')+' loop playsinline preload="none"></video>'
471
613
  : '<img class="rk-library-viewer-media" src="'+attr(t.url||t.poster)+'" alt="'+attr(t.title||'Published cut')+'">';
472
614
  var cta=t.editor_href
473
- ? '<a class="rk-btn rk-btn-gold rk-btn-sm rk-library-viewer-cta" href="'+attr(t.editor_href)+'">Open in editor</a>'
615
+ ? '<a class="rk-btn rk-btn-gold rk-btn-sm rk-library-viewer-cta" href="'+attr(t.editor_href)+'" target="_blank" rel="noreferrer">Open in editor</a>'
474
616
  : '';
475
617
  var view=t.post_href
476
618
  ? '<a class="rk-btn rk-btn-ghost-light rk-btn-sm rk-library-viewer-cta" href="'+attr(t.post_href)+'" target="_blank" rel="noreferrer">View post</a>'
@@ -552,9 +694,6 @@ const CLIPS_SCRIPT = `
552
694
  var empty=root.getElementById('rkClipsEmpty');
553
695
  var form=root.getElementById('rkClipsSearch');
554
696
  var query=root.getElementById('rkClipsQuery');
555
- var hoverEl=root.getElementById('rkRawsHover');
556
- var hoverVid=hoverEl&&hoverEl.querySelector('.rk-raws-hover-vid');
557
- var hoverCap=root.getElementById('rkRawsHoverCap');
558
697
  var canHover=!(window.matchMedia&&window.matchMedia('(hover:none)').matches);
559
698
 
560
699
  // ── file-directory state ────────────────────────────────────────────────
@@ -586,16 +725,34 @@ const CLIPS_SCRIPT = `
586
725
  query.setAttribute('placeholder','Search '+n+' raw clip'+(n===1?'':'s')+String.fromCharCode(8230));
587
726
  }
588
727
 
589
- // Group raws into folders by their source video (source_video_id, falling back
590
- // to source_filename). First-seen order is preserved.
728
+ function normFp(v){ return (v==null?'':String(v)).replace(/^\\/+|\\/+$/g,''); }
729
+ function slugFolder(v){ return String(v||'').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'').slice(0,60); }
730
+
731
+ // Group raws into REAL directory folders keyed on each clip's folder_path
732
+ // (canonical /raws/<folder_path>) — the same folders the file directory and the
733
+ // AI agent see, so a folder can be renamed and clips moved between them. Legacy
734
+ // clips with no folder_path fall back to a synthetic source-derived group; those
735
+ // aren't backed by a stored folder yet, so renaming one MATERIALIZES a folder by
736
+ // moving its clips (see renameFolder). First-seen order is preserved.
591
737
  function buildFolders(){
592
738
  var map={}; FOLDERS=[];
593
739
  ALL.forEach(function(c){
594
- var id=String(c.source_video_id||c.source_filename||'ungrouped');
595
- var f=map[id];
596
- if(!f){ f={id:id,name:c.source_filename||'Untitled source',clips:[]}; map[id]=f; FOLDERS.push(f); }
740
+ var fp=normFp(c.folder_path);
741
+ var key,f;
742
+ if(fp){ key='fp:'+fp; }
743
+ else { key='src:'+String(c.source_video_id||c.source_filename||'ungrouped'); }
744
+ f=map[key];
745
+ if(!f){
746
+ f={ id:key, folderPath:fp||null, name:fp||(c.source_filename||'Untitled source'), clips:[] };
747
+ map[key]=f; FOLDERS.push(f);
748
+ }
597
749
  f.clips.push(c);
598
750
  });
751
+ // Float the full-length original video (role:"source") to the top of its
752
+ // folder so it reads as the reference the mined clips were cut from.
753
+ FOLDERS.forEach(function(f){
754
+ f.clips.sort(function(a,b){ return (b.is_source_original?1:0)-(a.is_source_original?1:0); });
755
+ });
599
756
  }
600
757
 
601
758
  // Optimistic "importing…" placeholder card (spinner, or an error state with a
@@ -628,14 +785,31 @@ const CLIPS_SCRIPT = `
628
785
  } else {
629
786
  mosaic='<span class="rk-raws-folder-mosaic is-empty">'+${JSON.stringify(ICON_FOLDER)}+'</span>';
630
787
  }
631
- var n=folder.clips.length;
632
- return '<button type="button" class="rk-raws-folder rk-card-hover" data-folder-id="'+attr(folder.id)+'">'
788
+ // Count MINED clips only — the full-length original is a reference raw, not a
789
+ // mined scene and note the presence of a full video alongside the count.
790
+ var hasFull=folder.clips.some(function(c){ return c.is_source_original; });
791
+ var n=folder.clips.filter(function(c){ return !c.is_source_original; }).length;
792
+ var countTxt=n+' clip'+(n===1?'':'s')+(hasFull?' · full video':'');
793
+ var canonPath='/raws'+(folder.folderPath?'/'+folder.folderPath:'');
794
+ // Kebab: rename the folder + copy its canonical /raws path. Sits OUTSIDE the
795
+ // open-folder button (buttons can't nest); its own click is caught before the
796
+ // tile's navigation handler.
797
+ var menu='<div class="rk-raws-folder-menu" data-folder-menu>'
798
+ + '<button type="button" class="rk-raws-folder-kebab" data-folder-kebab aria-label="Folder actions" aria-haspopup="true" aria-expanded="false">'+${JSON.stringify(ICON_MORE)}+'</button>'
799
+ + '<div class="rk-raws-folder-pop" data-folder-pop hidden role="menu">'
800
+ + '<button type="button" role="menuitem" data-folder-rename="'+attr(folder.id)+'">Rename folder</button>'
801
+ + '<button type="button" role="menuitem" data-folder-copy="'+attr(canonPath)+'">Copy path</button>'
802
+ + '</div></div>';
803
+ return '<div class="rk-raws-folder-wrap">'
804
+ + '<button type="button" class="rk-raws-folder rk-card-hover" data-folder-id="'+attr(folder.id)+'">'
633
805
  + mosaic
634
806
  + '<span class="rk-raws-folder-meta">'
635
807
  + '<span class="rk-raws-folder-ic">'+${JSON.stringify(ICON_FOLDER)}+'</span>'
636
808
  + '<span class="rk-raws-folder-txt"><span class="rk-raws-folder-name" title="'+attr(folder.name)+'">'+esc(folder.name)+'</span>'
637
- + '<span class="rk-raws-folder-count">'+n+' clip'+(n===1?'':'s')+'</span></span>'
638
- + '</span></button>';
809
+ + '<span class="rk-raws-folder-count">'+countTxt+'</span></span>'
810
+ + '</span></button>'
811
+ + menu
812
+ + '</div>';
639
813
  }
640
814
 
641
815
  function card(clip,i){
@@ -647,8 +821,12 @@ const CLIPS_SCRIPT = `
647
821
  var tags=topTags(clip.tags).map(function(t){ return '<span class="rk-clips-tag">'+esc(t)+'</span>'; }).join('');
648
822
  var desc=clip.description || (clip.tags&&clip.tags.transcript) || '(untagged clip)';
649
823
  var dl='/raws/'+encodeURIComponent(clip.clip_id)+'/download';
650
- return '<article class="rk-clips-card rk-card-hover">'
824
+ // The full-length original a hunt was mined from gets a distinct badge.
825
+ var fullBadge=clip.is_source_original?'<span class="rk-clips-fullbadge">Full video</span>':'';
826
+ return '<article class="rk-clips-card rk-card-hover'+(clip.is_source_original?' is-source-original':'')+'">'
651
827
  + '<button type="button" class="rk-clips-thumb" data-rk-clip-view="'+i+'" aria-label="Open raw">'+media
828
+ + '<video class="rk-clips-thumb-vid" muted loop playsinline preload="none"'+(clip.thumbnail_url?' poster="'+attr(clip.thumbnail_url)+'"':'')+'></video>'
829
+ + fullBadge
652
830
  + '<span class="rk-clips-dur">'+esc(range+(dur?' · '+dur:''))+'</span>'
653
831
  + '<span class="rk-clips-play" aria-hidden="true">'+${JSON.stringify(PLAY)}+'</span>'
654
832
  + '</button>'
@@ -660,35 +838,29 @@ const CLIPS_SCRIPT = `
660
838
  + '</div></article>';
661
839
  }
662
840
 
663
- /* ── mouseover preview popover ────────────────────────────────────────────── */
841
+ /* ── mouseover: play the clip in place inside its own card ────────────────── */
664
842
  var hoverTimer=null;
843
+ var playingThumb=null; // the thumb whose inline <video> is currently active
844
+ function stopInline(thumb){
845
+ if(!thumb)return;
846
+ var vid=thumb.querySelector('.rk-clips-thumb-vid');
847
+ if(vid){ try{ vid.pause(); }catch(e){} vid.removeAttribute('src'); vid.load&&vid.load(); }
848
+ thumb.classList.remove('is-playing');
849
+ }
665
850
  function hideHover(){
666
851
  if(hoverTimer){ clearTimeout(hoverTimer); hoverTimer=null; }
667
- if(!hoverEl)return;
668
- hoverEl.hidden=true; hoverEl.classList.remove('is-visible');
669
- if(hoverVid){ try{ hoverVid.pause(); }catch(e){} hoverVid.removeAttribute('src'); hoverVid.load&&hoverVid.load(); }
670
- }
671
- function positionHover(rect){
672
- if(!hoverEl)return;
673
- var gap=12, bw=hoverEl.offsetWidth||250, bh=hoverEl.offsetHeight||330;
674
- var x=rect.right+gap;
675
- if(x+bw>window.innerWidth-8) x=rect.left-gap-bw;
676
- if(x<8) x=Math.max(8,Math.min(rect.left,window.innerWidth-bw-8));
677
- var y=rect.top+rect.height/2-bh/2;
678
- y=Math.max(8,Math.min(y,window.innerHeight-bh-8));
679
- hoverEl.style.left=Math.round(x)+'px';
680
- hoverEl.style.top=Math.round(y)+'px';
852
+ if(playingThumb){ stopInline(playingThumb); playingThumb=null; }
681
853
  }
682
- function showHover(clip,rect){
683
- if(!hoverEl||!clip||(!clip.view_url&&!clip.thumbnail_url))return;
684
- if(hoverCap){ hoverCap.textContent=clip.description||(clip.tags&&clip.tags.transcript)||clip.source_filename||'Raw clip'; }
685
- if(hoverVid){
686
- hoverVid.poster=clip.thumbnail_url||'';
687
- if(clip.view_url){ hoverVid.setAttribute('src',clip.view_url); hoverVid.load&&hoverVid.load(); var p=hoverVid.play&&hoverVid.play(); if(p&&p.catch)p.catch(function(){}); }
688
- else { hoverVid.removeAttribute('src'); }
689
- }
690
- hoverEl.hidden=false; hoverEl.classList.add('is-visible');
691
- positionHover(rect);
854
+ function playInline(thumb,clip){
855
+ if(!thumb||!clip||!clip.view_url)return;
856
+ if(playingThumb&&playingThumb!==thumb){ stopInline(playingThumb); }
857
+ playingThumb=thumb;
858
+ var vid=thumb.querySelector('.rk-clips-thumb-vid');
859
+ if(!vid)return;
860
+ if(clip.thumbnail_url) vid.poster=clip.thumbnail_url;
861
+ vid.setAttribute('src',clip.view_url); vid.load&&vid.load();
862
+ thumb.classList.add('is-playing');
863
+ var p=vid.play&&vid.play(); if(p&&p.catch)p.catch(function(){});
692
864
  }
693
865
  function bindHover(){
694
866
  if(!grid||!canHover)return;
@@ -696,17 +868,17 @@ const CLIPS_SCRIPT = `
696
868
  var idx=Number(thumb.getAttribute('data-rk-clip-view'));
697
869
  thumb.addEventListener('mouseenter',function(){
698
870
  var clip=VIEW[idx]; if(!clip)return;
699
- var rect=thumb.getBoundingClientRect();
700
871
  if(hoverTimer)clearTimeout(hoverTimer);
701
- hoverTimer=setTimeout(function(){ showHover(clip,rect); },90);
872
+ hoverTimer=setTimeout(function(){ playInline(thumb,clip); },90);
873
+ });
874
+ thumb.addEventListener('mouseleave',function(){
875
+ if(hoverTimer){ clearTimeout(hoverTimer); hoverTimer=null; }
876
+ stopInline(thumb);
877
+ if(playingThumb===thumb) playingThumb=null;
702
878
  });
703
- thumb.addEventListener('mouseleave',hideHover);
704
879
  });
705
880
  }
706
- if(hoverEl){
707
- window.addEventListener('scroll',hideHover,true);
708
- window.addEventListener('resize',hideHover);
709
- }
881
+ window.addEventListener('scroll',hideHover,true);
710
882
 
711
883
  /* ── breadcrumb + view rendering ──────────────────────────────────────────── */
712
884
  function renderCrumbs(){
@@ -891,13 +1063,67 @@ const CLIPS_SCRIPT = `
891
1063
  window.addEventListener('resize',function(){ if(vOpen&&vActive>=0)vTrack.scrollTop=vActive*vTrack.clientHeight; });
892
1064
  }
893
1065
 
1066
+ /* ── folder rename / copy-path (kebab on each folder tile) ────────────────── */
1067
+ function closeFolderMenus(except){
1068
+ if(!grid)return;
1069
+ Array.prototype.slice.call(grid.querySelectorAll('[data-folder-menu]')).forEach(function(m){
1070
+ if(m===except)return;
1071
+ var pop=m.querySelector('[data-folder-pop]'); if(pop)pop.hidden=true;
1072
+ var kb=m.querySelector('[data-folder-kebab]'); if(kb)kb.setAttribute('aria-expanded','false');
1073
+ });
1074
+ }
1075
+ function copyPath(path){
1076
+ try{
1077
+ if(navigator.clipboard&&navigator.clipboard.writeText){ navigator.clipboard.writeText(path); }
1078
+ else { var ta=document.createElement('textarea'); ta.value=path; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); }
1079
+ if(statusEl)statusEl.textContent='Copied '+path;
1080
+ }catch(e){ if(statusEl)statusEl.textContent=path; }
1081
+ }
1082
+ // Rename a raws folder. Real folders (folder_path set) go through the directory
1083
+ // rename endpoint (re-points every clip + nested subfolders server-side). Legacy
1084
+ // source-groups (no folder_path) have no stored folder to rename, so we
1085
+ // MATERIALIZE one by moving each clip into /raws/<slug> via PATCH /raws/:id.
1086
+ function renameFolder(folderId){
1087
+ var f=folderById(folderId); if(!f)return;
1088
+ var current=f.folderPath?(f.folderPath.split('/').pop()):f.name;
1089
+ var next=(window.prompt('Rename folder',current||'')||'').trim();
1090
+ if(!next||next===current)return;
1091
+ if(statusEl)statusEl.textContent='Renaming…';
1092
+ if(f.folderPath){
1093
+ fetch('/api/v1/user/me/directory/rename',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json',Accept:'application/json'},body:JSON.stringify({path:'/raws/'+f.folderPath,new_name:next})})
1094
+ .then(function(r){ return r.ok?r.json():r.json().catch(function(){return{};}).then(function(d){ throw new Error((d&&d.error)||'Rename failed'); }); })
1095
+ .then(function(){ loadFeed(); })
1096
+ .catch(function(err){ if(statusEl)statusEl.textContent=(err&&err.message)||'Rename failed'; });
1097
+ } else {
1098
+ var target=slugFolder(next); if(!target){ if(statusEl)statusEl.textContent='Pick a folder name with letters or numbers.'; return; }
1099
+ Promise.all(f.clips.map(function(c){
1100
+ return fetch('/raws/'+encodeURIComponent(c.clip_id),{method:'PATCH',credentials:'same-origin',headers:{'Content-Type':'application/json',Accept:'application/json'},body:JSON.stringify({folder_path:target})}).then(function(r){ return r.ok; });
1101
+ })).then(function(oks){
1102
+ if(oks.some(function(ok){ return !ok; })){ if(statusEl)statusEl.textContent='Some clips could not be moved — try again.'; }
1103
+ loadFeed();
1104
+ }).catch(function(){ if(statusEl)statusEl.textContent='Rename failed'; loadFeed(); });
1105
+ }
1106
+ }
1107
+
894
1108
  // Folder navigation (grid tile) + breadcrumb "All raws" back link.
895
1109
  if(grid){
896
1110
  grid.addEventListener('click',function(e){
897
- var f=e.target&&e.target.closest?e.target.closest('[data-folder-id]'):null;
898
- if(!f)return; e.preventDefault(); hideHover(); if(query)query.value=''; paintFolder(f.getAttribute('data-folder-id'));
1111
+ var tgt=e.target;
1112
+ // kebab toggle
1113
+ var kb=tgt&&tgt.closest?tgt.closest('[data-folder-kebab]'):null;
1114
+ if(kb){ e.preventDefault(); e.stopPropagation(); var menu=kb.closest('[data-folder-menu]'); var pop=menu&&menu.querySelector('[data-folder-pop]'); var willOpen=pop&&pop.hidden; closeFolderMenus(menu); if(pop){ pop.hidden=!willOpen; kb.setAttribute('aria-expanded',willOpen?'true':'false'); } return; }
1115
+ var rn=tgt&&tgt.closest?tgt.closest('[data-folder-rename]'):null;
1116
+ if(rn){ e.preventDefault(); closeFolderMenus(null); renameFolder(rn.getAttribute('data-folder-rename')); return; }
1117
+ var cp=tgt&&tgt.closest?tgt.closest('[data-folder-copy]'):null;
1118
+ if(cp){ e.preventDefault(); closeFolderMenus(null); copyPath(cp.getAttribute('data-folder-copy')); return; }
1119
+ var f=tgt&&tgt.closest?tgt.closest('[data-folder-id]'):null;
1120
+ if(!f)return; e.preventDefault(); closeFolderMenus(null); hideHover(); if(query)query.value=''; paintFolder(f.getAttribute('data-folder-id'));
899
1121
  });
900
1122
  }
1123
+ document.addEventListener('click',function(e){
1124
+ var t=e.target;
1125
+ if(!(t&&t.closest&&t.closest('[data-folder-menu]')))closeFolderMenus(null);
1126
+ });
901
1127
  if(crumbsEl){
902
1128
  crumbsEl.addEventListener('click',function(e){
903
1129
  var r=e.target&&e.target.closest?e.target.closest('[data-crumb-root]'):null;
@@ -957,7 +1183,7 @@ const CLIPS_SCRIPT = `
957
1183
  var scanSubmit=root.getElementById('rkClipsScanSubmit');
958
1184
  var scanClip=root.getElementById('rkScanClip');
959
1185
  var scanAdvanced=root.getElementById('rkClipsAdvanced');
960
- var IMPORT_NOTE="We'll pull the whole video into your raws library.";
1186
+ var IMPORT_NOTE="We'll open the Clipper so you can preview the video and save an exact subrange (or the whole thing) to your raws.";
961
1187
  var CLIP_NOTE='AI tagging runs on your own key (BYOK) — vidfarm only bills the clip-scan compute.';
962
1188
  var SCAN_OPTS=null; // GET /raws/scan-options: which providers/keys can run a hunt
963
1189
  function setNote(msg,tone){ if(!scanNote)return; scanNote.textContent=msg; if(tone)scanNote.setAttribute('data-tone',tone); else scanNote.removeAttribute('data-tone'); }
@@ -989,7 +1215,7 @@ const CLIPS_SCRIPT = `
989
1215
  function syncClipMode(){
990
1216
  var on=!!(scanClip&&scanClip.checked);
991
1217
  if(scanAdvanced)scanAdvanced.hidden=!on;
992
- if(scanSubmit)scanSubmit.innerHTML=(on?'Start scan':'Import video')+' <span class="rk-arrow">→</span>';
1218
+ if(scanSubmit)scanSubmit.innerHTML=(on?'Start scan':'Open clipper')+' <span class="rk-arrow">→</span>';
993
1219
  if(on&&!clipReady()){
994
1220
  setNote('No AI provider available — add a Gemini, OpenAI, or OpenRouter key in Settings to clip raws (or uncheck to import the whole video).','error');
995
1221
  } else {
@@ -1014,7 +1240,16 @@ const CLIPS_SCRIPT = `
1014
1240
  var prompt=clip?(scanPrompt&&scanPrompt.value||'').trim():'';
1015
1241
  var provider=clip?(scanProvider&&scanProvider.value||'').trim():'';
1016
1242
  if(!url){ setNote('Enter a video URL.','error'); return; }
1017
- setNote(clip?'Starting scan…':'Importing video…');
1243
+ // Whole-video (non-AI) path now opens the Clipper so the user can pick an
1244
+ // exact subrange (or save the whole thing) with a live video preview.
1245
+ if(!clip){
1246
+ var clp=new URL('/tools/clipper', window.location.origin);
1247
+ clp.searchParams.set('source_url', url);
1248
+ if(tracer) clp.searchParams.set('tracer', tracer);
1249
+ window.location.href=clp.pathname+clp.search;
1250
+ return;
1251
+ }
1252
+ setNote('Starting scan…');
1018
1253
  if(scanSubmit){ scanSubmit.disabled=true; scanSubmit.classList.add('is-loading'); }
1019
1254
 
1020
1255
  var payload={source_url:url};
@@ -1080,7 +1315,7 @@ export function renderReskinLibrary(tab = "approved", input = EMPTY_LIBRARY_INPU
1080
1315
  const panel = isLogs
1081
1316
  ? `<div class="rk-library-topbar">${tabs}</div>\n ${jobRunsPanel(logsBoot)}`
1082
1317
  : isFiles
1083
- ? filesPanel(tabs)
1318
+ ? filesPanel(tabs, Boolean(input.cloudSpace), input.initialFilesPath)
1084
1319
  : isRaws
1085
1320
  ? clipsPanel(tabs)
1086
1321
  : approvedPanel(input, tabs);
@@ -1267,6 +1502,17 @@ video.rk-library-viewer-media{cursor:pointer}
1267
1502
  .rk-raws-explorer.is-folders{grid-template-columns:repeat(auto-fill,minmax(228px,1fr))}
1268
1503
 
1269
1504
  /* folder tile — 2×2 thumbnail mosaic + name + count */
1505
+ .rk-raws-folder-wrap{position:relative;display:flex}
1506
+ .rk-raws-folder-wrap>.rk-raws-folder{flex:1;min-width:0}
1507
+ .rk-raws-folder-menu{position:absolute;top:10px;right:10px;z-index:4}
1508
+ .rk-raws-folder-kebab{width:30px;height:30px;display:inline-flex;align-items:center;justify-content:center;border:0;border-radius:var(--rk-r-full);background:rgba(255,255,255,.92);color:var(--rk-ink);cursor:pointer;opacity:0;transform:scale(.85);box-shadow:var(--rk-shadow-sm);transition:opacity var(--rk-dur) var(--rk-ease),transform var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease)}
1509
+ .rk-raws-folder-wrap:hover .rk-raws-folder-kebab,.rk-raws-folder-menu [aria-expanded="true"]{opacity:1;transform:scale(1)}
1510
+ .rk-raws-folder-kebab:hover{background:#fff}
1511
+ @media(hover:none){.rk-raws-folder-kebab{opacity:1;transform:scale(1)}}
1512
+ .rk-raws-folder-pop{position:absolute;top:36px;right:0;min-width:168px;background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-lg);box-shadow:var(--rk-shadow-lg);padding:6px;display:flex;flex-direction:column;gap:2px}
1513
+ .rk-raws-folder-pop[hidden]{display:none}
1514
+ .rk-raws-folder-pop button{appearance:none;border:0;background:none;font:inherit;text-align:left;padding:8px 10px;border-radius:var(--rk-r-md);color:var(--rk-ink);font-size:13px;font-weight:600;cursor:pointer}
1515
+ .rk-raws-folder-pop button:hover{background:var(--rk-n-100)}
1270
1516
  .rk-raws-folder{display:flex;flex-direction:column;overflow:hidden;background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-2xl);box-shadow:var(--rk-shadow-card);cursor:pointer;text-align:left;font:inherit;color:inherit;padding:0;transition:transform var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
1271
1517
  .rk-raws-folder:hover{transform:translateY(-3px);box-shadow:var(--rk-shadow-md);border-color:var(--rk-gold-600)}
1272
1518
  .rk-raws-folder-mosaic{position:relative;display:grid;grid-template-columns:1fr 1fr;grid-auto-rows:1fr;gap:2px;aspect-ratio:4/3;background:var(--rk-n-100);overflow:hidden}
@@ -1280,21 +1526,17 @@ video.rk-library-viewer-media{cursor:pointer}
1280
1526
  .rk-raws-folder-name{font-family:var(--rk-font-display);font-size:14px;font-weight:700;color:var(--rk-ink);letter-spacing:-.01em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1281
1527
  .rk-raws-folder-count{font-size:12px;font-weight:600;color:var(--rk-text-muted)}
1282
1528
 
1283
- /* mouseover preview popover (floating enlarged media) */
1284
- .rk-raws-hover{position:fixed;z-index:90;width:250px;pointer-events:none;border-radius:var(--rk-r-2xl);overflow:hidden;background:#0d0d0d;border:1px solid rgba(255,255,255,.12);box-shadow:var(--rk-shadow-xl);opacity:0;transform:scale(.96);transition:opacity .12s var(--rk-ease),transform .12s var(--rk-ease)}
1285
- .rk-raws-hover.is-visible{opacity:1;transform:scale(1)}
1286
- .rk-raws-hover[hidden]{display:none}
1287
- .rk-raws-hover-vid{display:block;width:100%;aspect-ratio:3/4;object-fit:cover;background:#0d0d0d}
1288
- .rk-raws-hover-cap{padding:9px 12px 11px;font-size:12px;font-weight:600;line-height:1.35;color:rgba(255,255,255,.92);background:linear-gradient(180deg,#171717,#0d0d0d);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
1289
- @media(hover:none){.rk-raws-hover{display:none!important}}
1290
1529
 
1291
1530
  .rk-clips-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:20px}
1292
1531
  .rk-clips-card{display:flex;flex-direction:column;overflow:hidden;background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-2xl);box-shadow:var(--rk-shadow-card)}
1293
1532
  .rk-clips-thumb{position:relative;aspect-ratio:3/4;overflow:hidden;display:grid;place-items:center;background:linear-gradient(150deg,#2c2c2c,#171717)}
1294
1533
  .rk-clips-thumb-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
1295
- .rk-clips-thumb-vid{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:none}
1534
+ .rk-clips-thumb-vid{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:none;z-index:1}
1535
+ .rk-clips-thumb.is-playing .rk-clips-thumb-vid{display:block}
1536
+ .rk-clips-thumb.is-playing .rk-clips-play{opacity:0!important}
1296
1537
  .rk-clips-noimg{color:rgba(255,255,255,.68);font-size:12px;font-weight:600;letter-spacing:.01em}
1297
1538
  .rk-clips-dur{position:absolute;left:10px;bottom:10px;padding:3px 9px;border-radius:var(--rk-r-full);background:rgba(255,255,255,.92);color:var(--rk-ink);font-size:11px;font-weight:700;font-variant-numeric:tabular-nums;box-shadow:var(--rk-shadow-xs);z-index:2}
1539
+ .rk-clips-fullbadge{position:absolute;left:10px;top:10px;padding:3px 9px;border-radius:var(--rk-r-full);background:var(--rk-ink);color:#fff;font-size:11px;font-weight:700;letter-spacing:.01em;box-shadow:var(--rk-shadow-xs);z-index:3}
1298
1540
  .rk-clips-play{position:absolute;right:10px;top:10px;width:34px;height:34px;display:grid;place-items:center;padding:0 0 0 2px;border:0;border-radius:var(--rk-r-full);background:rgba(255,255,255,.92);color:var(--rk-ink);font-size:13px;cursor:pointer;opacity:0;transform:scale(.85);box-shadow:var(--rk-shadow-sm);z-index:2;transition:opacity var(--rk-dur) var(--rk-ease),transform var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease)}
1299
1541
  .rk-clips-card:hover .rk-clips-play{opacity:1;transform:scale(1)}
1300
1542
  @media(hover:none){.rk-clips-play{opacity:1;transform:scale(1)}}