@officexapp/vidfarm-devcli 0.21.26 → 0.21.27

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.
@@ -576,6 +576,7 @@
576
576
  background:#fff;border:1px solid var(--rk-border);border-radius:var(--rk-r-full);padding:4px 11px;text-decoration:none;line-height:1.4;
577
577
  transition:background var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
578
578
  .rk-aichat-preview-act:hover{background:var(--rk-gold-tint);border-color:var(--rk-gold-600)}
579
+ .rk-aichat-preview-act[hidden]{display:none}
579
580
 
580
581
  /* ── markdown pipe tables (assistant messages + .md previews) ── */
581
582
  .rk-md-tablewrap{max-width:100%;overflow-x:auto;margin:8px 0;border:1px solid var(--rk-border);border-radius:var(--rk-r-lg)}
@@ -607,6 +608,14 @@
607
608
  .rk-aichat-file-thumb--markdown,.rk-aichat-file-thumb--text{background:var(--rk-n-50)}.rk-aichat-file-thumb--audio{background:var(--rk-gold-tint2)}
608
609
  .rk-aichat-file-play{position:absolute;inset:0;display:grid;place-items:center;font-size:22px;color:#fff;text-shadow:0 1px 6px rgba(0,0,0,.55);pointer-events:none}
609
610
  .rk-aichat-file-name{font-size:11px;font-weight:600;color:var(--rk-n-600);line-height:1.3;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:112px}
611
+ /* "Edit in the tool" pencil — only on image/video cards; opens /tools/image or
612
+ /tools/video in a new tab with this file preloaded as the working file. */
613
+ .rk-aichat-file-open{position:absolute;top:5px;right:5px;width:24px;height:24px;border-radius:var(--rk-r-md);
614
+ display:grid;place-items:center;background:rgba(255,255,255,.94);border:1px solid var(--rk-border);color:var(--rk-ink);
615
+ opacity:0;transition:opacity var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease);z-index:2}
616
+ .rk-aichat-file-open svg{width:13px;height:13px}
617
+ .rk-aichat-file:hover .rk-aichat-file-open,.rk-aichat-file-open:focus-visible{opacity:1}
618
+ .rk-aichat-file-open:hover{background:var(--rk-gold-tint)}
610
619
 
611
620
  /* create_video pipeline: live status row + clickable result links */
612
621
  .rk-aichat-flow{display:flex;align-items:center;gap:9px;font-size:12.5px;color:var(--rk-text-muted);padding:2px 2px}
@@ -1095,6 +1104,12 @@ html,body{margin:0;background:#050604;color:#fffbe6}
1095
1104
  .rk-aichat--editor .rk-aichat-toolnote.is-ok::before{background:#33d17a}
1096
1105
  .rk-aichat--editor .rk-aichat-toolnote.is-err{color:#ff9b8f}
1097
1106
  .rk-aichat--editor .rk-aichat-toolnote.is-err::before{background:#ff6a5a}
1107
+ /* "Edit in the tool" pencil + preview actions are hardcoded light in the shared
1108
+ sheet; dark them so they read on the editor dock's dark chrome. */
1109
+ .rk-aichat--editor .rk-aichat-file-open,
1110
+ .rk-aichat--editor .rk-aichat-preview-act{background:#141810;color:var(--rk-ink)}
1111
+ .rk-aichat--editor .rk-aichat-file-open:hover,
1112
+ .rk-aichat--editor .rk-aichat-preview-act:hover{background:#1b2013}
1098
1113
 
1099
1114
  /* Hide the SPA's own embedded chat (replaced by the farmville dock). */
1100
1115
  .chat-dock,.chat-dock-fab{display:none !important}
@@ -1822,6 +1837,34 @@ html,body{margin:0;background:#050604;color:#fffbe6}
1822
1837
  }
1823
1838
  function fileIcon(kind) { return kind === 'pdf' ? '📕' : kind === 'json' ? '{ }' : kind === 'markdown' ? '📝' : kind === 'audio' ? '🎵' : kind === 'text' ? '📄' : '📄'; }
1824
1839
 
1840
+ // ── "edit this media" hand-off: dock chat → the matching creative tool ──
1841
+ // Images open /tools/image, videos /tools/video, each with the file preloaded as
1842
+ // that tool's working file (its chat then edits THAT media). Deliberately a fresh
1843
+ // tool session — no ?tracer — so the tool names its own chat from the first prompt.
1844
+ var PENCIL_SVG = '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13.6 3.6a1.9 1.9 0 0 1 2.8 2.8L7.7 15.1l-3.6.8.8-3.6z"/></svg>';
1845
+ function toolEditUrl(file) {
1846
+ if (!file || !file.viewUrl) return '';
1847
+ var k = fileKind(file.name, file.contentType);
1848
+ var name = file.name || '';
1849
+ if (k === 'image') return '/tools/image?source_image_url=' + encodeURIComponent(file.viewUrl) + (name ? '&source_image_name=' + encodeURIComponent(name) : '');
1850
+ if (k === 'video') return '/tools/video?source_video_url=' + encodeURIComponent(file.viewUrl) + (name ? '&source_video_name=' + encodeURIComponent(name) : '');
1851
+ return '';
1852
+ }
1853
+ function toolEditLink(file, className) {
1854
+ var href = toolEditUrl(file);
1855
+ if (!href) return null;
1856
+ var a = document.createElement('a');
1857
+ a.className = className;
1858
+ a.href = href;
1859
+ a.target = '_blank';
1860
+ a.rel = 'noopener noreferrer';
1861
+ a.title = 'Edit in the ' + (fileKind(file.name, file.contentType) === 'video' ? 'video' : 'image') + ' tool';
1862
+ a.setAttribute('aria-label', a.title);
1863
+ // The card itself opens the lightbox; the pencil must not also trigger it.
1864
+ a.addEventListener('click', function (e) { e.stopPropagation(); });
1865
+ return a;
1866
+ }
1867
+
1825
1868
  // Compact markdown → HTML for .md previews (headings, lists, quotes, rules,
1826
1869
  // fenced + inline code, bold/italic/links). Escapes first, then injects tags
1827
1870
  // (same order as linkify). Backticks are built via char codes to avoid
@@ -1882,7 +1925,9 @@ html,body{margin:0;background:#050604;color:#fffbe6}
1882
1925
  var dlBtn = document.createElement('a'); dlBtn.className = 'rk-aichat-preview-act'; dlBtn.target = '_blank'; dlBtn.rel = 'noopener noreferrer'; dlBtn.textContent = 'Download';
1883
1926
  var linkBtn = document.createElement('button'); linkBtn.type = 'button'; linkBtn.className = 'rk-aichat-preview-act'; linkBtn.textContent = 'Copy link';
1884
1927
  var xBtn = document.createElement('button'); xBtn.type = 'button'; xBtn.className = 'rk-aichat-modal-x'; xBtn.setAttribute('aria-label', 'Close'); xBtn.innerHTML = '&times;';
1885
- head.appendChild(nameEl); head.appendChild(copyBtn); head.appendChild(dlBtn); head.appendChild(linkBtn); head.appendChild(xBtn);
1928
+ // Same hand-off as the card pencil, spelled out as a labelled action here.
1929
+ var editBtn = document.createElement('a'); editBtn.className = 'rk-aichat-preview-act'; editBtn.target = '_blank'; editBtn.rel = 'noopener noreferrer'; editBtn.textContent = 'Edit';
1930
+ head.appendChild(nameEl); head.appendChild(editBtn); head.appendChild(copyBtn); head.appendChild(dlBtn); head.appendChild(linkBtn); head.appendChild(xBtn);
1886
1931
  function flashAct(btn, label) { var o = btn.getAttribute('data-label') || btn.textContent; btn.setAttribute('data-label', o); btn.textContent = label; setTimeout(function () { btn.textContent = o; }, 1400); }
1887
1932
  // Copy = file text (json/md/txt) or its URL for binaries. Copy link = the
1888
1933
  // durable public S3 URL. Download opens in a new tab when the browser can't
@@ -1931,6 +1976,9 @@ html,body{margin:0;background:#050604;color:#fffbe6}
1931
1976
  var it = items[idx];
1932
1977
  nameEl.textContent = it.name || 'file'; nameEl.title = it.name || '';
1933
1978
  dlBtn.href = it.viewUrl; dlBtn.setAttribute('download', it.name || '');
1979
+ var editHref = toolEditUrl(it);
1980
+ editBtn.hidden = !editHref;
1981
+ if (editHref) { editBtn.href = editHref; editBtn.title = 'Edit in the ' + (fileKind(it.name, it.contentType) === 'video' ? 'video' : 'image') + ' tool'; }
1934
1982
  linkBtn.style.display = it.publicUrl ? '' : 'none';
1935
1983
  body.innerHTML = '';
1936
1984
  var kind = fileKind(it.name, it.contentType);
@@ -1985,7 +2033,15 @@ html,body{margin:0;background:#050604;color:#fffbe6}
1985
2033
  var files = [], seen = {};
1986
2034
  var arts = (job && job.artifacts) || [];
1987
2035
  for (var i = 0; i < arts.length; i++) { var f = rkFileFromArtifact(arts[i]); if (f && !seen[f.viewUrl]) { seen[f.viewUrl] = true; files.push(f); } }
1988
- var mu = rkResultMediaUrl(job); if (mu && !seen[mu]) { seen[mu] = true; files.push({ name: (job.tracer || 'output'), contentType: '', viewUrl: mu, publicUrl: mu }); }
2036
+ // Name the direct result URL after the FILE, not the tracer fileKind() reads the
2037
+ // extension, and a tracer-named entry ("my-poster") classifies as 'other', losing
2038
+ // the image/video thumbnail and the edit-in-tool hand-off.
2039
+ var mu = rkResultMediaUrl(job);
2040
+ if (mu && !seen[mu]) {
2041
+ seen[mu] = true;
2042
+ var seg = String(mu).split('?')[0].split('/').pop() || '';
2043
+ files.push({ name: (/.[a-z0-9]{2,5}$/i.test(seg) ? seg : (job.tracer || 'output')), contentType: '', viewUrl: mu, publicUrl: mu });
2044
+ }
1989
2045
  var res = job && job.result;
1990
2046
  if (res && typeof res === 'object' && !Array.isArray(res) && Object.keys(res).length) {
1991
2047
  try { var blob = new Blob([JSON.stringify(res, null, 2)], { type: 'application/json' }); files.push({ name: (job.tracer || 'result') + '.json', contentType: 'application/json', viewUrl: URL.createObjectURL(blob), publicUrl: null }); } catch (e) {}
@@ -2038,14 +2094,21 @@ html,body{margin:0;background:#050604;color:#fffbe6}
2038
2094
  var grid = document.createElement('div'); grid.className = 'rk-aichat-files-grid';
2039
2095
  list.forEach(function (f, ix) {
2040
2096
  var k = fileKind(f.name, f.contentType);
2041
- var card = document.createElement('button'); card.type = 'button'; card.className = 'rk-aichat-file';
2097
+ // A div, not a button: media cards carry a nested "edit in tool" link, and
2098
+ // an anchor inside a button is invalid (and unclickable in some browsers).
2099
+ var card = document.createElement('div'); card.className = 'rk-aichat-file'; card.tabIndex = 0; card.setAttribute('role', 'button');
2042
2100
  var thumb = document.createElement('div'); thumb.className = 'rk-aichat-file-thumb rk-aichat-file-thumb--' + k;
2043
2101
  if (k === 'image') { var im = document.createElement('img'); im.src = f.viewUrl; im.alt = ''; im.loading = 'lazy'; thumb.appendChild(im); }
2044
2102
  else if (k === 'video') { var v = document.createElement('video'); v.src = f.viewUrl; v.muted = true; v.preload = 'metadata'; v.playsInline = true; thumb.appendChild(v); var pl = document.createElement('span'); pl.className = 'rk-aichat-file-play'; pl.textContent = '▶'; thumb.appendChild(pl); }
2045
2103
  else { var ic = document.createElement('span'); ic.className = 'rk-aichat-file-ic'; ic.textContent = fileIcon(k); thumb.appendChild(ic); }
2104
+ var edit = toolEditLink(f, 'rk-aichat-file-open');
2105
+ if (edit) { edit.innerHTML = PENCIL_SVG; thumb.appendChild(edit); }
2046
2106
  var nm = document.createElement('div'); nm.className = 'rk-aichat-file-name'; nm.textContent = f.name || 'file'; nm.title = f.name || '';
2047
2107
  card.appendChild(thumb); card.appendChild(nm);
2048
2108
  card.addEventListener('click', function () { openFilePreview(list, ix); });
2109
+ card.addEventListener('keydown', function (e) {
2110
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openFilePreview(list, ix); }
2111
+ });
2049
2112
  grid.appendChild(card);
2050
2113
  });
2051
2114
  wrap.appendChild(grid); turn.appendChild(wrap); logBottom();
@@ -591,6 +591,7 @@
591
591
  background:#fff;border:1px solid var(--rk-border);border-radius:var(--rk-r-full);padding:4px 11px;text-decoration:none;line-height:1.4;
592
592
  transition:background var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
593
593
  .rk-aichat-preview-act:hover{background:var(--rk-gold-tint);border-color:var(--rk-gold-600)}
594
+ .rk-aichat-preview-act[hidden]{display:none}
594
595
 
595
596
  /* ── markdown pipe tables (assistant messages + .md previews) ── */
596
597
  .rk-md-tablewrap{max-width:100%;overflow-x:auto;margin:8px 0;border:1px solid var(--rk-border);border-radius:var(--rk-r-lg)}
@@ -622,6 +623,14 @@
622
623
  .rk-aichat-file-thumb--markdown,.rk-aichat-file-thumb--text{background:var(--rk-n-50)}.rk-aichat-file-thumb--audio{background:var(--rk-gold-tint2)}
623
624
  .rk-aichat-file-play{position:absolute;inset:0;display:grid;place-items:center;font-size:22px;color:#fff;text-shadow:0 1px 6px rgba(0,0,0,.55);pointer-events:none}
624
625
  .rk-aichat-file-name{font-size:11px;font-weight:600;color:var(--rk-n-600);line-height:1.3;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:112px}
626
+ /* "Edit in the tool" pencil — only on image/video cards; opens /tools/image or
627
+ /tools/video in a new tab with this file preloaded as the working file. */
628
+ .rk-aichat-file-open{position:absolute;top:5px;right:5px;width:24px;height:24px;border-radius:var(--rk-r-md);
629
+ display:grid;place-items:center;background:rgba(255,255,255,.94);border:1px solid var(--rk-border);color:var(--rk-ink);
630
+ opacity:0;transition:opacity var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease);z-index:2}
631
+ .rk-aichat-file-open svg{width:13px;height:13px}
632
+ .rk-aichat-file:hover .rk-aichat-file-open,.rk-aichat-file-open:focus-visible{opacity:1}
633
+ .rk-aichat-file-open:hover{background:var(--rk-gold-tint)}
625
634
 
626
635
  /* create_video pipeline: live status row + clickable result links */
627
636
  .rk-aichat-flow{display:flex;align-items:center;gap:9px;font-size:12.5px;color:var(--rk-text-muted);padding:2px 2px}
@@ -2148,6 +2157,34 @@ button.rk-clips-thumb{appearance:none;-webkit-appearance:none;border:0;margin:0;
2148
2157
  }
2149
2158
  function fileIcon(kind) { return kind === 'pdf' ? '📕' : kind === 'json' ? '{ }' : kind === 'markdown' ? '📝' : kind === 'audio' ? '🎵' : kind === 'text' ? '📄' : '📄'; }
2150
2159
 
2160
+ // ── "edit this media" hand-off: dock chat → the matching creative tool ──
2161
+ // Images open /tools/image, videos /tools/video, each with the file preloaded as
2162
+ // that tool's working file (its chat then edits THAT media). Deliberately a fresh
2163
+ // tool session — no ?tracer — so the tool names its own chat from the first prompt.
2164
+ var PENCIL_SVG = '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13.6 3.6a1.9 1.9 0 0 1 2.8 2.8L7.7 15.1l-3.6.8.8-3.6z"/></svg>';
2165
+ function toolEditUrl(file) {
2166
+ if (!file || !file.viewUrl) return '';
2167
+ var k = fileKind(file.name, file.contentType);
2168
+ var name = file.name || '';
2169
+ if (k === 'image') return '/tools/image?source_image_url=' + encodeURIComponent(file.viewUrl) + (name ? '&source_image_name=' + encodeURIComponent(name) : '');
2170
+ if (k === 'video') return '/tools/video?source_video_url=' + encodeURIComponent(file.viewUrl) + (name ? '&source_video_name=' + encodeURIComponent(name) : '');
2171
+ return '';
2172
+ }
2173
+ function toolEditLink(file, className) {
2174
+ var href = toolEditUrl(file);
2175
+ if (!href) return null;
2176
+ var a = document.createElement('a');
2177
+ a.className = className;
2178
+ a.href = href;
2179
+ a.target = '_blank';
2180
+ a.rel = 'noopener noreferrer';
2181
+ a.title = 'Edit in the ' + (fileKind(file.name, file.contentType) === 'video' ? 'video' : 'image') + ' tool';
2182
+ a.setAttribute('aria-label', a.title);
2183
+ // The card itself opens the lightbox; the pencil must not also trigger it.
2184
+ a.addEventListener('click', function (e) { e.stopPropagation(); });
2185
+ return a;
2186
+ }
2187
+
2151
2188
  // Compact markdown → HTML for .md previews (headings, lists, quotes, rules,
2152
2189
  // fenced + inline code, bold/italic/links). Escapes first, then injects tags
2153
2190
  // (same order as linkify). Backticks are built via char codes to avoid
@@ -2208,7 +2245,9 @@ button.rk-clips-thumb{appearance:none;-webkit-appearance:none;border:0;margin:0;
2208
2245
  var dlBtn = document.createElement('a'); dlBtn.className = 'rk-aichat-preview-act'; dlBtn.target = '_blank'; dlBtn.rel = 'noopener noreferrer'; dlBtn.textContent = 'Download';
2209
2246
  var linkBtn = document.createElement('button'); linkBtn.type = 'button'; linkBtn.className = 'rk-aichat-preview-act'; linkBtn.textContent = 'Copy link';
2210
2247
  var xBtn = document.createElement('button'); xBtn.type = 'button'; xBtn.className = 'rk-aichat-modal-x'; xBtn.setAttribute('aria-label', 'Close'); xBtn.innerHTML = '&times;';
2211
- head.appendChild(nameEl); head.appendChild(copyBtn); head.appendChild(dlBtn); head.appendChild(linkBtn); head.appendChild(xBtn);
2248
+ // Same hand-off as the card pencil, spelled out as a labelled action here.
2249
+ var editBtn = document.createElement('a'); editBtn.className = 'rk-aichat-preview-act'; editBtn.target = '_blank'; editBtn.rel = 'noopener noreferrer'; editBtn.textContent = 'Edit';
2250
+ head.appendChild(nameEl); head.appendChild(editBtn); head.appendChild(copyBtn); head.appendChild(dlBtn); head.appendChild(linkBtn); head.appendChild(xBtn);
2212
2251
  function flashAct(btn, label) { var o = btn.getAttribute('data-label') || btn.textContent; btn.setAttribute('data-label', o); btn.textContent = label; setTimeout(function () { btn.textContent = o; }, 1400); }
2213
2252
  // Copy = file text (json/md/txt) or its URL for binaries. Copy link = the
2214
2253
  // durable public S3 URL. Download opens in a new tab when the browser can't
@@ -2257,6 +2296,9 @@ button.rk-clips-thumb{appearance:none;-webkit-appearance:none;border:0;margin:0;
2257
2296
  var it = items[idx];
2258
2297
  nameEl.textContent = it.name || 'file'; nameEl.title = it.name || '';
2259
2298
  dlBtn.href = it.viewUrl; dlBtn.setAttribute('download', it.name || '');
2299
+ var editHref = toolEditUrl(it);
2300
+ editBtn.hidden = !editHref;
2301
+ if (editHref) { editBtn.href = editHref; editBtn.title = 'Edit in the ' + (fileKind(it.name, it.contentType) === 'video' ? 'video' : 'image') + ' tool'; }
2260
2302
  linkBtn.style.display = it.publicUrl ? '' : 'none';
2261
2303
  body.innerHTML = '';
2262
2304
  var kind = fileKind(it.name, it.contentType);
@@ -2311,7 +2353,15 @@ button.rk-clips-thumb{appearance:none;-webkit-appearance:none;border:0;margin:0;
2311
2353
  var files = [], seen = {};
2312
2354
  var arts = (job && job.artifacts) || [];
2313
2355
  for (var i = 0; i < arts.length; i++) { var f = rkFileFromArtifact(arts[i]); if (f && !seen[f.viewUrl]) { seen[f.viewUrl] = true; files.push(f); } }
2314
- var mu = rkResultMediaUrl(job); if (mu && !seen[mu]) { seen[mu] = true; files.push({ name: (job.tracer || 'output'), contentType: '', viewUrl: mu, publicUrl: mu }); }
2356
+ // Name the direct result URL after the FILE, not the tracer fileKind() reads the
2357
+ // extension, and a tracer-named entry ("my-poster") classifies as 'other', losing
2358
+ // the image/video thumbnail and the edit-in-tool hand-off.
2359
+ var mu = rkResultMediaUrl(job);
2360
+ if (mu && !seen[mu]) {
2361
+ seen[mu] = true;
2362
+ var seg = String(mu).split('?')[0].split('/').pop() || '';
2363
+ files.push({ name: (/.[a-z0-9]{2,5}$/i.test(seg) ? seg : (job.tracer || 'output')), contentType: '', viewUrl: mu, publicUrl: mu });
2364
+ }
2315
2365
  var res = job && job.result;
2316
2366
  if (res && typeof res === 'object' && !Array.isArray(res) && Object.keys(res).length) {
2317
2367
  try { var blob = new Blob([JSON.stringify(res, null, 2)], { type: 'application/json' }); files.push({ name: (job.tracer || 'result') + '.json', contentType: 'application/json', viewUrl: URL.createObjectURL(blob), publicUrl: null }); } catch (e) {}
@@ -2364,14 +2414,21 @@ button.rk-clips-thumb{appearance:none;-webkit-appearance:none;border:0;margin:0;
2364
2414
  var grid = document.createElement('div'); grid.className = 'rk-aichat-files-grid';
2365
2415
  list.forEach(function (f, ix) {
2366
2416
  var k = fileKind(f.name, f.contentType);
2367
- var card = document.createElement('button'); card.type = 'button'; card.className = 'rk-aichat-file';
2417
+ // A div, not a button: media cards carry a nested "edit in tool" link, and
2418
+ // an anchor inside a button is invalid (and unclickable in some browsers).
2419
+ var card = document.createElement('div'); card.className = 'rk-aichat-file'; card.tabIndex = 0; card.setAttribute('role', 'button');
2368
2420
  var thumb = document.createElement('div'); thumb.className = 'rk-aichat-file-thumb rk-aichat-file-thumb--' + k;
2369
2421
  if (k === 'image') { var im = document.createElement('img'); im.src = f.viewUrl; im.alt = ''; im.loading = 'lazy'; thumb.appendChild(im); }
2370
2422
  else if (k === 'video') { var v = document.createElement('video'); v.src = f.viewUrl; v.muted = true; v.preload = 'metadata'; v.playsInline = true; thumb.appendChild(v); var pl = document.createElement('span'); pl.className = 'rk-aichat-file-play'; pl.textContent = '▶'; thumb.appendChild(pl); }
2371
2423
  else { var ic = document.createElement('span'); ic.className = 'rk-aichat-file-ic'; ic.textContent = fileIcon(k); thumb.appendChild(ic); }
2424
+ var edit = toolEditLink(f, 'rk-aichat-file-open');
2425
+ if (edit) { edit.innerHTML = PENCIL_SVG; thumb.appendChild(edit); }
2372
2426
  var nm = document.createElement('div'); nm.className = 'rk-aichat-file-name'; nm.textContent = f.name || 'file'; nm.title = f.name || '';
2373
2427
  card.appendChild(thumb); card.appendChild(nm);
2374
2428
  card.addEventListener('click', function () { openFilePreview(list, ix); });
2429
+ card.addEventListener('keydown', function (e) {
2430
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openFilePreview(list, ix); }
2431
+ });
2375
2432
  grid.appendChild(card);
2376
2433
  });
2377
2434
  wrap.appendChild(grid); turn.appendChild(wrap); logBottom();
@@ -591,6 +591,7 @@
591
591
  background:#fff;border:1px solid var(--rk-border);border-radius:var(--rk-r-full);padding:4px 11px;text-decoration:none;line-height:1.4;
592
592
  transition:background var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
593
593
  .rk-aichat-preview-act:hover{background:var(--rk-gold-tint);border-color:var(--rk-gold-600)}
594
+ .rk-aichat-preview-act[hidden]{display:none}
594
595
 
595
596
  /* ── markdown pipe tables (assistant messages + .md previews) ── */
596
597
  .rk-md-tablewrap{max-width:100%;overflow-x:auto;margin:8px 0;border:1px solid var(--rk-border);border-radius:var(--rk-r-lg)}
@@ -622,6 +623,14 @@
622
623
  .rk-aichat-file-thumb--markdown,.rk-aichat-file-thumb--text{background:var(--rk-n-50)}.rk-aichat-file-thumb--audio{background:var(--rk-gold-tint2)}
623
624
  .rk-aichat-file-play{position:absolute;inset:0;display:grid;place-items:center;font-size:22px;color:#fff;text-shadow:0 1px 6px rgba(0,0,0,.55);pointer-events:none}
624
625
  .rk-aichat-file-name{font-size:11px;font-weight:600;color:var(--rk-n-600);line-height:1.3;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:112px}
626
+ /* "Edit in the tool" pencil — only on image/video cards; opens /tools/image or
627
+ /tools/video in a new tab with this file preloaded as the working file. */
628
+ .rk-aichat-file-open{position:absolute;top:5px;right:5px;width:24px;height:24px;border-radius:var(--rk-r-md);
629
+ display:grid;place-items:center;background:rgba(255,255,255,.94);border:1px solid var(--rk-border);color:var(--rk-ink);
630
+ opacity:0;transition:opacity var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease);z-index:2}
631
+ .rk-aichat-file-open svg{width:13px;height:13px}
632
+ .rk-aichat-file:hover .rk-aichat-file-open,.rk-aichat-file-open:focus-visible{opacity:1}
633
+ .rk-aichat-file-open:hover{background:var(--rk-gold-tint)}
625
634
 
626
635
  /* create_video pipeline: live status row + clickable result links */
627
636
  .rk-aichat-flow{display:flex;align-items:center;gap:9px;font-size:12.5px;color:var(--rk-text-muted);padding:2px 2px}
@@ -3224,6 +3233,34 @@ button.rk-clips-thumb{appearance:none;-webkit-appearance:none;border:0;margin:0;
3224
3233
  }
3225
3234
  function fileIcon(kind) { return kind === 'pdf' ? '📕' : kind === 'json' ? '{ }' : kind === 'markdown' ? '📝' : kind === 'audio' ? '🎵' : kind === 'text' ? '📄' : '📄'; }
3226
3235
 
3236
+ // ── "edit this media" hand-off: dock chat → the matching creative tool ──
3237
+ // Images open /tools/image, videos /tools/video, each with the file preloaded as
3238
+ // that tool's working file (its chat then edits THAT media). Deliberately a fresh
3239
+ // tool session — no ?tracer — so the tool names its own chat from the first prompt.
3240
+ var PENCIL_SVG = '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13.6 3.6a1.9 1.9 0 0 1 2.8 2.8L7.7 15.1l-3.6.8.8-3.6z"/></svg>';
3241
+ function toolEditUrl(file) {
3242
+ if (!file || !file.viewUrl) return '';
3243
+ var k = fileKind(file.name, file.contentType);
3244
+ var name = file.name || '';
3245
+ if (k === 'image') return '/tools/image?source_image_url=' + encodeURIComponent(file.viewUrl) + (name ? '&source_image_name=' + encodeURIComponent(name) : '');
3246
+ if (k === 'video') return '/tools/video?source_video_url=' + encodeURIComponent(file.viewUrl) + (name ? '&source_video_name=' + encodeURIComponent(name) : '');
3247
+ return '';
3248
+ }
3249
+ function toolEditLink(file, className) {
3250
+ var href = toolEditUrl(file);
3251
+ if (!href) return null;
3252
+ var a = document.createElement('a');
3253
+ a.className = className;
3254
+ a.href = href;
3255
+ a.target = '_blank';
3256
+ a.rel = 'noopener noreferrer';
3257
+ a.title = 'Edit in the ' + (fileKind(file.name, file.contentType) === 'video' ? 'video' : 'image') + ' tool';
3258
+ a.setAttribute('aria-label', a.title);
3259
+ // The card itself opens the lightbox; the pencil must not also trigger it.
3260
+ a.addEventListener('click', function (e) { e.stopPropagation(); });
3261
+ return a;
3262
+ }
3263
+
3227
3264
  // Compact markdown → HTML for .md previews (headings, lists, quotes, rules,
3228
3265
  // fenced + inline code, bold/italic/links). Escapes first, then injects tags
3229
3266
  // (same order as linkify). Backticks are built via char codes to avoid
@@ -3284,7 +3321,9 @@ button.rk-clips-thumb{appearance:none;-webkit-appearance:none;border:0;margin:0;
3284
3321
  var dlBtn = document.createElement('a'); dlBtn.className = 'rk-aichat-preview-act'; dlBtn.target = '_blank'; dlBtn.rel = 'noopener noreferrer'; dlBtn.textContent = 'Download';
3285
3322
  var linkBtn = document.createElement('button'); linkBtn.type = 'button'; linkBtn.className = 'rk-aichat-preview-act'; linkBtn.textContent = 'Copy link';
3286
3323
  var xBtn = document.createElement('button'); xBtn.type = 'button'; xBtn.className = 'rk-aichat-modal-x'; xBtn.setAttribute('aria-label', 'Close'); xBtn.innerHTML = '&times;';
3287
- head.appendChild(nameEl); head.appendChild(copyBtn); head.appendChild(dlBtn); head.appendChild(linkBtn); head.appendChild(xBtn);
3324
+ // Same hand-off as the card pencil, spelled out as a labelled action here.
3325
+ var editBtn = document.createElement('a'); editBtn.className = 'rk-aichat-preview-act'; editBtn.target = '_blank'; editBtn.rel = 'noopener noreferrer'; editBtn.textContent = 'Edit';
3326
+ head.appendChild(nameEl); head.appendChild(editBtn); head.appendChild(copyBtn); head.appendChild(dlBtn); head.appendChild(linkBtn); head.appendChild(xBtn);
3288
3327
  function flashAct(btn, label) { var o = btn.getAttribute('data-label') || btn.textContent; btn.setAttribute('data-label', o); btn.textContent = label; setTimeout(function () { btn.textContent = o; }, 1400); }
3289
3328
  // Copy = file text (json/md/txt) or its URL for binaries. Copy link = the
3290
3329
  // durable public S3 URL. Download opens in a new tab when the browser can't
@@ -3333,6 +3372,9 @@ button.rk-clips-thumb{appearance:none;-webkit-appearance:none;border:0;margin:0;
3333
3372
  var it = items[idx];
3334
3373
  nameEl.textContent = it.name || 'file'; nameEl.title = it.name || '';
3335
3374
  dlBtn.href = it.viewUrl; dlBtn.setAttribute('download', it.name || '');
3375
+ var editHref = toolEditUrl(it);
3376
+ editBtn.hidden = !editHref;
3377
+ if (editHref) { editBtn.href = editHref; editBtn.title = 'Edit in the ' + (fileKind(it.name, it.contentType) === 'video' ? 'video' : 'image') + ' tool'; }
3336
3378
  linkBtn.style.display = it.publicUrl ? '' : 'none';
3337
3379
  body.innerHTML = '';
3338
3380
  var kind = fileKind(it.name, it.contentType);
@@ -3387,7 +3429,15 @@ button.rk-clips-thumb{appearance:none;-webkit-appearance:none;border:0;margin:0;
3387
3429
  var files = [], seen = {};
3388
3430
  var arts = (job && job.artifacts) || [];
3389
3431
  for (var i = 0; i < arts.length; i++) { var f = rkFileFromArtifact(arts[i]); if (f && !seen[f.viewUrl]) { seen[f.viewUrl] = true; files.push(f); } }
3390
- var mu = rkResultMediaUrl(job); if (mu && !seen[mu]) { seen[mu] = true; files.push({ name: (job.tracer || 'output'), contentType: '', viewUrl: mu, publicUrl: mu }); }
3432
+ // Name the direct result URL after the FILE, not the tracer fileKind() reads the
3433
+ // extension, and a tracer-named entry ("my-poster") classifies as 'other', losing
3434
+ // the image/video thumbnail and the edit-in-tool hand-off.
3435
+ var mu = rkResultMediaUrl(job);
3436
+ if (mu && !seen[mu]) {
3437
+ seen[mu] = true;
3438
+ var seg = String(mu).split('?')[0].split('/').pop() || '';
3439
+ files.push({ name: (/.[a-z0-9]{2,5}$/i.test(seg) ? seg : (job.tracer || 'output')), contentType: '', viewUrl: mu, publicUrl: mu });
3440
+ }
3391
3441
  var res = job && job.result;
3392
3442
  if (res && typeof res === 'object' && !Array.isArray(res) && Object.keys(res).length) {
3393
3443
  try { var blob = new Blob([JSON.stringify(res, null, 2)], { type: 'application/json' }); files.push({ name: (job.tracer || 'result') + '.json', contentType: 'application/json', viewUrl: URL.createObjectURL(blob), publicUrl: null }); } catch (e) {}
@@ -3440,14 +3490,21 @@ button.rk-clips-thumb{appearance:none;-webkit-appearance:none;border:0;margin:0;
3440
3490
  var grid = document.createElement('div'); grid.className = 'rk-aichat-files-grid';
3441
3491
  list.forEach(function (f, ix) {
3442
3492
  var k = fileKind(f.name, f.contentType);
3443
- var card = document.createElement('button'); card.type = 'button'; card.className = 'rk-aichat-file';
3493
+ // A div, not a button: media cards carry a nested "edit in tool" link, and
3494
+ // an anchor inside a button is invalid (and unclickable in some browsers).
3495
+ var card = document.createElement('div'); card.className = 'rk-aichat-file'; card.tabIndex = 0; card.setAttribute('role', 'button');
3444
3496
  var thumb = document.createElement('div'); thumb.className = 'rk-aichat-file-thumb rk-aichat-file-thumb--' + k;
3445
3497
  if (k === 'image') { var im = document.createElement('img'); im.src = f.viewUrl; im.alt = ''; im.loading = 'lazy'; thumb.appendChild(im); }
3446
3498
  else if (k === 'video') { var v = document.createElement('video'); v.src = f.viewUrl; v.muted = true; v.preload = 'metadata'; v.playsInline = true; thumb.appendChild(v); var pl = document.createElement('span'); pl.className = 'rk-aichat-file-play'; pl.textContent = '▶'; thumb.appendChild(pl); }
3447
3499
  else { var ic = document.createElement('span'); ic.className = 'rk-aichat-file-ic'; ic.textContent = fileIcon(k); thumb.appendChild(ic); }
3500
+ var edit = toolEditLink(f, 'rk-aichat-file-open');
3501
+ if (edit) { edit.innerHTML = PENCIL_SVG; thumb.appendChild(edit); }
3448
3502
  var nm = document.createElement('div'); nm.className = 'rk-aichat-file-name'; nm.textContent = f.name || 'file'; nm.title = f.name || '';
3449
3503
  card.appendChild(thumb); card.appendChild(nm);
3450
3504
  card.addEventListener('click', function () { openFilePreview(list, ix); });
3505
+ card.addEventListener('keydown', function (e) {
3506
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openFilePreview(list, ix); }
3507
+ });
3451
3508
  grid.appendChild(card);
3452
3509
  });
3453
3510
  wrap.appendChild(grid); turn.appendChild(wrap); logBottom();
@@ -591,6 +591,7 @@
591
591
  background:#fff;border:1px solid var(--rk-border);border-radius:var(--rk-r-full);padding:4px 11px;text-decoration:none;line-height:1.4;
592
592
  transition:background var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
593
593
  .rk-aichat-preview-act:hover{background:var(--rk-gold-tint);border-color:var(--rk-gold-600)}
594
+ .rk-aichat-preview-act[hidden]{display:none}
594
595
 
595
596
  /* ── markdown pipe tables (assistant messages + .md previews) ── */
596
597
  .rk-md-tablewrap{max-width:100%;overflow-x:auto;margin:8px 0;border:1px solid var(--rk-border);border-radius:var(--rk-r-lg)}
@@ -622,6 +623,14 @@
622
623
  .rk-aichat-file-thumb--markdown,.rk-aichat-file-thumb--text{background:var(--rk-n-50)}.rk-aichat-file-thumb--audio{background:var(--rk-gold-tint2)}
623
624
  .rk-aichat-file-play{position:absolute;inset:0;display:grid;place-items:center;font-size:22px;color:#fff;text-shadow:0 1px 6px rgba(0,0,0,.55);pointer-events:none}
624
625
  .rk-aichat-file-name{font-size:11px;font-weight:600;color:var(--rk-n-600);line-height:1.3;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:112px}
626
+ /* "Edit in the tool" pencil — only on image/video cards; opens /tools/image or
627
+ /tools/video in a new tab with this file preloaded as the working file. */
628
+ .rk-aichat-file-open{position:absolute;top:5px;right:5px;width:24px;height:24px;border-radius:var(--rk-r-md);
629
+ display:grid;place-items:center;background:rgba(255,255,255,.94);border:1px solid var(--rk-border);color:var(--rk-ink);
630
+ opacity:0;transition:opacity var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease);z-index:2}
631
+ .rk-aichat-file-open svg{width:13px;height:13px}
632
+ .rk-aichat-file:hover .rk-aichat-file-open,.rk-aichat-file-open:focus-visible{opacity:1}
633
+ .rk-aichat-file-open:hover{background:var(--rk-gold-tint)}
625
634
 
626
635
  /* create_video pipeline: live status row + clickable result links */
627
636
  .rk-aichat-flow{display:flex;align-items:center;gap:9px;font-size:12.5px;color:var(--rk-text-muted);padding:2px 2px}
@@ -2674,6 +2683,34 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
2674
2683
  }
2675
2684
  function fileIcon(kind) { return kind === 'pdf' ? '📕' : kind === 'json' ? '{ }' : kind === 'markdown' ? '📝' : kind === 'audio' ? '🎵' : kind === 'text' ? '📄' : '📄'; }
2676
2685
 
2686
+ // ── "edit this media" hand-off: dock chat → the matching creative tool ──
2687
+ // Images open /tools/image, videos /tools/video, each with the file preloaded as
2688
+ // that tool's working file (its chat then edits THAT media). Deliberately a fresh
2689
+ // tool session — no ?tracer — so the tool names its own chat from the first prompt.
2690
+ var PENCIL_SVG = '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13.6 3.6a1.9 1.9 0 0 1 2.8 2.8L7.7 15.1l-3.6.8.8-3.6z"/></svg>';
2691
+ function toolEditUrl(file) {
2692
+ if (!file || !file.viewUrl) return '';
2693
+ var k = fileKind(file.name, file.contentType);
2694
+ var name = file.name || '';
2695
+ if (k === 'image') return '/tools/image?source_image_url=' + encodeURIComponent(file.viewUrl) + (name ? '&source_image_name=' + encodeURIComponent(name) : '');
2696
+ if (k === 'video') return '/tools/video?source_video_url=' + encodeURIComponent(file.viewUrl) + (name ? '&source_video_name=' + encodeURIComponent(name) : '');
2697
+ return '';
2698
+ }
2699
+ function toolEditLink(file, className) {
2700
+ var href = toolEditUrl(file);
2701
+ if (!href) return null;
2702
+ var a = document.createElement('a');
2703
+ a.className = className;
2704
+ a.href = href;
2705
+ a.target = '_blank';
2706
+ a.rel = 'noopener noreferrer';
2707
+ a.title = 'Edit in the ' + (fileKind(file.name, file.contentType) === 'video' ? 'video' : 'image') + ' tool';
2708
+ a.setAttribute('aria-label', a.title);
2709
+ // The card itself opens the lightbox; the pencil must not also trigger it.
2710
+ a.addEventListener('click', function (e) { e.stopPropagation(); });
2711
+ return a;
2712
+ }
2713
+
2677
2714
  // Compact markdown → HTML for .md previews (headings, lists, quotes, rules,
2678
2715
  // fenced + inline code, bold/italic/links). Escapes first, then injects tags
2679
2716
  // (same order as linkify). Backticks are built via char codes to avoid
@@ -2734,7 +2771,9 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
2734
2771
  var dlBtn = document.createElement('a'); dlBtn.className = 'rk-aichat-preview-act'; dlBtn.target = '_blank'; dlBtn.rel = 'noopener noreferrer'; dlBtn.textContent = 'Download';
2735
2772
  var linkBtn = document.createElement('button'); linkBtn.type = 'button'; linkBtn.className = 'rk-aichat-preview-act'; linkBtn.textContent = 'Copy link';
2736
2773
  var xBtn = document.createElement('button'); xBtn.type = 'button'; xBtn.className = 'rk-aichat-modal-x'; xBtn.setAttribute('aria-label', 'Close'); xBtn.innerHTML = '&times;';
2737
- head.appendChild(nameEl); head.appendChild(copyBtn); head.appendChild(dlBtn); head.appendChild(linkBtn); head.appendChild(xBtn);
2774
+ // Same hand-off as the card pencil, spelled out as a labelled action here.
2775
+ var editBtn = document.createElement('a'); editBtn.className = 'rk-aichat-preview-act'; editBtn.target = '_blank'; editBtn.rel = 'noopener noreferrer'; editBtn.textContent = 'Edit';
2776
+ head.appendChild(nameEl); head.appendChild(editBtn); head.appendChild(copyBtn); head.appendChild(dlBtn); head.appendChild(linkBtn); head.appendChild(xBtn);
2738
2777
  function flashAct(btn, label) { var o = btn.getAttribute('data-label') || btn.textContent; btn.setAttribute('data-label', o); btn.textContent = label; setTimeout(function () { btn.textContent = o; }, 1400); }
2739
2778
  // Copy = file text (json/md/txt) or its URL for binaries. Copy link = the
2740
2779
  // durable public S3 URL. Download opens in a new tab when the browser can't
@@ -2783,6 +2822,9 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
2783
2822
  var it = items[idx];
2784
2823
  nameEl.textContent = it.name || 'file'; nameEl.title = it.name || '';
2785
2824
  dlBtn.href = it.viewUrl; dlBtn.setAttribute('download', it.name || '');
2825
+ var editHref = toolEditUrl(it);
2826
+ editBtn.hidden = !editHref;
2827
+ if (editHref) { editBtn.href = editHref; editBtn.title = 'Edit in the ' + (fileKind(it.name, it.contentType) === 'video' ? 'video' : 'image') + ' tool'; }
2786
2828
  linkBtn.style.display = it.publicUrl ? '' : 'none';
2787
2829
  body.innerHTML = '';
2788
2830
  var kind = fileKind(it.name, it.contentType);
@@ -2837,7 +2879,15 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
2837
2879
  var files = [], seen = {};
2838
2880
  var arts = (job && job.artifacts) || [];
2839
2881
  for (var i = 0; i < arts.length; i++) { var f = rkFileFromArtifact(arts[i]); if (f && !seen[f.viewUrl]) { seen[f.viewUrl] = true; files.push(f); } }
2840
- var mu = rkResultMediaUrl(job); if (mu && !seen[mu]) { seen[mu] = true; files.push({ name: (job.tracer || 'output'), contentType: '', viewUrl: mu, publicUrl: mu }); }
2882
+ // Name the direct result URL after the FILE, not the tracer fileKind() reads the
2883
+ // extension, and a tracer-named entry ("my-poster") classifies as 'other', losing
2884
+ // the image/video thumbnail and the edit-in-tool hand-off.
2885
+ var mu = rkResultMediaUrl(job);
2886
+ if (mu && !seen[mu]) {
2887
+ seen[mu] = true;
2888
+ var seg = String(mu).split('?')[0].split('/').pop() || '';
2889
+ files.push({ name: (/.[a-z0-9]{2,5}$/i.test(seg) ? seg : (job.tracer || 'output')), contentType: '', viewUrl: mu, publicUrl: mu });
2890
+ }
2841
2891
  var res = job && job.result;
2842
2892
  if (res && typeof res === 'object' && !Array.isArray(res) && Object.keys(res).length) {
2843
2893
  try { var blob = new Blob([JSON.stringify(res, null, 2)], { type: 'application/json' }); files.push({ name: (job.tracer || 'result') + '.json', contentType: 'application/json', viewUrl: URL.createObjectURL(blob), publicUrl: null }); } catch (e) {}
@@ -2890,14 +2940,21 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
2890
2940
  var grid = document.createElement('div'); grid.className = 'rk-aichat-files-grid';
2891
2941
  list.forEach(function (f, ix) {
2892
2942
  var k = fileKind(f.name, f.contentType);
2893
- var card = document.createElement('button'); card.type = 'button'; card.className = 'rk-aichat-file';
2943
+ // A div, not a button: media cards carry a nested "edit in tool" link, and
2944
+ // an anchor inside a button is invalid (and unclickable in some browsers).
2945
+ var card = document.createElement('div'); card.className = 'rk-aichat-file'; card.tabIndex = 0; card.setAttribute('role', 'button');
2894
2946
  var thumb = document.createElement('div'); thumb.className = 'rk-aichat-file-thumb rk-aichat-file-thumb--' + k;
2895
2947
  if (k === 'image') { var im = document.createElement('img'); im.src = f.viewUrl; im.alt = ''; im.loading = 'lazy'; thumb.appendChild(im); }
2896
2948
  else if (k === 'video') { var v = document.createElement('video'); v.src = f.viewUrl; v.muted = true; v.preload = 'metadata'; v.playsInline = true; thumb.appendChild(v); var pl = document.createElement('span'); pl.className = 'rk-aichat-file-play'; pl.textContent = '▶'; thumb.appendChild(pl); }
2897
2949
  else { var ic = document.createElement('span'); ic.className = 'rk-aichat-file-ic'; ic.textContent = fileIcon(k); thumb.appendChild(ic); }
2950
+ var edit = toolEditLink(f, 'rk-aichat-file-open');
2951
+ if (edit) { edit.innerHTML = PENCIL_SVG; thumb.appendChild(edit); }
2898
2952
  var nm = document.createElement('div'); nm.className = 'rk-aichat-file-name'; nm.textContent = f.name || 'file'; nm.title = f.name || '';
2899
2953
  card.appendChild(thumb); card.appendChild(nm);
2900
2954
  card.addEventListener('click', function () { openFilePreview(list, ix); });
2955
+ card.addEventListener('keydown', function (e) {
2956
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openFilePreview(list, ix); }
2957
+ });
2901
2958
  grid.appendChild(card);
2902
2959
  });
2903
2960
  wrap.appendChild(grid); turn.appendChild(wrap); logBottom();