transitions-refine 0.3.4 → 0.3.6

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.
package/demo.html CHANGED
@@ -6,7 +6,7 @@
6
6
  <title>Timeline Inspector — Demo</title>
7
7
  <link rel="preconnect" href="https://fonts.googleapis.com" />
8
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Roboto+Mono:wght@400;500&display=swap" rel="stylesheet" />
10
10
  <style>
11
11
  /* @inject-skip-start (demo-only global resets — excluded from the injected build) */
12
12
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
@@ -76,7 +76,7 @@
76
76
  --text-swap-ease: ease-in-out;
77
77
  --shimmer-ease: linear;
78
78
  /* transitions.dev — panel reveal (per-phase distance/ease/scale) */
79
- --panel-open-dur: 400ms; --panel-close-dur: 250ms;
79
+ --panel-open-dur: 400ms; --panel-close-dur: 350ms;
80
80
  /* refine panel close runs a touch longer than the main panel close */
81
81
  --refine-close-dur: 350ms;
82
82
  --panel-open-distance: 100px; --panel-close-distance: 10px;
@@ -213,12 +213,32 @@
213
213
  width: 40px; height: 3px; border-radius: 2px; background: #dcdce2; pointer-events: none;
214
214
  transition: background 0.15s;
215
215
  }
216
- .tl-resize-handle:hover::after, .tl-resize-handle.dragging::after { background: var(--c-blue); }
216
+ .tl-resize-handle:hover::after, .tl-resize-handle.dragging::after { background: var(--c-thumb); }
217
217
  .tl-panel-body { position: relative; flex: 1; min-height: 0; display: flex; flex-direction: row; gap: 0; pointer-events: auto;
218
- background: #fff; border-radius: 12px; box-shadow: var(--card-shadow); overflow: hidden; }
218
+ background: #fff; border-radius: 12px;
219
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08), 0 2px 6px 0 rgba(0, 0, 0, 0.05), 0 4px 42px 0 rgba(0, 0, 0, 0.06);
220
+ overflow: hidden; }
219
221
  .tl-panel-body > * { pointer-events: auto; }
220
222
  /* timeline content column — full width; the refine panel overlays it without reflow */
221
223
  .tl-panel-main { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; gap: 0; overflow: hidden; }
224
+ /* transient toast (Copy values → "Values copied"). enter 250ms Fast,
225
+ exit 350ms Medium (toast-close token), Smooth ease out both ways. */
226
+ .tl-toast-wrap { position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%);
227
+ z-index: 2147483600; pointer-events: none; }
228
+ .tl-toast { display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 14px 0 10px;
229
+ border-radius: 60px; background: #fff; color: var(--c-text-strong);
230
+ font: inherit; font-size: 13px; font-weight: 400; line-height: 14px; white-space: nowrap;
231
+ box-shadow: 0 0 0 1px rgba(0,0,0,0.08), 0 2px 6px 0 rgba(0,0,0,0.05), 0 4px 42px 0 rgba(0,0,0,0.06);
232
+ animation: tl-toast-in 250ms cubic-bezier(0.22,1,0.36,1) both; }
233
+ .tl-toast.is-closing { animation: tl-toast-out 350ms cubic-bezier(0.22,1,0.36,1) both; }
234
+ /* gray icon to match the Accept button's icon (#17181C) */
235
+ .tl-toast-ic { display: flex; flex: none; color: #17181C; }
236
+ @keyframes tl-toast-in { from { opacity: 0; transform: translateY(8px) scale(0.98); }
237
+ to { opacity: 1; transform: none; } }
238
+ @keyframes tl-toast-out { from { opacity: 1; transform: none; }
239
+ to { opacity: 0; transform: translateY(8px) scale(0.98); } }
240
+ @media (prefers-reduced-motion: reduce) {
241
+ .tl-toast, .tl-toast.is-closing { animation: none; } }
222
242
  .tl-pill { position: fixed; bottom: 16px; right: 16px; z-index: 99999;
223
243
  height: 40px; background: #fff; border-radius: 36px;
224
244
  padding: 6px 10px 6px 16px;
@@ -242,7 +262,7 @@
242
262
  .tl-header { flex: none; position: relative; z-index: 40; display: flex; align-items: center; gap: 8px; padding: 7.5px 14px;
243
263
  border-bottom: 1px solid var(--c-line); }
244
264
  .tl-header-label { font-size: 13px; font-weight: 500; line-height: 18px; color: var(--c-text-mut2); white-space: nowrap; }
245
- .tl-header-count { margin-left: auto; font-size: 13px; line-height: 18px; color: var(--c-count); white-space: nowrap; }
265
+ .tl-header-count { margin-left: auto; display: inline-flex; align-items: center; font-size: 13px; line-height: 18px; color: var(--c-count); white-space: nowrap; }
246
266
 
247
267
  /* ── buttons ── */
248
268
  /* topbar buttons share a fully rounded 60px corner radius */
@@ -306,7 +326,6 @@
306
326
  display: inline-flex; align-items: center; gap: 8px; height: 36px; padding: 0 12px;
307
327
  border: none; border-radius: 8px; background: transparent; color: var(--c-blue);
308
328
  box-shadow: var(--drop-btn);
309
- box-shadow: 0 1px 3px 0 color(display-p3 0.0157 0.1608 0.4588 / 0.08);
310
329
  font: inherit; font-size: 13px; font-weight: 500; line-height: 14px;
311
330
  cursor: pointer; white-space: nowrap; transition: color 0.12s ease; }
312
331
  .tl-refine-btn::before { content: ""; position: absolute; inset: 0; border-radius: inherit;
@@ -315,7 +334,6 @@
315
334
  transition: background 0.12s ease; z-index: 0; }
316
335
  .tl-refine-btn::after { content: ""; position: absolute; inset: 0; border-radius: inherit;
317
336
  box-shadow: var(--ring-blue);
318
- box-shadow: inset 0 0 0 1px color(display-p3 0 0.3961 0.8157 / 0.10), inset 0 -1px 0 0 color(display-p3 0.0118 0.2588 0.5569 / 0.15);
319
337
  pointer-events: none; z-index: 2; }
320
338
  .tl-refine-btn > * { position: relative; z-index: 1; }
321
339
  /* Figma: wand icon stroke is #0073E5, distinct from the #0071e2 label */
@@ -326,9 +344,9 @@
326
344
  .tl-refine-btn:disabled { cursor: default; color: var(--c-blue); }
327
345
  .tl-refine-btn:disabled::before { background: var(--c-blue-bg); background: color(display-p3 0 0.451 0.898 / 0.06); }
328
346
  .tl-refine-btn:disabled span { color: var(--c-blue-pressed); opacity: 0.5; }
329
- /* tiny 1px sparks flying out along each of the wand's spark rays on hover */
347
+ /* tiny 1.5px sparks flying out along each of the wand's spark rays on hover */
330
348
  .tl-refine-sparks { position: absolute; left: 23px; top: 15px; width: 0; height: 0; z-index: 3; pointer-events: none; }
331
- .tl-refine-sparks i { position: absolute; left: 0; top: 0; width: 1px; height: 1px; border-radius: 50%;
349
+ .tl-refine-sparks i { position: absolute; left: 0; top: 0; width: 1.5px; height: 1.5px; border-radius: 50%;
332
350
  background: var(--c-blue); opacity: 0; }
333
351
  @keyframes tl-refine-spark {
334
352
  0% { opacity: 0; transform: translate(var(--ox, 0), var(--oy, 0)) scale(0.4); }
@@ -344,9 +362,14 @@
344
362
  /* ── body / floating cards row ── */
345
363
  .tl-body { display: flex; flex: 1; min-height: 0; gap: 0; }
346
364
  .tl-main { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
347
- .tl-inspector { flex: 0 0 280px; padding: 14px 16px 18px; display: flex; flex-direction: column;
365
+ .tl-inspector { flex: 0 0 280px; padding: 16px 16px; display: flex; flex-direction: column;
348
366
  border-left: 1px solid var(--c-line); min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
349
- .tl-insp-title { font-size: 13px; font-weight: 500; line-height: 18px; color: #171717; margin-bottom: 10px; text-transform: capitalize; }
367
+ /* title sits centered in the ~52px band between the Refine toolbar and the
368
+ Duration field: 16px above + 20px title + 16px below = 52px */
369
+ .tl-insp-title { display: flex; align-items: center; font-size: 13px; font-weight: 500; line-height: 18px; color: #171717; margin-bottom: 16px; text-transform: capitalize; }
370
+ /* full-bleed divider between the Delay field and the Easing/Springs tabs
371
+ (Figma 580:9049 — spans the inspector edge-to-edge past the 16px padding) */
372
+ .tl-inspector-divider { height: 1px; background: var(--c-line); margin: 0 -16px; }
350
373
  .tl-insp-label { font-size: 12px; line-height: 18px; color: #737373; margin: 10px 0 6px; }
351
374
 
352
375
  /* ── tracks / ruler ── */
@@ -411,11 +434,11 @@
411
434
  .tl-col-resizer-line { width: 1px; height: 100%; background: transparent;
412
435
  transition: background 0.12s ease, width 0.12s ease; }
413
436
  .tl-col-resizer:hover .tl-col-resizer-line,
414
- .tl-col-resizer.dragging .tl-col-resizer-line { width: 2px; background: #1A7AFF; }
437
+ .tl-col-resizer.dragging .tl-col-resizer-line { width: 2px; background: var(--c-thumb); }
415
438
  @media (prefers-reduced-motion: reduce) { .tl-col-resizer-line { transition: none; } }
416
439
 
417
440
  /* ── value field (slider + input) — exact Figma "Value slider and input" ── */
418
- .tl-field-wrap { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; }
441
+ .tl-field-wrap { display: flex; align-items: center; gap: 6px; margin-bottom: 12px; }
419
442
  .tl-field { position: relative; flex: 1; min-width: 0; height: 32px; border-radius: 8px;
420
443
  background: var(--c-field-bg); transition: box-shadow 0.12s ease; }
421
444
  /* focused: dual inset ring (Figma Focused state) */
@@ -452,7 +475,7 @@
452
475
  .tl-field-chevron:active { scale: 0.96; }
453
476
 
454
477
  /* ── easing editor ── */
455
- .tl-ease { display: flex; flex-direction: column; gap: 8px; }
478
+ .tl-ease { display: flex; flex-direction: column; gap: 12px; }
456
479
 
457
480
  /* tab content page-slide (transitions.dev · 08-page-side-by-side) */
458
481
  .tl-ease-pages.t-page-slide {
@@ -472,6 +495,7 @@
472
495
  .tl-ease-pages .t-page[data-page-id="2"] { --t-page-from-x: var(--page-slide-distance); }
473
496
  .tl-ease-pages .t-page {
474
497
  position: absolute; top: 0; left: 0; right: 0;
498
+ display: flex; flex-direction: column; gap: 12px;
475
499
  opacity: 0; pointer-events: none;
476
500
  transform: translateX(calc(var(--t-page-from-x, 0px) * var(--page-exit-enabled)));
477
501
  filter: blur(calc(var(--page-blur) * var(--page-exit-enabled)));
@@ -501,48 +525,37 @@
501
525
  .tl-select:disabled { opacity: 0.5; cursor: default; }
502
526
  .tl-select-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
503
527
  .tl-select-chev { display: flex; color: var(--c-ruler); flex: none; }
504
- /* easing curve — exact Figma (node 13044:2506): #f6f6f7, ~6px radius, subtle inset border */
505
- .tl-curve { background: #f6f6f7; border-radius: 6px; box-shadow: inset 0 0 0 1px rgba(0,0,0,0.04);
528
+ /* easing curve — exact Figma (node 580:11158): #f6f6f7, 8px radius, no grid */
529
+ .tl-curve { background: #f6f6f7; border-radius: 8px;
506
530
  padding: 0; position: relative; overflow: hidden; }
507
531
  .tl-curve svg { display: block; cursor: crosshair; width: 100%; height: auto; }
508
532
  .tl-curve-handle { cursor: grab; }
509
533
  .tl-curve-handle:active { cursor: grabbing; }
510
534
  .tl-cubic-row { display: flex; gap: 4px; }
511
- /* number field — Logram design system (node 13064:2431): 32px, #f7f7f7 fill,
512
- Inter Medium 13/16, gray hover overlay, inset 1px focus ring */
513
- .tl-cubic-row input { flex: 1; width: 0; height: 32px; border: none; border-radius: 8px; background: #f7f7f7;
514
- box-sizing: border-box; padding: 8px 0;
515
- text-align: center; font: inherit; font-size: 13px; font-weight: 500; line-height: 16px; color: #1b1b1b;
516
- outline: none; box-shadow: inset 0 0 0 0 rgba(0,0,0,0.14); transition: box-shadow 0.12s ease, background 0.12s ease; }
535
+ /* number field — Figma node 580:4814: value left-aligned (#1b1b1b),
536
+ 32px #f7f7f7 pill, hover overlay + inset focus ring. */
537
+ .tl-cubic-cell { flex: 1; min-width: 0; display: flex; flex-direction: row; align-items: center;
538
+ gap: 4px; height: 32px; box-sizing: border-box;
539
+ padding: 0 8px; border-radius: 8px; background: #f7f7f7;
540
+ box-shadow: inset 0 0 0 0 rgba(0,0,0,0.14); transition: box-shadow 0.12s ease, background 0.12s ease; }
541
+ .tl-cubic-cell:hover { background: linear-gradient(rgba(170,170,170,0.2),rgba(170,170,170,0.2)), #f7f7f7; }
542
+ .tl-cubic-cell:focus-within { box-shadow: inset 0 0 0 1px rgba(0,0,0,0.14); }
543
+ .tl-cubic-row input { flex: 1; min-width: 0; width: 100%; height: 100%; border: none; background: transparent;
544
+ box-sizing: border-box; padding: 0; text-align: left;
545
+ font: inherit; font-size: 12px; font-weight: 500; line-height: 16px; color: #1b1b1b; outline: none;
546
+ -moz-appearance: textfield; appearance: textfield; }
547
+ .tl-cubic-row input::-webkit-outer-spin-button, .tl-cubic-row input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; width: 0; }
517
548
  .tl-cubic-row input::placeholder { color: #979797; }
518
- .tl-cubic-row input:hover { background: linear-gradient(rgba(170,170,170,0.2),rgba(170,170,170,0.2)), #f7f7f7; }
519
- .tl-cubic-row input:focus { box-shadow: inset 0 0 0 1px rgba(0,0,0,0.14); }
520
549
  .tl-custom-input { height: 30px; border: none; border-radius: 6px; background: var(--c-field-bg); padding: 0 8px;
521
550
  font: inherit; font-size: 12px; font-family: monospace; color: var(--c-text); outline: none;
522
551
  box-shadow: inset 0 0 0 1px rgba(0,0,0,0.04); }
523
- .tl-bounce { background: #fff; border-radius: 10px; box-shadow: inset 0 0 0 1px var(--c-hairline);
524
- padding: 12px; display: flex; flex-direction: column; gap: 11px; }
525
- .tl-bounce-title { font-size: 11px; color: var(--c-text-mut); font-weight: 600; letter-spacing: 0.01em; }
526
- .tl-bounce-row { display: flex; align-items: center; gap: 10px; }
527
- .tl-bounce-row label { font-size: 12px; color: #585858; font-weight: 500; width: 58px; flex: none; }
528
- .tl-bounce-row input[type="range"] { flex: 1; height: 5px; -webkit-appearance: none; appearance: none;
529
- background: #e7e7ec; border-radius: 3px; outline: none; cursor: pointer; transition: filter 0.12s ease; }
530
- .tl-bounce-row input[type="range"]:hover { filter: brightness(0.98); }
531
- .tl-bounce-row input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none;
532
- width: 14px; height: 14px; border-radius: 50%; background: var(--c-blue); cursor: pointer;
533
- border: 2px solid #fff; box-shadow: 0 1px 3px rgba(0,113,226,0.4), 0 0 0 0.5px rgba(0,0,0,0.04);
534
- transition: transform 0.12s cubic-bezier(0.22,1,0.36,1); }
535
- .tl-bounce-row input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.12); }
536
- .tl-bounce-row input[type="range"]:active::-webkit-slider-thumb { transform: scale(1.18); }
537
- .tl-bounce-row input[type="range"]::-moz-range-thumb { width: 14px; height: 14px; border-radius: 50%;
538
- background: var(--c-blue); cursor: pointer; border: 2px solid #fff; box-shadow: 0 1px 3px rgba(0,113,226,0.4); }
539
- .tl-bounce-val { font-size: 12px; font-weight: 600; color: var(--c-blue); width: 38px; text-align: right;
540
- font-variant-numeric: tabular-nums; }
541
- .tl-bounce select { height: 26px; border: none; border-radius: 6px; background: var(--c-field-bg);
542
- font: inherit; font-size: 11px; color: var(--c-text); outline: none; box-shadow: inset 0 0 0 1px rgba(0,0,0,0.04); flex: 1; }
552
+ /* Spring physics controls exact Figma "Value slider and input" scrubbers
553
+ (same .tl-field component as Duration/Delay; no chevron stepper). */
554
+ .tl-spring-fields { display: flex; flex-direction: column; gap: 10px; }
555
+ .tl-spring-field { margin-bottom: 0; }
543
556
 
544
557
  /* ── Easing / Springs tabs — Logram design system (node 13064:2552): trackless toggle pills ── */
545
- .tl-seg { display: flex; align-items: center; gap: 4px; margin-top: 10px; }
558
+ .tl-seg { display: flex; align-items: center; gap: 4px; margin-top: 0; }
546
559
  .tl-seg-btn { height: 32px; padding: 6px 12px; border: none; border-radius: 8px; background: transparent;
547
560
  font: inherit; font-size: 13px; font-weight: 500; line-height: 14px; color: #676767; cursor: pointer;
548
561
  transition: background 0.14s ease, color 0.14s ease; }
@@ -551,40 +564,42 @@
551
564
 
552
565
  /* position preview (animated marker à la easing.dev) — collapsible accordion
553
566
  (transitions.dev · 21-accordion: grid-rows 0fr↔1fr + vertical chevron flip) */
554
- .tl-preview { margin-top: 10px; background: #fff; border-radius: 10px;
555
- box-shadow: inset 0 0 0 1px var(--c-hairline); overflow: hidden;
567
+ .tl-preview { margin-top: 0; background: transparent;
556
568
  --acc-dur: 260ms; --acc-ease: cubic-bezier(0.22, 1, 0.36, 1); }
557
- .tl-preview-head { display: flex; align-items: center; justify-content: space-between; width: 100%;
558
- padding: 9px 12px; border: none; background: transparent; cursor: pointer; font: inherit;
559
- border-radius: 10px; transition: background 0.12s ease; }
560
- .tl-preview-head:hover { background: rgba(170,170,170,0.06); }
561
- .tl-preview-title { font-size: 11px; font-weight: 600; color: var(--c-text-mut); letter-spacing: 0.01em; }
562
- .tl-preview-chevron { display: inline-flex; color: var(--c-ruler);
569
+ .tl-preview-head { display: flex; align-items: center; justify-content: flex-start; gap: 6px; width: 100%;
570
+ padding: 0 0 9px; border: none; background: transparent; cursor: pointer; font: inherit;
571
+ transition: color 0.12s ease; }
572
+ .tl-preview-head:hover .tl-preview-title,
573
+ .tl-preview-head:hover .tl-preview-chevron { color: #000; }
574
+ .tl-preview-title { font-size: 13px; font-weight: 500; color: #171717; letter-spacing: 0; line-height: 18px; }
575
+ .tl-preview-chevron { display: inline-flex; color: #171717;
563
576
  transform: scaleY(1); transform-origin: center; transition: transform var(--acc-dur) var(--acc-ease); }
564
577
  .tl-preview[data-open="true"] .tl-preview-chevron { transform: scaleY(-1); }
565
578
  .tl-preview-panel { display: grid; grid-template-rows: 0fr;
566
579
  transition: grid-template-rows var(--acc-dur) var(--acc-ease); }
567
580
  .tl-preview[data-open="true"] .tl-preview-panel { grid-template-rows: 1fr; }
568
- .tl-preview-panel-inner { overflow: hidden; opacity: 0; filter: blur(2px); padding: 0 12px 12px;
581
+ .tl-preview-panel-inner { overflow: hidden; opacity: 0; filter: blur(2px);
569
582
  transition: opacity var(--acc-dur) var(--acc-ease), filter var(--acc-dur) var(--acc-ease); }
570
583
  .tl-preview[data-open="true"] .tl-preview-panel-inner { opacity: 1; filter: blur(0); }
571
- .tl-preview-controls { display: flex; justify-content: flex-end; margin-bottom: 10px; }
572
- .tl-preview-btn { display: inline-flex; align-items: center; gap: 5px; height: 22px; padding: 0 8px 0 7px;
573
- border: none; border-radius: 6px; background: var(--c-field-bg); color: var(--c-text-mut2);
574
- font: inherit; font-size: 11px; font-weight: 500; cursor: pointer;
575
- transition: background 0.12s ease, scale 0.12s ease; }
576
- .tl-preview-btn:hover { background: var(--c-sec-h); }
577
- .tl-preview-btn:active { scale: 0.96; }
578
- .tl-preview-track { position: relative; height: 22px; }
579
- .tl-preview-rail { position: absolute; left: 0; right: 0; top: 50%; height: 2px; transform: translateY(-50%);
580
- background: var(--c-track); border-radius: 2px; }
581
- .tl-preview-end { position: absolute; top: 50%; width: 2px; height: 10px; transform: translateY(-50%);
582
- background: #d0d0d6; border-radius: 2px; }
583
- .tl-preview-end.left { left: 0; }
584
- .tl-preview-end.right { right: 0; }
585
- .tl-preview-dot { position: absolute; left: 0; top: 50%; width: 18px; height: 18px; margin-top: -9px;
586
- border-radius: 50%; background: var(--c-blue); border: 2px solid #fff;
587
- box-shadow: 0 2px 6px rgba(0,113,226,0.4), 0 0 0 0.5px rgba(0,0,0,0.05);
584
+ .tl-preview-body { padding: 0; }
585
+ .tl-preview-curve { margin-bottom: 12px; min-height: 162px; display: flex; align-items: center; }
586
+ .tl-preview-sub { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
587
+ .tl-preview-sub-label { font-size: 13px; font-weight: 500; color: #171717; line-height: 18px; }
588
+ .tl-preview-play { border: none; background: transparent; padding: 0; font: inherit; font-size: 13px;
589
+ font-weight: 500; color: var(--c-text-mut); cursor: pointer; line-height: 18px;
590
+ transition: color 0.12s ease; }
591
+ .tl-preview-play:hover { color: #171717; }
592
+ .tl-preview-play:active { color: var(--c-blue); }
593
+ /* position-preview track — Figma 580:9117: dot + dashed rail inside a
594
+ #f6f6f7 rounded card (58px tall, 21px horizontal inset for the dot) */
595
+ .tl-preview-track { height: 58px; background: #f6f6f7; border-radius: 8px;
596
+ display: flex; align-items: center; padding: 0 21px; }
597
+ .tl-preview-rail-wrap { position: relative; flex: 1; height: 28px; }
598
+ .tl-preview-rail { position: absolute; left: 14px; right: 0; top: 50%; height: 0; transform: translateY(-50%);
599
+ border-top: 1.5px dashed #d4d4d8; }
600
+ .tl-preview-dot { position: absolute; left: 0; top: 50%; width: 28px; height: 28px; margin-top: -14px;
601
+ border-radius: 50%; background: var(--c-blue);
602
+ box-shadow: 0 2px 8px rgba(0,113,226,0.28), 0 0 0 0.5px rgba(0,0,0,0.04);
588
603
  will-change: transform; }
589
604
  @media (prefers-reduced-motion: reduce) {
590
605
  .tl-preview-panel, .tl-preview-panel-inner, .tl-preview-chevron { transition: none !important; }
@@ -595,10 +610,10 @@
595
610
  color: #8b8b8b; pointer-events: none; }
596
611
  .tl-menu-group:first-child { padding-top: 4px; }
597
612
 
598
- /* spring params box (reuses bounce row layout) */
599
- .tl-spring-dur { display: flex; align-items: baseline; gap: 6px; font-size: 12px; color: var(--c-text-mut);
600
- padding-top: 10px; border-top: 1px solid var(--c-hairline); margin-top: 0; }
601
- .tl-spring-dur b { color: var(--c-text); font-weight: 600; font-variant-numeric: tabular-nums; }
613
+ /* derived-duration caption under the spring fields (Figma 580:9116 muted) */
614
+ .tl-spring-dur { font-size: 12px; line-height: 16px; color: var(--c-text-faint);
615
+ font-variant-numeric: tabular-nums; }
616
+ .tl-spring-dur b { color: var(--c-text-mut); font-weight: 500; font-variant-numeric: tabular-nums; }
602
617
  .tl-spring-dur .tl-spring-dur-hint { color: var(--c-text-faint); }
603
618
 
604
619
  /* read-only (spring-driven) value field */
@@ -609,7 +624,11 @@
609
624
  .tl-field.is-readonly .tl-field-value { color: var(--c-text-mut); }
610
625
  .tl-field-lock { position: absolute; right: 11px; top: 50%; transform: translateY(-50%);
611
626
  display: flex; color: var(--c-text-faint); pointer-events: none; z-index: 4; }
612
- .tl-field-wrap.is-locked .t-tt { left: 0; transform: translate(0, 4px) scale(var(--tt-scale));
627
+ /* render below the field: the locked Duration field sits at the top of the
628
+ scrollable .tl-inspector (overflow-y:auto), so an above-tooltip gets its
629
+ top clipped. Anchor to top:100% instead and grow downward. */
630
+ .tl-field-wrap.is-locked .t-tt { left: 0; bottom: auto; top: calc(100% + 8px);
631
+ transform: translate(0, -4px) scale(var(--tt-scale));
613
632
  transform-origin: 0 0; white-space: normal; width: 244px; text-align: left; font-size: 12px;
614
633
  line-height: 1.45; padding: 9px 11px; }
615
634
  .tl-field-wrap.is-locked:hover .t-tt { opacity: 1; transform: translate(0, 0) scale(1);
@@ -620,10 +639,11 @@
620
639
  display: flex; align-items: center; justify-content: center;
621
640
  background: #fff; border-radius: 12px; box-shadow: var(--card-shadow); }
622
641
 
623
- /* ── shared dropdown surface (Figma node 580:1696) ──
624
- container: rounded-12 p-8 + menu shadow; rows: h-40 rounded-8 px-12,
625
- Inter Medium 13/16 #1b1b1b label with #979797 suffix; section headers:
626
- Inter Regular 11/14 #8b8b8b, pt-12 pb-8 pl-12, no divider. */
642
+ /* ── shared dropdown surface (Figma node 580:4723) ──
643
+ container: rounded-12 px-6 py-8 + menu shadow; rows: h-32 rounded-8,
644
+ Inter 13/16 #1b1b1b label with #979797 suffix; section headers:
645
+ Inter Regular 11/14 #8b8b8b, pt-12 pb-8 pl-12. Group divider (580:4779)
646
+ is a full-width #f0f0f0 hairline (see .tl-menu-divider). */
627
647
  .tl-menu { background: #fff; border-radius: 12px; padding: 6px; box-shadow: var(--menu-shadow); }
628
648
  /* full-bleed divider: negative side margins cancel the .tl-menu 6px padding so
629
649
  the line spans the whole dropdown width; no top gap. */
@@ -631,8 +651,8 @@
631
651
  .tl-menu-search-input { width: 100%; height: 32px; box-sizing: border-box; border: none; outline: none;
632
652
  background: transparent; font: inherit; font-size: 13px; line-height: 16px; color: #1b1b1b; padding: 0 8px; }
633
653
  .tl-menu-search-input::placeholder { color: #999; }
634
- .tl-menu-item { display: flex; align-items: center; gap: 8px; min-height: 36px; padding: 0 8px;
635
- border-radius: 8px; font-size: 13px; font-weight: 500; line-height: 16px; color: #1b1b1b; cursor: pointer;
654
+ .tl-menu-item { display: flex; align-items: center; gap: 8px; min-height: 32px; padding: 0 8px;
655
+ border-radius: 8px; font-size: 13px; font-weight: 400; line-height: 16px; color: #1b1b1b; cursor: pointer;
636
656
  transition: background 0.1s ease, box-shadow 0.1s ease; }
637
657
  .tl-menu-item:hover { background: #f4f4f5; }
638
658
  .tl-menu-item:active { background: #ededee; }
@@ -645,6 +665,21 @@
645
665
  .tl-menu-empty { padding: 10px 8px; color: var(--c-disabled); font-size: 13px; }
646
666
  .tl-menu-section { padding: 12px 8px 8px; font-size: 11px; font-weight: 400; line-height: 14px;
647
667
  color: #8b8b8b; }
668
+ /* full-bleed group divider (Figma 580:4779) — #f0f0f0 1px hairline spanning
669
+ the whole dropdown width; the -6px sides cancel the .tl-menu 6px padding. */
670
+ .tl-menu-divider { height: 1px; background: #f0f0f0; margin: 6px -6px; }
671
+ /* leading row icon (Figma copy-03 / refresh-cw-03, 16px, #696969 stroke) */
672
+ .tl-menu-icon { display: flex; flex: none; color: #696969; }
673
+ /* dropdown header row (Figma 580:4781) — product name + version, non-clickable.
674
+ pt-12 pb-8 px-12, Inter Regular 11/14; "Refine" reads darker than the rest. */
675
+ .tl-menu-head { display: flex; align-items: center; justify-content: space-between;
676
+ gap: 12px; padding: 10px 12px; font-size: 11px; line-height: 14px;
677
+ font-weight: 400; color: #8b8b8b; white-space: nowrap; pointer-events: none;
678
+ user-select: none; }
679
+ .tl-menu-head-name { color: #4c4c4c; }
680
+ .tl-menu-head-ver { flex: none; line-height: 14px; font-variant-numeric: tabular-nums; }
681
+ /* trailing chevron on link rows (Figma 581:2828 — #696969 right chevron) */
682
+ .tl-menu-chevr { display: flex; flex: none; color: #696969; }
648
683
 
649
684
  /* ═════ transitions.dev — menu dropdown (verbatim) ═════ */
650
685
  .t-dropdown {
@@ -748,6 +783,16 @@
748
783
  }
749
784
  /* below-variant for triggers near the panel top edge */
750
785
  .t-tt.tl-tt-below { bottom: auto; top: calc(100% + 8px); transform-origin: 50% 0; font-size: 12px; }
786
+ /* multi-line message variant (e.g. Accept error) — long text wraps to a
787
+ readable measure instead of one overflowing line. Right-anchored so the
788
+ wide bubble grows leftward and never clips off the panel edge. Mirrors the
789
+ .tl-field-wrap.is-locked .t-tt wrapping pattern. */
790
+ .t-tt.tl-tt-msg { left: auto; right: 0; width: 252px; white-space: normal;
791
+ overflow-wrap: break-word; text-wrap: pretty; text-align: left; line-height: 1.45;
792
+ padding: 9px 11px; transform-origin: 100% 0;
793
+ transform: translate(0, 0) scale(var(--tt-scale)); }
794
+ .t-tt-wrap:hover .t-tt.tl-tt-msg,
795
+ .t-tt-trigger:focus-visible + .t-tt.tl-tt-msg { transform: translate(0, 0) scale(1); }
751
796
 
752
797
  /* ═════ transitions.dev — panel reveal (per-phase distance/ease/scale) ═════ */
753
798
  /* base = closed / close-end state: resolved travel + scale come from the
@@ -837,6 +882,9 @@
837
882
  /* must sit above the header (z-index:40) so it overlays the whole panel */
838
883
  position: absolute; top: 0; right: 0; bottom: 0; z-index: 50;
839
884
  width: var(--refine-w, 360px); max-width: 100%; overflow: hidden;
885
+ /* shadow lives on the panel (not the inner) so its own overflow:hidden
886
+ doesn't clip it; the left edge then casts over the timeline */
887
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08), 0 2px 6px 0 rgba(0, 0, 0, 0.05), 0 4px 42px 0 rgba(0, 0, 0, 0.06);
840
888
  --panel-translate-x: var(--panel-close-distance);
841
889
  --panel-scale-now: var(--panel-scale-close);
842
890
  transform: translateX(calc(100% + var(--panel-translate-x))) scale(var(--panel-scale-now));
@@ -871,14 +919,12 @@
871
919
  width: var(--refine-w, 360px); height: 100%; box-sizing: border-box;
872
920
  display: flex; flex-direction: column;
873
921
  background: #fff; color: var(--c-text);
874
- border-left: 1px solid #f0f0f0;
875
- box-shadow: -10px 0 30px rgba(0,0,0,0.05);
876
922
  font-family: "Inter", system-ui, -apple-system, sans-serif;
877
923
  }
878
924
  /* header: title block (left) + Agent/Deterministic mode dropdown (right) */
879
- /* match the timeline top bar exactly: 10px/14px padding, centered, 57px tall (36px row + 20 + 1px border) */
925
+ /* match the timeline top bar exactly: 7.5px/14px padding, centered, 52px tall (36px row + 15 + 1px border) */
880
926
  .tl-refine-head { flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between;
881
- gap: 8px; box-sizing: border-box; padding: 10px 14px;
927
+ gap: 8px; box-sizing: border-box; padding: 7.5px 14px;
882
928
  border-bottom: 1px solid #f0f0f0; }
883
929
  .tl-refine-titles { min-width: 0; }
884
930
  .tl-refine-titles h3 { font-size: 13px; font-weight: 500; line-height: 14px; color: #17181c; }
@@ -939,26 +985,46 @@
939
985
  .tl-scan-morph { position: relative; isolation: isolate; box-sizing: border-box;
940
986
  display: inline-flex; align-items: center; justify-content: center;
941
987
  margin-bottom: 20px;
942
- width: var(--scan-idle-w, 122px); height: 35px; padding: 0 16px;
988
+ width: var(--scan-idle-w, 122px); height: 36px; padding: 0 12px;
943
989
  border: none; border-radius: 60px; cursor: pointer;
944
- font: inherit; font-size: 13px; font-weight: 500; line-height: 14px; color: #0073e5;
945
- background: rgba(0,115,229,0.04); text-shadow: 0 1px 3px rgba(0,0,0,0.04);
946
- box-shadow: var(--drop-btn), var(--ring-blue);
990
+ font: inherit; font-size: 13px; font-weight: 500; line-height: 14px;
991
+ color: var(--c-blue); background: transparent;
992
+ box-shadow: var(--drop-btn);
993
+ white-space: nowrap;
947
994
  --resize-dur: 300ms; --resize-ease: cubic-bezier(0.22, 1, 0.36, 1);
948
995
  transition: width var(--resize-dur) var(--resize-ease),
949
996
  height var(--resize-dur) var(--resize-ease),
950
997
  border-radius var(--resize-dur) var(--resize-ease),
951
- background var(--resize-dur) var(--resize-ease),
952
998
  color var(--resize-dur) var(--resize-ease),
953
999
  box-shadow var(--resize-dur) var(--resize-ease); }
1000
+ .tl-scan-morph::before { content: ""; position: absolute; inset: 0; border-radius: inherit;
1001
+ background: var(--c-blue-bg);
1002
+ background: color(display-p3 0 0.451 0.898 / 0.06);
1003
+ transition: background 0.12s ease; z-index: 0; }
1004
+ .tl-scan-morph::after { content: ""; position: absolute; inset: 0; border-radius: inherit;
1005
+ box-shadow: var(--ring-blue);
1006
+ pointer-events: none; z-index: 2; }
954
1007
  /* explicit refine-footer spacing for both idle + scanning states */
955
1008
  .tl-refine-foot .tl-scan-morph { margin-bottom: 20px; }
956
- .tl-scan-morph:hover:not(:disabled) { background: rgba(0,115,229,0.08); }
957
- .tl-scan-morph:active:not(.is-scanning):not(:disabled) { scale: 0.98; }
958
- .tl-scan-morph:disabled:not(.is-scanning) { cursor: default; opacity: 0.45; }
1009
+ .tl-scan-morph:hover:not(:disabled):not(.is-scanning)::before {
1010
+ background: var(--c-blue-bg-h); background: color(display-p3 0 0.451 0.898 / 0.10); }
1011
+ .tl-scan-morph:active:not(.is-scanning):not(:disabled) { color: var(--c-blue-pressed); }
1012
+ .tl-scan-morph:active:not(.is-scanning):not(:disabled)::before {
1013
+ background: var(--c-blue-bg-h); background: color(display-p3 0 0.451 0.898 / 0.10); }
1014
+ .tl-scan-morph:disabled:not(.is-scanning) { cursor: default; color: var(--c-blue); }
1015
+ .tl-scan-morph:disabled:not(.is-scanning)::before {
1016
+ background: var(--c-blue-bg); background: color(display-p3 0 0.451 0.898 / 0.06); }
1017
+ .tl-scan-morph:disabled:not(.is-scanning) .tl-scan-label {
1018
+ color: var(--c-blue-pressed); opacity: 0.5; }
959
1019
  .tl-scan-morph.is-scanning { width: 100%; height: 49px; border-radius: 60px;
960
- padding: 8px 16px; cursor: default; color: #676767; background: #fff;
961
- text-shadow: none; box-shadow: inset 0 0 0 1px #f0f0f0; }
1020
+ padding: 8px 16px; cursor: default; color: #676767; background: transparent;
1021
+ box-shadow: none; }
1022
+ .tl-scan-morph.is-scanning::before {
1023
+ background: #fff;
1024
+ transition: background var(--resize-dur) var(--resize-ease); }
1025
+ .tl-scan-morph.is-scanning::after {
1026
+ box-shadow: inset 0 0 0 1px #f0f0f0;
1027
+ transition: box-shadow var(--resize-dur) var(--resize-ease); }
962
1028
  /* Two faces share one grid cell and cross-blur (icon swap, 09) between the
963
1029
  label and the icon+status; whichever is hidden fades out with blur+scale.
964
1030
  overflow:hidden + nowrap keep the status copy on a single line while the
@@ -987,13 +1053,79 @@
987
1053
  beam stays an absolute overlay filling the button instead of collapsing
988
1054
  into flow as a flex sibling (which hid the beam and skewed the width). */
989
1055
  .tl-scan-morph .tl-scan-beam { position: absolute; inset: 0; z-index: 0;
990
- border-radius: 60px; pointer-events: none;
1056
+ border-radius: inherit; pointer-events: none;
991
1057
  animation: tl-fade-in 360ms var(--resize-ease) both; }
992
- .tl-scan-beam-fill { display: block; width: 100%; height: 100%; border-radius: 10px; }
1058
+ /* Beam radius is auto-detected from this fill child; inherit keeps it locked
1059
+ to the button's own border-radius so the hairline traces the edge at any
1060
+ size (idle pill ↔ scanning rect) without a hardcoded magic number. */
1061
+ .tl-scan-beam-fill { display: block; width: 100%; height: 100%; border-radius: inherit; }
993
1062
  @keyframes tl-fade-in { from { opacity: 0; } to { opacity: 1; } }
994
- .tl-scan-ic { flex: 0 0 auto; width: 13px; height: 15px; color: #c4c4c4;
995
- animation: tl-scan-spin 1.8s linear infinite; }
996
- @keyframes tl-scan-spin { to { transform: rotate(360deg); } }
1063
+ /* ── gated states (Figma 580:3595) the panel is blocked until a live agent
1064
+ is connected (/refine live) AND it has scanned the page's transitions.
1065
+ Both screens center their content in the panel body. */
1066
+ .tl-gate { flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center;
1067
+ padding: 24px 16px; animation: tl-fade-in 260ms var(--resize-ease, cubic-bezier(0.22,1,0.36,1)) both; }
1068
+ .tl-gate-col { display: flex; flex-direction: column; align-items: center; text-align: center; }
1069
+ /* "Before we start" */
1070
+ .tl-gate-title { font-size: 15px; font-weight: 500; line-height: 21px; color: #2c2c2c;
1071
+ text-wrap: balance; }
1072
+ .tl-gate-text { margin: 4px 0 0; max-width: 250px; font-size: 13px; font-weight: 400; line-height: 21px;
1073
+ color: #676767; text-wrap: pretty; }
1074
+ .tl-gate-text .tl-code { color: #4b4b4b; }
1075
+ /* "Agent is scanning your transitions" — pill with dot-matrix loader + a
1076
+ line-style border beam tracing the capsule edge, with a subtext below. */
1077
+ .tl-gate-pill-wrap { position: relative; }
1078
+ .tl-gate-pill { position: relative; z-index: 1; display: inline-flex; align-items: center; gap: 6px;
1079
+ height: 36px; padding: 6px 12px; border-radius: 36px; background: #fff; color: #17181c;
1080
+ font-size: 13px; font-weight: 400; line-height: 14px; white-space: nowrap;
1081
+ box-shadow: inset 0 0 0 1px rgba(0,0,0,0.06), inset 0 0 0 1px rgba(196,196,196,0.10); }
1082
+ .tl-gate-pill .tl-dotm { color: #17181c; }
1083
+ .tl-gate-pill-wrap .tl-gate-beam { position: absolute; inset: 0; z-index: 0; border-radius: 36px;
1084
+ pointer-events: none; animation: tl-fade-in 360ms var(--resize-ease, cubic-bezier(0.22,1,0.36,1)) both; }
1085
+ .tl-gate-beam-fill { display: block; width: 100%; height: 100%; border-radius: 36px; background: #fff; }
1086
+ .tl-gate-sub { margin: 16px 0 0; max-width: 244px; font-size: 12px; font-weight: 400; line-height: 14px;
1087
+ color: #6f6f6f; text-wrap: pretty; }
1088
+ /* recovery actions when a scan errored/timed out — never trap the panel */
1089
+ .tl-gate-actions { display: flex; align-items: center; gap: 8px; margin: 18px 0 0; }
1090
+ .tl-gate-btn { height: 32px; padding: 0 14px; border: 0; border-radius: 60px; cursor: pointer;
1091
+ font: 500 12px/14px inherit; color: #2c2c2c; background: #f3f3f3; white-space: nowrap;
1092
+ box-shadow: 0 1px 3px 0 rgba(0,0,0,0.04), inset 0 0 0 1px rgba(0,0,0,0.06), inset 0 -1px 0 0 rgba(0,0,0,0.10);
1093
+ transition: background 140ms cubic-bezier(0.4,0,0.2,1); }
1094
+ .tl-gate-btn:hover { background: #ececec; }
1095
+ .tl-gate-btn:active { background: #e6e6e6; }
1096
+ .tl-gate-btn-primary { color: #fff; background: #0a84ff;
1097
+ box-shadow: 0 0 0 1px rgba(0,101,208,0.10) inset, 0 -1px 0 0 rgba(3,66,142,0.15) inset, 0 1px 3px 0 rgba(4,41,117,0.08); }
1098
+ .tl-gate-btn-primary:hover { background: #0a78e6; }
1099
+ @media (prefers-reduced-motion: reduce) {
1100
+ .tl-gate, .tl-gate-pill-wrap .tl-gate-beam { animation: none !important; }
1101
+ .tl-gate-btn { transition: none !important; } }
1102
+ /* dot-matrix loader — ported from @dotmatrix/dotm-square-14
1103
+ (github.com/zzzzshawn/matrix, MIT). A 5×5 dot grid cross-fades through four
1104
+ frame masks in the sequence 0→1→2→3→2→1, dot opacities x=1 / o=0.52 / .=0.08.
1105
+ The original drives this with a JS stepped cycle; here each symmetry group of
1106
+ dots owns one opacity @keyframes over a single 1.36s loop (no JS timer). */
1107
+ .tl-dotm { flex: 0 0 auto; display: grid; width: 16px; height: 16px;
1108
+ grid-template-columns: repeat(5, 1fr); grid-template-rows: repeat(5, 1fr);
1109
+ gap: 1px; }
1110
+ .tl-dotm-dot { align-self: center; justify-self: center; width: 100%; height: 100%;
1111
+ border-radius: 50%; background: currentColor; opacity: 0.08;
1112
+ animation-duration: 1.36s; animation-timing-function: linear; animation-iteration-count: infinite; }
1113
+ .tl-dotm-a { animation-name: tl-dotm-a; }
1114
+ .tl-dotm-b { animation-name: tl-dotm-b; }
1115
+ .tl-dotm-c { animation-name: tl-dotm-c; }
1116
+ .tl-dotm-d { animation-name: tl-dotm-d; }
1117
+ .tl-dotm-e { animation-name: tl-dotm-e; }
1118
+ .tl-dotm-f { animation-name: tl-dotm-f; }
1119
+ .tl-dotm-g { animation-name: tl-dotm-g; }
1120
+ .tl-dotm-h { opacity: 0.52; }
1121
+ @keyframes tl-dotm-a { 0%{opacity:1} 16.667%{opacity:.08} 33.333%{opacity:.08} 50%{opacity:1} 66.667%{opacity:.08} 83.333%{opacity:.08} 100%{opacity:1} }
1122
+ @keyframes tl-dotm-b { 0%{opacity:.08} 16.667%{opacity:.08} 33.333%{opacity:1} 50%{opacity:.08} 66.667%{opacity:1} 83.333%{opacity:.08} 100%{opacity:.08} }
1123
+ @keyframes tl-dotm-c { 0%{opacity:.08} 16.667%{opacity:1} 33.333%{opacity:.08} 50%{opacity:1} 66.667%{opacity:.08} 83.333%{opacity:1} 100%{opacity:.08} }
1124
+ @keyframes tl-dotm-d { 0%{opacity:.08} 16.667%{opacity:1} 33.333%{opacity:1} 50%{opacity:.08} 66.667%{opacity:1} 83.333%{opacity:1} 100%{opacity:.08} }
1125
+ @keyframes tl-dotm-e { 0%{opacity:1} 16.667%{opacity:.52} 33.333%{opacity:.08} 50%{opacity:.52} 66.667%{opacity:.08} 83.333%{opacity:.52} 100%{opacity:1} }
1126
+ @keyframes tl-dotm-f { 0%{opacity:.08} 16.667%{opacity:1} 33.333%{opacity:.52} 50%{opacity:.08} 66.667%{opacity:.52} 83.333%{opacity:1} 100%{opacity:.08} }
1127
+ @keyframes tl-dotm-g { 0%{opacity:.08} 16.667%{opacity:.52} 33.333%{opacity:.08} 50%{opacity:.08} 66.667%{opacity:.08} 83.333%{opacity:.52} 100%{opacity:.08} }
1128
+ .tl-scanning-row { display: inline-flex; align-items: center; gap: 6px; }
997
1129
  /* loading status text — transitions.dev shimmer text (15) + text states swap (04) */
998
1130
  .tl-refine-status-text {
999
1131
  --shimmer-dur: 2000ms; --shimmer-base: #9a9a9a; --shimmer-highlight: #17181c;
@@ -1029,42 +1161,106 @@
1029
1161
  .tl-mode-row-check { flex: 0 0 auto; width: 16px; height: 16px; display: flex; color: #17181c; }
1030
1162
  @keyframes tl-fade-up { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
1031
1163
 
1032
- /* suggestion cards */
1033
- .tl-refine-summary { font-size: 12px; color: var(--c-text-mut); margin-bottom: 2px; }
1034
- .tl-sug { border: 1px solid var(--c-line); border-radius: 10px; padding: 12px 13px;
1035
- display: flex; flex-direction: column; gap: 8px; background: #fff;
1036
- box-shadow: 0 1px 2px rgba(0,0,0,0.03); }
1037
- .tl-sug-top { display: flex; align-items: center; gap: 8px; }
1038
- .tl-sug-kind { font-size: 10px; font-weight: 600; letter-spacing: 0.4px; text-transform: uppercase;
1039
- color: var(--c-blue); background: color(display-p3 0 0.451 0.898 / 0.08); padding: 2px 7px; border-radius: 5px; }
1040
- .tl-sug-prop { font-size: 12px; font-weight: 600; color: #171717; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
1041
- .tl-sug-title { font-size: 13px; font-weight: 600; color: #171717; }
1042
- .tl-sug-delta { font-size: 12px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1043
- display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
1044
- .tl-sug-from { color: var(--c-text-faint); text-decoration: line-through; }
1045
- .tl-sug-arrow { color: var(--c-text-faint); }
1046
- .tl-sug-to { color: var(--c-blue); font-weight: 600; }
1047
- .tl-sug-reason { font-size: 12px; color: var(--c-text-mut); line-height: 1.45; text-wrap: balance; }
1048
- .tl-sug-apply { align-self: flex-start; font: inherit; font-size: 12px; font-weight: 500;
1049
- padding: 6px 12px; border-radius: 7px; border: none; cursor: pointer;
1050
- background: #171717; color: #fff; transition: background 0.12s ease, scale 0.12s ease; }
1051
- .tl-sug-apply:hover { background: #000; }
1052
- .tl-sug-apply:active { scale: 0.97; }
1053
- .tl-sug-apply.is-applied { background: color(display-p3 0 0.451 0.898 / 0.10); color: var(--c-blue);
1054
- cursor: default; display: flex; align-items: center; gap: 6px; }
1164
+ /* suggestion cards (Figma node 580:9158) */
1165
+ .tl-sug { box-sizing: border-box; display: flex; flex-direction: column; gap: 10px;
1166
+ padding: 10px 12px 12px; background: #fff; border-radius: 12px; overflow: clip;
1167
+ box-shadow: 0 0 0 1px rgba(0,0,0,0.08); }
1168
+ /* top row: member chip + property (or a single heading for replace cards) */
1169
+ .tl-sug-head { display: flex; align-items: center; gap: 8px; min-height: 20px; }
1170
+ .tl-sug-member { flex: none; display: inline-flex; align-items: center; height: 20px; padding: 0 6px;
1171
+ border-radius: 50px; background: #fff; box-shadow: 0 0 0 1px rgba(0,0,0,0.08);
1172
+ font-size: 11px; font-weight: 400; line-height: 18px; color: #696969; white-space: nowrap; }
1173
+ .tl-sug-prop { font-size: 13px; font-weight: 500; line-height: 18px; color: #171717; white-space: nowrap; }
1174
+ /* full-bleed hairline (Figma 581:5970) — cancels the 12px card side padding */
1175
+ .tl-sug-divider { height: 1px; background: #f1f1f1; margin: 0 -12px; }
1176
+ /* colored category chip — color set by kind modifier below */
1177
+ .tl-sug-cat { align-self: flex-start; display: inline-flex; align-items: center; height: 24px;
1178
+ padding: 0 10px; border-radius: 50px; font-size: 13px; font-weight: 500; line-height: 16px;
1179
+ white-space: nowrap; }
1180
+ .tl-sug-cat.is-easing { color: #a830fd; background: rgba(173,68,247,0.06); box-shadow: inset 0 0 0 1px rgba(173,68,247,0.18); }
1181
+ .tl-sug-cat.is-duration { color: #3097fd; background: rgba(111,183,255,0.08); box-shadow: inset 0 0 0 1px rgba(48,151,253,0.18); }
1182
+ .tl-sug-cat.is-scale { color: #26953b; background: rgba(38,149,59,0.06); box-shadow: inset 0 0 0 1px rgba(38,149,59,0.18); }
1183
+ .tl-sug-cat.is-distance, .tl-sug-cat.is-delay { color: #c35c00; background: rgba(247,152,68,0.06); box-shadow: inset 0 0 0 1px rgba(205,115,36,0.18); }
1184
+ .tl-sug-cat.is-replace { color: #3097fd; background: rgba(111,183,255,0.08); box-shadow: inset 0 0 0 1px rgba(48,151,253,0.18); }
1185
+ /* content stack */
1186
+ .tl-sug-body { display: flex; flex-direction: column; gap: 8px; width: 100%; }
1187
+ .tl-sug-text { display: flex; flex-direction: column; gap: 8px; width: 100%; }
1188
+ .tl-sug-title { font-size: 12px; font-weight: 500; line-height: 16px; color: #1b1b1b; }
1189
+ /* before/after delta — two stacked Roboto Mono lines (Figma 581:4469) */
1190
+ .tl-sug-delta { display: flex; flex-direction: column;
1191
+ font-family: "Roboto Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
1192
+ font-size: 13px; font-weight: 400; line-height: 16px; }
1193
+ .tl-sug-from { color: #7b7b7b; text-decoration: line-through; }
1194
+ .tl-sug-to { color: #1b1b1b; }
1195
+ .tl-sug-reason { font-size: 12px; font-weight: 400; line-height: 18px; color: #6d6d6d; }
1196
+ /* shared white pill button (Figma 580:757) — card Apply + bottom-bar actions */
1197
+ .tl-pill-btn { align-self: flex-start; box-sizing: border-box; display: inline-flex; align-items: center;
1198
+ justify-content: center; gap: 8px; height: 32px; padding: 6px 12px; border: none; cursor: pointer;
1199
+ border-radius: 36px; background: #fff;
1200
+ font: inherit; font-size: 13px; font-weight: 500; line-height: 14px; color: #2b2b2b; white-space: nowrap;
1201
+ box-shadow: 0 1px 3px 0 rgba(0,0,0,0.04),
1202
+ inset 0 0 0 1px rgba(0,0,0,0.06), inset 0 -1px 0 0 rgba(0,0,0,0.1), inset 0 0 0 1px rgba(196,196,196,0.1);
1203
+ transition: background 0.12s ease, scale 0.12s ease, opacity 0.12s ease; }
1204
+ .tl-pill-btn .tl-dim { color: #696969; }
1205
+ .tl-pill-btn:hover:not(:disabled) { background: #fafafa; }
1206
+ .tl-pill-btn:active:not(:disabled) { scale: 0.98; }
1207
+ .tl-pill-btn:disabled { opacity: 0.45; cursor: default; }
1208
+ /* applied state — GRAY, never green (matches Accept icon --c-text #17181c) */
1209
+ .tl-pill-btn.is-applied { color: var(--c-text); cursor: default; }
1210
+ .tl-pill-btn.is-applied svg path { stroke: var(--c-text); }
1211
+ /* card resize (transitions.dev 01-card-resize): width tween on Apply -> Applied.
1212
+ 250ms cubic-bezier(0.22,1,0.36,1); width is driven explicitly from JS. */
1213
+ .tl-pill-btn.tl-apply-btn {
1214
+ transition: background 0.12s ease, scale 0.12s ease, opacity 0.12s ease,
1215
+ width 250ms cubic-bezier(0.22, 1, 0.36, 1);
1216
+ will-change: width; }
1217
+ /* success check (transitions.dev 10-success-check): fade + rotate + Y-bob +
1218
+ blur on the wrapper, stroke-draw on the path. GRAY stroke. */
1219
+ .tl-sug-check { display: inline-flex; transform-origin: center; opacity: 0;
1220
+ will-change: transform, opacity, filter; }
1221
+ .tl-sug-check svg { display: block; overflow: visible; }
1222
+ /* stroke-dasharray = path.getTotalLength() of ICONS.accept "M4 8.4268 L6.46155
1223
+ 11.19223 L12 4.97001" (~12.03 user units, rounded up to 13). JS also sets it
1224
+ inline per-render via getTotalLength() for sub-pixel safety. */
1225
+ .tl-sug-check svg path { stroke-dasharray: 13; stroke-dashoffset: 13; }
1226
+ .tl-sug-check[data-state="in"] {
1227
+ animation:
1228
+ tl-check-fade 500ms cubic-bezier(0.22, 1, 0.36, 1) forwards,
1229
+ tl-check-rotate 500ms cubic-bezier(0.22, 1, 0.36, 1) forwards,
1230
+ tl-check-blur 500ms cubic-bezier(0.22, 1, 0.36, 1) forwards,
1231
+ tl-check-bob 500ms cubic-bezier(0.34, 1.35, 0.64, 1) forwards; }
1232
+ .tl-sug-check[data-state="in"] svg path {
1233
+ animation: tl-check-draw 500ms cubic-bezier(0.22, 1, 0.36, 1) 80ms forwards; }
1234
+ @keyframes tl-check-fade { from { opacity: 0; } to { opacity: 1; } }
1235
+ @keyframes tl-check-rotate { from { transform: rotate(80deg); } to { transform: rotate(0deg); } }
1236
+ @keyframes tl-check-blur { from { filter: blur(10px); } to { filter: blur(0); } }
1237
+ @keyframes tl-check-bob { from { translate: 0 40px; } to { translate: 0 0; } }
1238
+ @keyframes tl-check-draw { to { stroke-dashoffset: 0; } }
1239
+ /* results action bar (Figma node 581:6285): white sticky bar with top hairline
1240
+ holding two equal-width pill buttons (Apply all + Scan again). */
1241
+ .tl-refine-foot.is-bar { padding: 8px 16px; gap: 8px; justify-content: stretch;
1242
+ background: #fff; border-top: 1px solid #f0f0f0; }
1243
+ .tl-refine-foot.is-bar .tl-pill-btn { flex: 1; align-self: stretch; }
1055
1244
  .tl-refine-empty { font-size: 13px; color: var(--c-text-mut); padding: 18px 4px; text-align: center; line-height: 1.5; text-wrap: balance; }
1056
1245
  .tl-refine-error { font-size: 13px; color: #c0392b; padding: 14px; line-height: 1.5;
1057
1246
  background: rgba(192,57,43,0.06); border-radius: 10px; }
1058
1247
  .tl-refine-error code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
1059
1248
  .tl-refine-foot { padding: 12px 20px; display: flex; gap: 8px; }
1249
+ /* results action bar (Figma 581:6285) — sticky white bar, top hairline, two equal pills */
1250
+ .tl-refine-foot.is-bar { padding: 8px 16px; gap: 8px; justify-content: stretch;
1251
+ background: #fff; border-top: 1px solid #f0f0f0; }
1252
+ .tl-refine-foot.is-bar .tl-pill-btn { flex: 1; align-self: stretch; }
1060
1253
  @media (prefers-reduced-motion: reduce) {
1061
1254
  .tl-refine-panel { transition: none; }
1062
- .tl-scan-ic { animation: none; }
1255
+ .tl-dotm-dot { animation: none !important; }
1063
1256
  .tl-refine-status-text, .tl-sug { animation: none; }
1064
1257
  .t-shimmer::before { animation: none !important; }
1065
1258
  .t-text-swap { transition: none !important; }
1066
1259
  .tl-refine-results .t-stagger-line { transition: none !important; }
1067
1260
  .t-resize, .tl-scan-morph { transition: none !important; }
1261
+ .tl-pill-btn.tl-apply-btn { transition: background 0.12s ease, scale 0.12s ease, opacity 0.12s ease !important; }
1262
+ .tl-sug-check { animation: none !important; opacity: 1; }
1263
+ .tl-sug-check svg path { animation: none !important; stroke-dashoffset: 0 !important; }
1068
1264
  .tl-scan-face { transition: none !important; }
1069
1265
  .tl-scan-beam { animation: none !important; }
1070
1266
  .tl-pill { animation: none !important; }
@@ -1090,6 +1286,12 @@
1090
1286
  const { createElement: h, useState, useEffect, useLayoutEffect, useRef, useMemo,
1091
1287
  useCallback, useSyncExternalStore, createContext, useContext } = React;
1092
1288
 
1289
+ // Single source of truth for the displayed version. When served via the relay
1290
+ // (inject.js), the build injects window.__TX_REFINE_VERSION from package.json so
1291
+ // it never drifts; the fallback mirrors the current package.json version for the
1292
+ // standalone demo.html.
1293
+ const REFINE_VERSION = (typeof window !== "undefined" && window.__TX_REFINE_VERSION) || "0.3.5";
1294
+
1093
1295
  // ── helpers ──
1094
1296
  function parseCssTime(v) { const t=v.trim(); if(t.endsWith("ms")) return parseFloat(t); if(t.endsWith("s")) return parseFloat(t)*1000; const n=parseFloat(t); return isNaN(n)?0:n; }
1095
1297
  function formatCssTime(ms) { if(ms>=1000&&ms%1000===0) return `${ms/1000}s`; if(ms>=100) return `${ms/1000}s`; return `${ms}ms`; }
@@ -1366,8 +1568,8 @@
1366
1568
  play: {vb:"0 0 10 12", svg:`<path d="M9.49453 6.92883L1.54117 11.8508C0.866207 12.2681 0 11.7628 0 10.9216V1.07765C0 0.237764 0.864957 -0.268832 1.54117 0.149776L9.49453 5.07175C9.64807 5.16524 9.7757 5.30037 9.86447 5.46344C9.95324 5.62651 10 5.81172 10 6.00029C10 6.18885 9.95324 6.37407 9.86447 6.53714C9.7757 6.70021 9.64807 6.83534 9.49453 6.92883Z" fill="currentColor"/>`},
1367
1569
  pause: {vb:"0 0 24 24", svg:`<path d="M5.75 3C4.7835 3 4 3.7835 4 4.75V19.25C4 20.2165 4.7835 21 5.75 21H8.25C9.2165 21 10 20.2165 10 19.25V4.75C10 3.7835 9.2165 3 8.25 3H5.75Z" fill="currentColor"/><path d="M15.75 3C14.7835 3 14 3.7835 14 4.75V19.25C14 20.2165 14.7835 21 15.75 21H18.25C19.2165 21 20 20.2165 20 19.25V4.75C20 3.7835 19.2165 3 18.25 3H15.75Z" fill="currentColor"/>`},
1368
1570
  stop: {vb:"0 0 24 24", svg:`<path fill-rule="evenodd" clip-rule="evenodd" d="M5.32378 3C5.3325 3 5.34124 3 5.35 3L18.6762 3C18.9337 2.99998 19.1702 2.99997 19.3679 3.01612C19.581 3.03353 19.8142 3.07339 20.0445 3.19074C20.3738 3.35852 20.6415 3.62624 20.8093 3.95552C20.9266 4.18583 20.9665 4.419 20.9839 4.63213C21 4.82981 21 5.06629 21 5.32377V18.6762C21 18.9337 21 19.1702 20.9839 19.3679C20.9665 19.581 20.9266 19.8142 20.8093 20.0445C20.6415 20.3738 20.3738 20.6415 20.0445 20.8093C19.8142 20.9266 19.581 20.9665 19.3679 20.9839C19.1702 21 18.9337 21 18.6762 21H5.32377C5.06629 21 4.82981 21 4.63213 20.9839C4.419 20.9665 4.18583 20.9266 3.95552 20.8093C3.62624 20.6415 3.35852 20.3738 3.19074 20.0445C3.07339 19.8142 3.03353 19.581 3.01612 19.3679C2.99997 19.1702 2.99998 18.9337 3 18.6762L3 5.35C3 5.34124 3 5.3325 3 5.32379C2.99998 5.0663 2.99997 4.82982 3.01612 4.63213C3.03353 4.419 3.07339 4.18583 3.19074 3.95552C3.35852 3.62624 3.62624 3.35852 3.95552 3.19074C4.18583 3.07339 4.419 3.03353 4.63213 3.01612C4.82982 2.99997 5.0663 2.99998 5.32378 3Z" fill="currentColor"/>`},
1369
- restart: {vb:"0 0 16 16", svg:`<path d="M1.33333 6.66667C1.33333 6.66667 2.66999 4.84548 3.75589 3.75883C4.84179 2.67218 6.3424 2 8 2C11.3137 2 14 4.68629 14 8C14 11.3137 11.3137 14 8 14C5.2646 14 2.95674 12.1695 2.23451 9.66667M5.33333 6.66667H1.33333V2.66667" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`},
1370
- copy: {vb:"0 0 16 16", svg:`<path d="M5.3335 5.33325V3.46659C5.3335 2.71985 5.3335 2.34648 5.47882 2.06126C5.60665 1.81038 5.81063 1.60641 6.06151 1.47858C6.34672 1.33325 6.72009 1.33325 7.46683 1.33325H12.5335C13.2802 1.33325 13.6536 1.33325 13.9388 1.47858C14.1897 1.60641 14.3937 1.81038 14.5215 2.06126C14.6668 2.34648 14.6668 2.71985 14.6668 3.46659V8.53325C14.6668 9.27999 14.6668 9.65336 14.5215 9.93857C14.3937 10.1895 14.1897 10.3934 13.9388 10.5213C13.6536 10.6666 13.2802 10.6666 12.5335 10.6666H10.6668M3.46683 14.6666H8.5335C9.28023 14.6666 9.6536 14.6666 9.93882 14.5213C10.1897 14.3934 10.3937 14.1895 10.5215 13.9386C10.6668 13.6534 10.6668 13.28 10.6668 12.5333V7.46658C10.6668 6.71985 10.6668 6.34648 10.5215 6.06126C10.3937 5.81038 10.1897 5.60641 9.93882 5.47858C9.6536 5.33325 9.28023 5.33325 8.5335 5.33325H3.46683C2.72009 5.33325 2.34672 5.33325 2.06151 5.47858C1.81063 5.60641 1.60665 5.81038 1.47882 6.06126C1.3335 6.34648 1.3335 6.71985 1.3335 7.46658V12.5333C1.3335 13.28 1.3335 13.6534 1.47882 13.9386C1.60665 14.1895 1.81063 14.3934 2.06151 14.5213C2.34672 14.6666 2.72009 14.6666 3.46683 14.6666Z" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>`},
1571
+ restart: {vb:"0 0 16 16", svg:`<g transform="translate(1.75,1.25)"><path d="M7.47222 12.75C7.47222 12.75 7.9912 12.6772 10.1391 10.5684C12.287 8.45955 12.287 5.04046 10.1391 2.93162C9.37809 2.18446 8.44956 1.70202 7.47222 1.4843M7.47222 9.15L7.47222 12.75H11.1389M5.02778 0.750117C5.02778 0.750117 4.5088 0.82291 2.36091 2.93174C0.213029 5.04057 0.213029 8.45966 2.36091 10.5685C3.12191 11.3157 4.05044 11.7981 5.02778 12.0158M5.02778 4.35L5.02778 0.750117L1.36111 0.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></g>`},
1572
+ copy: {vb:"0 0 16 16", svg:`<path d="M5.6 5.6V3.92C5.6 3.24794 5.6 2.91191 5.73079 2.65521C5.84584 2.42942 6.02942 2.24584 6.25521 2.13079C6.51191 2 6.84794 2 7.52 2H12.08C12.7521 2 13.0881 2 13.3448 2.13079C13.5706 2.24584 13.7542 2.42942 13.8692 2.65521C14 2.91191 14 3.24794 14 3.92V8.48C14 9.15206 14 9.4881 13.8692 9.74479C13.7542 9.97058 13.5706 10.1542 13.3448 10.2692C13.0881 10.4 12.7521 10.4 12.08 10.4H10.4M3.92 14H8.48C9.15206 14 9.48809 14 9.74479 13.8692C9.97058 13.7542 10.1542 13.5706 10.2692 13.3448C10.4 13.0881 10.4 12.7521 10.4 12.08V7.52C10.4 6.84794 10.4 6.51191 10.2692 6.25521C10.1542 6.02942 9.97058 5.84584 9.74479 5.73079C9.48809 5.6 9.15206 5.6 8.48 5.6H3.92C3.24794 5.6 2.91191 5.6 2.65521 5.73079C2.42942 5.84584 2.24584 6.02942 2.13079 6.25521C2 6.51191 2 6.84794 2 7.52V12.08C2 12.7521 2 13.0881 2.13079 13.3448C2.24584 13.5706 2.42942 13.7542 2.65521 13.8692C2.91191 14 3.24794 14 3.92 14Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`},
1371
1573
  check: {vb:"0 0 16 16", svg:`<path d="M3 7.88889L5.76923 11L12 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>`},
1372
1574
  chevron: {vb:"0 0 16 16", svg:`<path fill-rule="evenodd" clip-rule="evenodd" d="M4.46967 6.46967C4.76256 6.17678 5.23744 6.17678 5.53033 6.46967L8 8.93934L10.4697 6.46967C10.7626 6.17678 11.2374 6.17678 11.5303 6.46967C11.8232 6.76256 11.8232 7.23744 11.5303 7.53033L8.53033 10.5303C8.23744 10.8232 7.76256 10.8232 7.46967 10.5303L4.46967 7.53033C4.17678 7.23744 4.17678 6.76256 4.46967 6.46967Z" fill="currentColor"/>`},
1373
1575
  gear: {vb:"0 0 16 16", svg:`<path d="M8.00016 9.99992C9.10473 9.99992 10.0002 9.10449 10.0002 7.99992C10.0002 6.89535 9.10473 5.99992 8.00016 5.99992C6.89559 5.99992 6.00016 6.89535 6.00016 7.99992C6.00016 9.10449 6.89559 9.99992 8.00016 9.99992Z" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/><path d="M12.485 9.8181C12.4043 10.0009 12.3803 10.2037 12.4159 10.4003C12.4516 10.5969 12.5453 10.7783 12.685 10.9211L12.7214 10.9575C12.8341 11.0701 12.9235 11.2038 12.9845 11.3509C13.0455 11.498 13.0769 11.6558 13.0769 11.8151C13.0769 11.9744 13.0455 12.1321 12.9845 12.2792C12.9235 12.4264 12.8341 12.5601 12.7214 12.6726C12.6088 12.7853 12.4751 12.8747 12.328 12.9357C12.1808 12.9967 12.0231 13.0281 11.8638 13.0281C11.7045 13.0281 11.5468 12.9967 11.3996 12.9357C11.2525 12.8747 11.1188 12.7853 11.0062 12.6726L10.9699 12.6363C10.827 12.4966 10.6456 12.4028 10.449 12.3672C10.2524 12.3315 10.0496 12.3556 9.86683 12.4363C9.68757 12.5131 9.5347 12.6407 9.42702 12.8033C9.31933 12.9659 9.26155 13.1564 9.26077 13.3514V13.4545C9.26077 13.7759 9.13306 14.0842 8.90575 14.3116C8.67843 14.5389 8.37012 14.6666 8.04865 14.6666C7.72717 14.6666 7.41887 14.5389 7.19155 14.3116C6.96423 14.0842 6.83653 13.7759 6.83653 13.4545V13.3999C6.83183 13.1993 6.7669 13.0048 6.65017 12.8416C6.53344 12.6783 6.37031 12.554 6.18198 12.4848C5.99918 12.4041 5.79641 12.38 5.59981 12.4157C5.4032 12.4513 5.22179 12.545 5.07895 12.6848L5.04259 12.7211C4.93001 12.8338 4.79633 12.9232 4.64918 12.9842C4.50203 13.0452 4.3443 13.0766 4.18501 13.0766C4.02572 13.0766 3.86799 13.0452 3.72084 12.9842C3.57369 12.9232 3.44001 12.8338 3.32744 12.7211C3.21474 12.6086 3.12533 12.4749 3.06433 12.3277C3.00333 12.1806 2.97194 12.0228 2.97194 11.8636C2.97194 11.7043 3.00333 11.5465 3.06433 11.3994C3.12533 11.2522 3.21474 11.1186 3.32744 11.006L3.3638 10.9696C3.50352 10.8268 3.59724 10.6454 3.63289 10.4488C3.66854 10.2522 3.64447 10.0494 3.5638 9.86658C3.48697 9.68733 3.35941 9.53445 3.19681 9.42677C3.03421 9.31909 2.84367 9.2613 2.64865 9.26052H2.54562C2.22414 9.26052 1.91583 9.13282 1.68852 8.9055C1.4612 8.67819 1.3335 8.36988 1.3335 8.0484C1.3335 7.72693 1.4612 7.41862 1.68852 7.1913C1.91583 6.96399 2.22414 6.83628 2.54562 6.83628H2.60016C2.80077 6.83159 2.99532 6.76666 3.15853 6.64992C3.32173 6.53319 3.44605 6.37006 3.51531 6.18174C3.59599 5.99894 3.62006 5.79616 3.58441 5.59956C3.54876 5.40296 3.45503 5.22154 3.31531 5.07871L3.27895 5.04234C3.16625 4.92977 3.07685 4.79609 3.01585 4.64894C2.95485 4.50179 2.92345 4.34406 2.92345 4.18477C2.92345 4.02548 2.95485 3.86775 3.01585 3.7206C3.07685 3.57345 3.16625 3.43976 3.27895 3.32719C3.39152 3.21449 3.52521 3.12509 3.67236 3.06409C3.81951 3.00309 3.97723 2.97169 4.13653 2.97169C4.29582 2.97169 4.45355 3.00309 4.6007 3.06409C4.74785 3.12509 4.88153 3.21449 4.9941 3.32719L5.03047 3.36355C5.1733 3.50327 5.35472 3.597 5.55132 3.63265C5.74792 3.6683 5.9507 3.64423 6.1335 3.56355H6.18198C6.36123 3.48673 6.51411 3.35916 6.62179 3.19656C6.72948 3.03396 6.78726 2.84343 6.78804 2.6484V2.54537C6.78804 2.2239 6.91575 1.91559 7.14306 1.68827C7.37038 1.46096 7.67869 1.33325 8.00016 1.33325C8.32164 1.33325 8.62994 1.46096 8.85726 1.68827C9.08458 1.91559 9.21228 2.2239 9.21228 2.54537V2.59992C9.21306 2.79494 9.27085 2.98548 9.37853 3.14808C9.48621 3.31068 9.63909 3.43824 9.81834 3.51507C10.0011 3.59575 10.2039 3.61981 10.4005 3.58416C10.5971 3.54852 10.7785 3.45479 10.9214 3.31507L10.9577 3.27871C11.0703 3.16601 11.204 3.0766 11.3511 3.0156C11.4983 2.9546 11.656 2.92321 11.8153 2.92321C11.9746 2.92321 12.1323 2.9546 12.2795 3.0156C12.4266 3.0766 12.5603 3.16601 12.6729 3.27871C12.7856 3.39128 12.875 3.52496 12.936 3.67211C12.997 3.81926 13.0284 3.97699 13.0284 4.13628C13.0284 4.29557 12.997 4.4533 12.936 4.60045C12.875 4.7476 12.7856 4.88128 12.6729 4.99386L12.6365 5.03022C12.4968 5.17306 12.4031 5.35447 12.3674 5.55108C12.3318 5.74768 12.3558 5.95045 12.4365 6.13325V6.18174C12.5134 6.36099 12.6409 6.51387 12.8035 6.62155C12.9661 6.72923 13.1567 6.78702 13.3517 6.7878H13.4547C13.7762 6.7878 14.0845 6.9155 14.3118 7.14282C14.5391 7.37014 14.6668 7.67844 14.6668 7.99992C14.6668 8.32139 14.5391 8.6297 14.3118 8.85702C14.0845 9.08433 13.7762 9.21204 13.4547 9.21204H13.4002C13.2051 9.21282 13.0146 9.2706 12.852 9.37829C12.6894 9.48597 12.5618 9.63885 12.485 9.8181Z" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>`},
@@ -1378,11 +1580,17 @@
1378
1580
  scan: {vb:"0 0 13 15", svg:`<path fill-rule="evenodd" clip-rule="evenodd" d="M6.76556 1.2204C6.60067 1.12222 6.39933 1.12222 6.23444 1.2204L4.94588 1.98765C4.68515 2.1429 4.35488 2.04501 4.20822 1.76901C4.06156 1.49301 4.15403 1.14341 4.41477 0.988161L5.70333 0.220909C6.198 -0.0736362 6.802 -0.0736362 7.29667 0.220909L8.58523 0.988161C8.84597 1.14341 8.93844 1.49301 8.79178 1.76901C8.64512 2.04501 8.31485 2.1429 8.05412 1.98765L6.76556 1.2204ZM2.94914 2.51871C3.0958 2.79471 3.00333 3.14431 2.74259 3.29956L1.35944 4.12314C1.18888 4.2247 1.08333 4.41574 1.08333 4.62289L1.08333 6.16211C1.08333 6.47878 0.840821 6.73549 0.541667 6.73549C0.242513 6.73549 4.30478e-07 6.47878 4.73526e-07 6.16211L6.02669e-07 4.62289C6.45717e-07 4.00144 0.316652 3.42832 0.828326 3.12365L2.21148 2.30007C2.47221 2.14482 2.80248 2.24271 2.94914 2.51871ZM10.0509 2.51871C10.1975 2.24271 10.5278 2.14482 10.7885 2.30007L12.1717 3.12365C12.6833 3.42832 13 4.00144 13 4.62288V6.16211C13 6.47878 12.7575 6.73549 12.4583 6.73549C12.1592 6.73549 11.9167 6.47878 11.9167 6.16211V4.62288C11.9167 4.41574 11.8111 4.2247 11.6406 4.12314L10.2574 3.29956C9.99667 3.14431 9.9042 2.79471 10.0509 2.51871ZM12.4583 8.26451C12.7575 8.26451 13 8.52122 13 8.83789V10.3771C13 10.9986 12.6833 11.5717 12.1717 11.8763L10.7885 12.6999C10.5278 12.8552 10.1975 12.7573 10.0509 12.4813C9.9042 12.2053 9.99668 11.8557 10.2574 11.7004L11.6406 10.8769C11.8111 10.7753 11.9167 10.5843 11.9167 10.3771V8.83789C11.9167 8.52122 12.1592 8.26451 12.4583 8.26451ZM0.541667 8.26451C0.840821 8.26451 1.08333 8.52122 1.08333 8.83789L1.08333 10.3771C1.08333 10.5843 1.18888 10.7753 1.35944 10.8769L2.74259 11.7004C3.00332 11.8557 3.0958 12.2053 2.94913 12.4813C2.80247 12.7573 2.47221 12.8552 2.21147 12.6999L0.828326 11.8763C0.31665 11.5717 -4.30478e-08 10.9986 0 10.3771L1.29143e-07 8.83789C1.72191e-07 8.52122 0.242513 8.26451 0.541667 8.26451ZM4.20821 13.231C4.35488 12.955 4.68514 12.8571 4.94588 13.0123L6.23444 13.7796C6.39933 13.8778 6.60067 13.8778 6.76556 13.7796L8.05412 13.0123C8.31486 12.8571 8.64512 12.955 8.79179 13.231C8.93845 13.507 8.84598 13.8566 8.58524 14.0118L7.29667 14.7791C6.802 15.0736 6.198 15.0736 5.70333 14.7791L4.41476 14.0118C4.15402 13.8566 4.06155 13.507 4.20821 13.231Z" fill="currentColor"/><path fill-rule="evenodd" clip-rule="evenodd" d="M4.62357 6.35324C4.77315 6.07899 5.10443 5.98503 5.3635 6.14337L6.49997 6.83792L7.63643 6.14337C7.8955 5.98503 8.22678 6.07899 8.37636 6.35324C8.52594 6.62748 8.43717 6.97816 8.1781 7.13649L7.04164 7.83104V9.22014C7.04164 9.53681 6.79912 9.79353 6.49997 9.79353C6.20082 9.79353 5.9583 9.53681 5.9583 9.22014V7.83104L4.82184 7.13649C4.56276 6.97816 4.474 6.62748 4.62357 6.35324Z" fill="currentColor"/><path fill-rule="evenodd" clip-rule="evenodd" d="M6.49998 0.213281C6.79913 0.213281 7.04164 0.469993 7.04164 0.786663V3.10408C7.04164 3.42075 6.79913 3.67746 6.49998 3.67746C6.20082 3.67746 5.95831 3.42075 5.95831 3.10408V0.786663C5.95831 0.469993 6.20082 0.213281 6.49998 0.213281ZM0.349927 3.83504C0.499504 3.56079 0.830782 3.46683 1.08986 3.62516L3.02114 4.80548C3.28022 4.96381 3.36898 5.31449 3.21941 5.58873C3.06983 5.86297 2.73855 5.95694 2.47948 5.7986L0.548191 4.61829C0.289116 4.45995 0.20035 4.10928 0.349927 3.83504ZM12.65 3.83504C12.7996 4.10928 12.7108 4.45995 12.4518 4.61829L10.5205 5.7986C10.2614 5.95694 9.93012 5.86297 9.78055 5.58873C9.63097 5.31449 9.71974 4.96381 9.97881 4.80548L11.9101 3.62516C12.1692 3.46683 12.5004 3.56079 12.65 3.83504ZM3.24873 9.39335C3.3983 9.6676 3.30954 10.0183 3.05046 10.1766L1.08986 11.3748C0.830782 11.5332 0.499504 11.4392 0.349927 11.165C0.20035 10.8907 0.289115 10.54 0.548191 10.3817L2.50879 9.18348C2.76787 9.02515 3.09915 9.11911 3.24873 9.39335ZM9.75123 9.39335C9.90081 9.11911 10.2321 9.02515 10.4912 9.18348L12.4518 10.3817C12.7108 10.54 12.7996 10.8907 12.65 11.165C12.5004 11.4392 12.1692 11.5332 11.9101 11.3748L9.94949 10.1766C9.69042 10.0183 9.60165 9.6676 9.75123 9.39335ZM6.49998 11.2867C6.79913 11.2867 7.04164 11.5434 7.04164 11.8601V14.2133C7.04164 14.53 6.79913 14.7867 6.49998 14.7867C6.20082 14.7867 5.95831 14.53 5.95831 14.2133V11.8601C5.95831 11.5434 6.20082 11.2867 6.49998 11.2867Z" fill="currentColor"/>`}
1379
1581
  };
1380
1582
  ICONS.minimize = ICONS.chevron;
1583
+ // right chevron — exact Figma artwork (581:2828 "Chevron small down (Stroke)")
1584
+ // rotated -90° about its centre and centred in a 16px box.
1585
+ ICONS.chevronr = {vb:"0 0 16 16", svg:`<g transform="translate(8,8) rotate(-90) translate(-3.75,-2.25)"><path fill-rule="evenodd" clip-rule="evenodd" d="M0.21967 0.21967C0.512563 -0.0732233 0.987437 -0.0732233 1.28033 0.21967L3.75 2.68934L6.21967 0.21967C6.51256 -0.0732233 6.98744 -0.0732233 7.28033 0.21967C7.57322 0.512563 7.57322 0.987437 7.28033 1.28033L4.28033 4.28033C3.98744 4.57322 3.51256 4.57322 3.21967 4.28033L0.21967 1.28033C-0.0732233 0.987437 -0.0732233 0.512563 0.21967 0.21967Z" fill="currentColor"/></g>`};
1381
1586
  ICONS.close = {vb:"0 0 16 16", svg:`<path d="M4 4L12 12M12 4L4 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`};
1382
1587
  ICONS.play = {vb:"0 0 16 16", svg:`<path d="M5 3.5L12 8L5 12.5V3.5Z" fill="currentColor"/>`};
1383
1588
  ICONS.pause = {vb:"0 0 16 16", svg:`<path d="M5 3.5H6.8V12.5H5V3.5ZM9.2 3.5H11V12.5H9.2V3.5Z" fill="currentColor"/>`};
1384
1589
  // dots-vertical (Figma node 580:4819) — vertical "⋮" overflow icon
1385
1590
  ICONS.dotsv = {vb:"0 0 16 16", svg:`<path fill-rule="evenodd" clip-rule="evenodd" d="M6.66667 8C6.66667 7.26362 7.26362 6.66667 8 6.66667C8.73638 6.66667 9.33333 7.26362 9.33333 8C9.33333 8.73638 8.73638 9.33333 8 9.33333C7.26362 9.33333 6.66667 8.73638 6.66667 8Z" fill="currentColor"/><path fill-rule="evenodd" clip-rule="evenodd" d="M6.66667 3.33333C6.66667 2.59695 7.26362 2 8 2C8.73638 2 9.33333 2.59695 9.33333 3.33333C9.33333 4.06971 8.73638 4.66667 8 4.66667C7.26362 4.66667 6.66667 4.06971 6.66667 3.33333Z" fill="currentColor"/><path fill-rule="evenodd" clip-rule="evenodd" d="M6.66667 12.6667C6.66667 11.9303 7.26362 11.3333 8 11.3333C8.73638 11.3333 9.33333 11.9303 9.33333 12.6667C9.33333 13.403 8.73638 14 8 14C7.26362 14 6.66667 13.403 6.66667 12.6667Z" fill="currentColor"/>`};
1591
+ // accept checkmark (Figma node 580:9057) — slimmer 1.5px check centered in a
1592
+ // square 16-frame (the glyph spans ~50% width, like the source icon frame).
1593
+ ICONS.accept = {vb:"0 0 16 16", svg:`<path d="M4 8.4268L6.46155 11.19223L12 4.97001" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`};
1386
1594
  function Ic({name, size=16}){
1387
1595
  const ic = ICONS[name];
1388
1596
  if(!ic) return null;
@@ -1394,6 +1602,15 @@
1394
1602
  style:{display:"block"},dangerouslySetInnerHTML:{__html:ic.svg}});
1395
1603
  }
1396
1604
 
1605
+ // dot-matrix loader (ported from @dotmatrix/dotm-square-14). 25 dots laid out
1606
+ // 5×5; each cell maps to a symmetry group a–h whose opacity @keyframes replays
1607
+ // the source's 0→1→2→3→2→1 frame sequence. Inherits text color via currentColor.
1608
+ const DOTM_GROUPS = "ABCBADEFEDCGHGCBEFEBABCBA";
1609
+ function DotmLoader({className}){
1610
+ return h("span",{className:cx("tl-dotm",className),"aria-hidden":"true"},
1611
+ ...DOTM_GROUPS.split("").map((g,i)=>h("span",{key:i,className:"tl-dotm-dot tl-dotm-"+g.toLowerCase()})));
1612
+ }
1613
+
1397
1614
  // portaled, origin-aware dropdown surface (transitions.dev menu-dropdown)
1398
1615
  function Dropdown({open,onClose,triggerRef,width,align,children}){
1399
1616
  const ref=useRef(null);
@@ -1463,8 +1680,9 @@
1463
1680
  target);
1464
1681
  }
1465
1682
 
1466
- function MenuItem({active,disabled,onClick,right,children}){
1683
+ function MenuItem({active,disabled,onClick,left,right,children}){
1467
1684
  return h("div",{className:cx("tl-menu-item",disabled&&"disabled"),onClick:disabled?undefined:onClick},
1685
+ left,
1468
1686
  h("span",{className:"tl-menu-item-label"},children),
1469
1687
  right);
1470
1688
  }
@@ -1524,9 +1742,64 @@
1524
1742
  },[text]);
1525
1743
  return h("span",{ref:setEl,className:"tl-refine-status-text t-shimmer t-text-swap"});
1526
1744
  }
1527
- // Results entrance — transitions.dev texts reveal (18): the summary and each
1528
- // suggestion card are stagger lines that rise + unblur in sequence on mount.
1529
- function RefineResults({summary,suggestions,appliedIds,onApply}){
1745
+ // Results entrance — transitions.dev texts reveal (18): each suggestion card
1746
+ // is a stagger line that rises + unblur in sequence on mount.
1747
+ function resolveSugMember(s, lanes){
1748
+ const direct=(s.patch&&s.patch.member)||s.member;
1749
+ if(direct) return direct;
1750
+ if(!lanes||!s.property||s.property==="all") return null;
1751
+ const matches=lanes.filter(t=>t.property===s.property&&t.member);
1752
+ if(!matches.length) return null;
1753
+ const names=[...new Set(matches.map(t=>t.member))];
1754
+ return names.length===1?names[0]:matches[0].member;
1755
+ }
1756
+ // Apply button — card resize (transitions.dev 01) tweens the width on
1757
+ // Apply -> Applied, and the gray checkmark plays the success check (10):
1758
+ // fade + rotate + Y-bob + blur on the wrapper, stroke-draw on the path.
1759
+ // The same <button> node is reused across states so width can tween.
1760
+ function ApplyBtn({applied,onApply}){
1761
+ const ref=useRef(null);
1762
+ const lastW=useRef(0); // width captured while showing "Apply"
1763
+ const prev=useRef(applied);
1764
+ useLayoutEffect(()=>{
1765
+ const btn=ref.current;if(!btn)return;
1766
+ const justApplied=applied&&!prev.current;
1767
+ if(applied){
1768
+ const chk=btn.querySelector(".tl-sug-check");
1769
+ if(chk){
1770
+ const path=chk.querySelector("svg path");
1771
+ if(path){ const len=Math.ceil(path.getTotalLength()); // exact path length
1772
+ path.style.strokeDasharray=len; path.style.strokeDashoffset=len; }
1773
+ if(justApplied){
1774
+ // card-resize: explicit from(old "Apply") -> to(new auto) width tween
1775
+ const to=btn.getBoundingClientRect().width, from=lastW.current;
1776
+ if(from&&to&&Math.abs(from-to)>0.5){
1777
+ btn.style.width=from+"px";
1778
+ void btn.offsetWidth; // reflow so the tween starts from `from`
1779
+ btn.style.width=to+"px";
1780
+ const onEnd=(e)=>{ if(e.target===btn&&e.propertyName==="width"){
1781
+ btn.style.width=""; btn.removeEventListener("transitionend",onEnd); } };
1782
+ btn.addEventListener("transitionend",onEnd);
1783
+ }
1784
+ // success-check replay: reset, force reflow, play from offset 0
1785
+ chk.setAttribute("data-state","out");
1786
+ void chk.offsetWidth;
1787
+ chk.setAttribute("data-state","in");
1788
+ } else { chk.setAttribute("data-state","in"); }
1789
+ }
1790
+ } else { lastW.current=btn.getBoundingClientRect().width; }
1791
+ prev.current=applied;
1792
+ });
1793
+ if(applied) return h("button",{ref,className:"tl-pill-btn tl-apply-btn is-applied",disabled:true},
1794
+ h("span",{className:"tl-sug-check","data-state":"out","aria-hidden":"true"},
1795
+ h("svg",{width:13,height:13,viewBox:"0 0 16 16",fill:"none",
1796
+ xmlns:"http://www.w3.org/2000/svg",style:{display:"block"}},
1797
+ h("path",{d:"M4 8.4268L6.46155 11.19223L12 4.97001",stroke:"currentColor",
1798
+ strokeWidth:1.5,strokeLinecap:"round",strokeLinejoin:"round"}))),
1799
+ "Applied");
1800
+ return h("button",{ref,className:"tl-pill-btn tl-apply-btn",onClick:onApply},"Apply");
1801
+ }
1802
+ function RefineResults({suggestions,appliedIds,onApply,lanes}){
1530
1803
  const ref=useRef(null);
1531
1804
  useEffect(()=>{
1532
1805
  const el=ref.current;if(!el)return;
@@ -1535,27 +1808,45 @@
1535
1808
  el.classList.add("is-shown");
1536
1809
  },[]);
1537
1810
  const delay=(i)=>({transitionDelay:"calc(var(--stagger-stagger) * "+i+")"});
1811
+ // category chip label + color modifier, keyed by the suggestion kind
1812
+ const CAT={easing:["Easing","is-easing"],duration:["Duration","is-duration"],
1813
+ delay:["Delay","is-delay"],scale:["Scale","is-scale"],distance:["Distance","is-distance"],
1814
+ replace:["Transition replacement","is-replace"]};
1815
+ const cap=(p)=>p&&p!=="all"?p.charAt(0).toUpperCase()+p.slice(1):"";
1538
1816
  let i=0;
1539
1817
  return h("div",{ref,className:"tl-refine-results t-stagger"},
1540
- summary&&h("div",{className:"tl-refine-summary t-stagger-line",style:delay(i++)},summary),
1541
1818
  suggestions.map(s=>{
1542
1819
  const applied=!!appliedIds[s.id];
1820
+ const isReplace=s.kind==="replace";
1821
+ const member=resolveSugMember(s,lanes);
1822
+ const prop=cap(s.property);
1823
+ const cat=CAT[s.kind]||CAT.easing;
1824
+ const applyBtn=h(ApplyBtn,{applied,onApply:()=>onApply(s)});
1543
1825
  return h("div",{className:"tl-sug t-stagger-line",key:s.id,style:delay(i++)},
1544
- h("div",{className:"tl-sug-top"},
1545
- h("span",{className:"tl-sug-kind"},s.kind||"tweak"),
1546
- s.property&&h("span",{className:"tl-sug-prop"},s.property)),
1547
- h("div",{className:"tl-sug-title"},s.title),
1548
- h("div",{className:"tl-sug-delta"},
1549
- s.from&&h("span",{className:"tl-sug-from"},s.from),
1550
- s.from&&h("span",{className:"tl-sug-arrow"},"\u2192"),
1551
- h("span",{className:"tl-sug-to"},s.to)),
1552
- s.reason&&h("div",{className:"tl-sug-reason"},s.reason),
1553
- applied
1554
- ? h("button",{className:"tl-sug-apply is-applied",disabled:true},h(Ic,{name:"check",size:13}),"Applied")
1555
- : h("button",{className:"tl-sug-apply",onClick:()=>onApply(s)},"Apply"));
1826
+ // top row: member chip + property, or a single heading for replace cards
1827
+ h("div",{className:"tl-sug-head"},
1828
+ isReplace
1829
+ ? h("span",{className:"tl-sug-prop"},s.title||"Transition replacement")
1830
+ : h(React.Fragment,null,
1831
+ member&&h("span",{className:"tl-sug-member"},member),
1832
+ prop&&h("span",{className:"tl-sug-prop"},prop))),
1833
+ h("div",{className:"tl-sug-divider"}),
1834
+ h("span",{className:cx("tl-sug-cat",cat[1])},cat[0]),
1835
+ h("div",{className:"tl-sug-body"},
1836
+ h("div",{className:"tl-sug-text"},
1837
+ isReplace
1838
+ ? h("div",{className:"tl-sug-title"},
1839
+ "Current transition",h("br"),"\u2192 "+(s.title||"transition")+" from transitions.dev")
1840
+ : h(React.Fragment,null,
1841
+ s.title&&h("div",{className:"tl-sug-title"},s.title),
1842
+ (s.from||s.to)&&h("div",{className:"tl-sug-delta"},
1843
+ s.from&&h("span",{className:"tl-sug-from"},s.from),
1844
+ s.to&&h("span",{className:"tl-sug-to"},s.to))),
1845
+ s.reason&&h("div",{className:"tl-sug-reason"},s.reason)),
1846
+ applyBtn));
1556
1847
  }));
1557
1848
  }
1558
- function RefinePanel({open,onClose,phase,label,refineType,onType,suggestions,summary,error,appliedIds,onApply,onApplyAll,mode,onMode,llmAvailable,cliInstalled,onStart}){
1849
+ function RefinePanel({open,onClose,phase,label,refineType,onType,suggestions,summary,error,appliedIds,onApply,onApplyAll,mode,onMode,llmAvailable,cliInstalled,onStart,lanes}){
1559
1850
  // mount-on-open; keep mounted through the panel-reveal slide-out, then unmount
1560
1851
  const[render,setRender]=useState(open);
1561
1852
  const[panelOpen,setPanelOpen]=useState(false);
@@ -1605,6 +1896,12 @@
1605
1896
  const agentReady = !agentMode || llmAvailable!==false;
1606
1897
  const typeDesc = (REFINE_TYPES.find(t=>t.key===refineType)||REFINE_TYPES[0]).desc;
1607
1898
  const modeLabel = (REFINE_MODES.find(m=>m.key===mode)||REFINE_MODES[0]).label;
1899
+ // Idle empty-state copy is mode-aware: Agent (llm) keeps the type's
1900
+ // agent-oriented desc; Deterministic has no agent, so it describes the
1901
+ // math-snap behavior (and that "replace" needs Agent mode).
1902
+ const idleDesc = agentMode ? typeDesc : (refineType==="replace"
1903
+ ? "Deterministic mode can't pick a recipe to replace the transition — switch to Agent mode for replacements."
1904
+ : "Deterministic mode snaps your transition's timing to the nearest Transitions.dev motion tokens — mathematically, with no agent or credits.");
1608
1905
  // One persistent control for the whole foot: in idle/done/error it's the
1609
1906
  // pill button; while scanning it carries `.is-scanning` and the same DOM
1610
1907
  // node morphs (card resize) into the loading rectangle with the border-beam.
@@ -1615,17 +1912,17 @@
1615
1912
  return h("button",{className:cx("tl-scan-morph","t-resize",scanning&&"is-scanning"),
1616
1913
  disabled:(!!o.disabled)||scanning,"aria-busy":scanning?"true":undefined,
1617
1914
  onClick:(o.disabled||scanning)?undefined:onStart},
1618
- scanning&&h(BorderBeam,{size:"md",colorVariant:"ocean",theme:"light",borderRadius:10,duration:2.6,saturation:2,brightness:1.6,className:"tl-scan-beam"},
1915
+ scanning&&h(BorderBeam,{size:"md",colorVariant:"ocean",theme:"light",duration:2.6,saturation:2,brightness:1.6,className:"tl-scan-beam"},
1619
1916
  h("span",{className:"tl-scan-beam-fill"})),
1620
1917
  h("span",{className:"tl-scan-content"},
1621
1918
  h("span",{className:"tl-scan-face tl-scan-face-label","aria-hidden":scanning?"true":undefined},
1622
1919
  h("span",{className:"tl-scan-label"},o.label||"Start scanning")),
1623
1920
  h("span",{className:"tl-scan-face tl-scan-face-status","aria-hidden":scanning?undefined:"true"},
1624
- h("span",{className:"tl-scan-ic","aria-hidden":"true"},h(Ic,{name:"scan",size:15})),
1921
+ h(DotmLoader),
1625
1922
  h(ShimmerSwapText,{text:REFINE_STATUS[statusIx]}))));
1626
1923
  };
1627
1924
 
1628
- let body, foot;
1925
+ let body, foot, footBar=false;
1629
1926
  if(phase==="scanning"){
1630
1927
  body = h("div",{className:"tl-refine-center"});
1631
1928
  foot = startBtn();
@@ -1643,10 +1940,13 @@
1643
1940
  h("p",{className:"tl-refine-idle-text"},emptyMsg));
1644
1941
  foot = startBtn({label:"Scan again"});
1645
1942
  } else {
1646
- body = h(RefineResults,{summary:refineType==="replace"?null:summary,suggestions:visible,appliedIds,onApply});
1647
- foot = pending.length>1
1648
- ? h("button",{className:"pc-btn primary",style:{flex:1},onClick:()=>pending.forEach(onApply)},"Apply all ("+pending.length+")")
1649
- : startBtn({label:"Scan again"});
1943
+ body = h(RefineResults,{suggestions:visible,appliedIds,onApply,lanes:lanes||[]});
1944
+ footBar = true;
1945
+ foot = h(React.Fragment,null,
1946
+ h("button",{className:"tl-pill-btn",disabled:pending.length===0,
1947
+ onClick:()=>pending.forEach(onApply)},
1948
+ h("span",null,"Apply all ",h("span",{className:"tl-dim"},"("+pending.length+")"))),
1949
+ h("button",{className:"tl-pill-btn",onClick:onStart},"Scan again"));
1650
1950
  }
1651
1951
  } else { // idle
1652
1952
  if(!agentReady){
@@ -1659,7 +1959,7 @@
1659
1959
  foot = startBtn({disabled:true});
1660
1960
  } else {
1661
1961
  body = h("div",{className:"tl-refine-center"},
1662
- h("p",{className:"tl-refine-idle-text"},typeDesc));
1962
+ h("p",{className:"tl-refine-idle-text"},idleDesc));
1663
1963
  foot = startBtn({});
1664
1964
  }
1665
1965
  }
@@ -1684,13 +1984,13 @@
1684
1984
  h("div",{className:"tl-mode-row-main"},
1685
1985
  h("div",{className:"tl-mode-row-title"},m.label),
1686
1986
  h("div",{className:"tl-mode-row-desc"},m.desc)),
1687
- (mode===m.key)&&h("span",{className:"tl-mode-row-check"},h(Ic,{name:"check",size:16}))))),
1987
+ (mode===m.key)&&h("span",{className:"tl-mode-row-check"},h(Ic,{name:"accept",size:16}))))),
1688
1988
  h("div",{className:"tl-refine-tabs",role:"tablist","aria-label":"Refinement type"},
1689
1989
  REFINE_TYPES.map(t=>h("button",{key:t.key,className:"tl-refine-tab",role:"tab",
1690
1990
  "aria-selected":refineType===t.key?"true":"false",
1691
1991
  onClick:()=>{if(t.key!==refineType)onType(t.key);}},t.label))),
1692
1992
  h("div",{className:"tl-refine-body"},body),
1693
- h("div",{className:"tl-refine-foot"},foot)));
1993
+ h("div",{className:cx("tl-refine-foot",footBar&&"is-bar")},foot)));
1694
1994
  }
1695
1995
 
1696
1996
  // Diff the active item's effective (edited/refined) lanes against their
@@ -1736,10 +2036,10 @@
1736
2036
  const fFlat=flatItems.filter(e=>!ql||(e.label||"").toLowerCase().includes(ql));
1737
2037
  const noMatches=!!ql&&fGroups.length===0&&fFlat.length===0;
1738
2038
  const phaseItem=(e)=>h(MenuItem,{key:e.id,active:active&&e.id===active.id,onClick:()=>{onSelect(e.id);setPick(false);},
1739
- right:active&&e.id===active.id&&h("span",{className:"tl-menu-check"},h(Ic,{name:"check"}))},
2039
+ right:active&&e.id===active.id&&h("span",{className:"tl-menu-check"},h(Ic,{name:"accept"}))},
1740
2040
  h("span",null,e.phaseLabel,h("span",{className:"tl-menu-dim"}," "+e.durationMs+"ms")));
1741
2041
  const flatItem=(e)=>h(MenuItem,{key:e.id,active:active&&e.id===active.id,onClick:()=>{onSelect(e.id);setPick(false);},
1742
- right:active&&e.id===active.id&&h("span",{className:"tl-menu-check"},h(Ic,{name:"check"}))},
2042
+ right:active&&e.id===active.id&&h("span",{className:"tl-menu-check"},h(Ic,{name:"accept"}))},
1743
2043
  h("span",null,e.label,h("span",{className:"tl-menu-dim"}," "+e.durationMs+"ms")));
1744
2044
  return h("div",{className:"tl-header"},
1745
2045
  h("span",{className:"tl-header-label"},"Selected"),
@@ -1763,14 +2063,24 @@
1763
2063
  fFlat.length>0&&fGroups.length>0&&h("div",{className:"tl-menu-section"},"Ungrouped"),
1764
2064
  ...fFlat.map(flatItem))),
1765
2065
  h("span",{className:"tl-header-count"},
1766
- scanning?"Scanning transitions…":entries.length+" transition"+(entries.length===1?"":"s")+" found"),
2066
+ scanning
2067
+ ? h("span",{className:"tl-scanning-row"},h(DotmLoader),h("span",null,"Scanning transitions…"))
2068
+ : entries.length+" transition"+(entries.length===1?"":"s")+" found"),
1767
2069
  h("button",{ref:gearRef,className:cx("tl-icon-btn",setg&&"is-active"),title:"Settings",onClick:()=>setSetg(v=>!v)},h(Ic,{name:"dotsv"})),
1768
2070
  h(Dropdown,{open:setg,onClose:()=>setSetg(false),triggerRef:gearRef,width:210,align:"right"},
1769
- h(MenuItem,{onClick:()=>setSnap(v=>!v),right:snap&&h("span",{className:"tl-menu-check"},h(Ic,{name:"check"}))},"Snap to grid"),
2071
+ h("div",{className:"tl-menu-head"},
2072
+ h("span",null,"Transitions.dev ",h("span",{className:"tl-menu-head-name"},"Refine")),
2073
+ h("span",{className:"tl-menu-head-ver"},REFINE_VERSION)),
2074
+ h("div",{className:"tl-menu-divider"}),
1770
2075
  h(MenuItem,{disabled:!active,onClick:()=>{onCopy&&onCopy();setSetg(false);},
1771
- right:copied&&h("span",{className:"tl-menu-check"},h(Ic,{name:"check"}))},copied?"Copied":"Copy values"),
1772
- h(MenuItem,{disabled:scanning,onClick:()=>{setSetg(false);onRescan&&onRescan();}},
1773
- scanning?"Rescanning…":"Rescan transitions")),
2076
+ left:h("span",{className:"tl-menu-icon"},h(Ic,{name:"copy"})),
2077
+ right:copied&&h("span",{className:"tl-menu-check"},h(Ic,{name:"accept"}))},copied?"Copied":"Copy values"),
2078
+ h(MenuItem,{disabled:scanning,onClick:()=>{setSetg(false);onRescan&&onRescan();},
2079
+ left:h("span",{className:"tl-menu-icon"},h(Ic,{name:"restart"}))},
2080
+ scanning?"Rescanning…":"Rescan transitions"),
2081
+ h("div",{className:"tl-menu-divider"}),
2082
+ h(MenuItem,{onClick:()=>{setSetg(false);window.open("https://transitions.dev","_blank","noopener");},
2083
+ right:h("span",{className:"tl-menu-chevr"},h(Ic,{name:"chevronr",size:16}))},"Learn more")),
1774
2084
  h("span",{className:"t-tt-wrap"},
1775
2085
  h("button",{className:"tl-sec-btn t-tt-trigger",disabled:!active,onClick:onReset},"Reset"),
1776
2086
  h("span",{className:"t-tt tl-tt-below",role:"tooltip"},"Reset values")),
@@ -1779,9 +2089,9 @@
1779
2089
  disabled:!active||acceptDisabled||acceptState==="saving"||acceptState==="done",onClick:onAccept,"aria-label":"Accept changes to your code"},
1780
2090
  acceptState==="saving"
1781
2091
  ? h("span",{className:"tl-accept-spin","aria-hidden":"true"})
1782
- : h(Ic,{name:"check"}),
2092
+ : h(Ic,{name:"accept"}),
1783
2093
  h("span",null,acceptState==="done"?"Done":"Accept")),
1784
- h("span",{className:"t-tt tl-tt-below",role:"tooltip"},
2094
+ h("span",{className:cx("t-tt","tl-tt-below",acceptState==="error"&&acceptError&&"tl-tt-msg"),role:"tooltip"},
1785
2095
  acceptState==="error"&&acceptError?acceptError
1786
2096
  :acceptState==="done"?"Saved to your code"
1787
2097
  :acceptDisabled?"No changes to save"
@@ -1910,7 +2220,7 @@
1910
2220
  !readOnly&&h(Dropdown,{open:menu,onClose:()=>setMenu(false),triggerRef:chevRef,
1911
2221
  width:(wrapRef.current&&wrapRef.current.offsetWidth)||220,align:"right"},
1912
2222
  (tokens||[]).map(tk=>h(MenuItem,{key:tk.label,active:value===tk.ms,onClick:()=>{onChange(tk.ms);setMenu(false);},
1913
- right:value===tk.ms&&h("span",{className:"tl-menu-check"},h(Ic,{name:"check"}))},
2223
+ right:value===tk.ms&&h("span",{className:"tl-menu-check"},h(Ic,{name:"accept"}))},
1914
2224
  h("span",{className:"tl-menu-text"},tk.label,h("span",{className:"tl-menu-dim"}," "+tk.ms+"ms")))) ),
1915
2225
  readOnly&&readOnlyHint&&h("span",{className:"t-tt",role:"tooltip"},readOnlyHint),
1916
2226
  );
@@ -1924,6 +2234,131 @@
1924
2234
  // -0.5..1.5 range) stay visible instead of being clipped at the edges.
1925
2235
  const CURVE = { VBW:269, VBH:150, PAD_X:30, PAD_Y:42 };
1926
2236
 
2237
+ // Unified "Preview" section (Figma node 580:11158) — collapsible, collapsed by
2238
+ // default (transitions.dev · 21-accordion). Holds BOTH the timing curve card
2239
+ // (cubic w/ draggable handles, or the spring curve) AND the animated position
2240
+ // preview: a big blue marker travels along a dashed rail using the live timing
2241
+ // function + duration, pauses, returns, and loops.
2242
+ function Preview({easing, cubic, spring, durationMs, apply}){
2243
+ const trackRef = useRef(null);
2244
+ const dotRef = useRef(null);
2245
+ const svgRef = useRef(null);
2246
+ const [playing, setPlaying] = useState(false);
2247
+ const [open, setOpen] = useState(false);
2248
+ const safeEasing = (!easing || easing.trim()==="") ? "linear" : easing;
2249
+ const dur = Math.max(120, durationMs || 0);
2250
+
2251
+ const { VBW, VBH, PAD_X, PAD_Y } = CURVE;
2252
+ const plotW = VBW - 2*PAD_X, plotH = VBH - 2*PAD_Y;
2253
+ const originX = PAD_X, originY = VBH - PAD_Y, endX = VBW - PAD_X, endY = PAD_Y;
2254
+ const fx = f => PAD_X + f*plotW;
2255
+ const fy = v => (VBH - PAD_Y) - v*plotH;
2256
+ const cb = cubic || [0.25,0.1,0.25,1];
2257
+ const setEasing = useCallback(v=>apply({easing:v, spring:null}),[apply]);
2258
+
2259
+ const startHandleDrag = useCallback((pointIdx, e)=>{
2260
+ e.preventDefault(); e.stopPropagation();
2261
+ const svg = svgRef.current; if(!svg) return;
2262
+ const calc = (clientX, clientY) => {
2263
+ const rect = svg.getBoundingClientRect();
2264
+ const vbx = (clientX - rect.left) / rect.width * VBW;
2265
+ const vby = (clientY - rect.top) / rect.height * VBH;
2266
+ let x = (vbx - PAD_X) / plotW;
2267
+ let y = ((VBH - PAD_Y) - vby) / plotH;
2268
+ x = Math.round(Math.max(0, Math.min(1, x)) * 100) / 100;
2269
+ y = Math.round(Math.max(-0.5, Math.min(1.5, y)) * 100) / 100;
2270
+ const next = [...cb];
2271
+ next[pointIdx*2] = x; next[pointIdx*2+1] = y;
2272
+ setEasing(`cubic-bezier(${next.join(", ")})`);
2273
+ };
2274
+ const onMove = e2 => calc(e2.clientX, e2.clientY);
2275
+ const onUp = () => { window.removeEventListener("mousemove",onMove); window.removeEventListener("mouseup",onUp); };
2276
+ window.addEventListener("mousemove",onMove);
2277
+ window.addEventListener("mouseup",onUp);
2278
+ },[cb,setEasing,plotW,plotH,VBW,VBH,PAD_X,PAD_Y]);
2279
+
2280
+ // spring curve (read-only) — auto-fit so overshoot above 1 stays in frame
2281
+ const springCurve = useMemo(()=>{
2282
+ if(!spring) return null;
2283
+ const sim = simulateSpring(spring.stiffness, spring.damping, spring.mass ?? 1);
2284
+ const vals = sim.values;
2285
+ const hi = Math.max(1, ...vals), lo = Math.min(0, ...vals);
2286
+ const range = (hi - lo) || 1;
2287
+ const sx = i => PAD_X + (i/(vals.length-1))*plotW;
2288
+ const sy = v => (VBH - PAD_Y) - ((v - lo)/range)*plotH;
2289
+ return { d: vals.map((v,i)=>`${i===0?"M":"L"} ${sx(i).toFixed(2)} ${sy(v).toFixed(2)}`).join(" "),
2290
+ yOne: sy(1), yZero: sy(0) };
2291
+ },[spring,plotW,plotH,VBW,VBH,PAD_X,PAD_Y]);
2292
+
2293
+ useEffect(()=>{
2294
+ if(!playing || !open) return;
2295
+ const dot = dotRef.current, track = trackRef.current;
2296
+ if(!dot || !track) return;
2297
+ let cancelled = false, anim = null, timer = null, atRight = false;
2298
+ const GAP = 480, DOT = 28;
2299
+ const travel = ()=> Math.max(0, track.clientWidth - DOT);
2300
+ const step = ()=>{
2301
+ if(cancelled) return;
2302
+ const from = atRight ? travel() : 0;
2303
+ const to = atRight ? 0 : travel();
2304
+ try {
2305
+ anim = dot.animate(
2306
+ [{transform:`translateX(${from}px)`},{transform:`translateX(${to}px)`}],
2307
+ {duration:dur, easing:safeEasing, fill:"forwards"});
2308
+ } catch {
2309
+ anim = dot.animate(
2310
+ [{transform:`translateX(${from}px)`},{transform:`translateX(${to}px)`}],
2311
+ {duration:dur, easing:"linear", fill:"forwards"});
2312
+ }
2313
+ anim.onfinish = ()=>{ if(cancelled) return; atRight = !atRight; timer = setTimeout(step, GAP); };
2314
+ };
2315
+ step();
2316
+ return ()=>{ cancelled = true; if(anim){try{anim.cancel();}catch{}} if(timer)clearTimeout(timer); };
2317
+ },[playing, open, safeEasing, dur]);
2318
+
2319
+ const p1x = fx(cb[0]), p1y = fy(cb[1]), p2x = fx(cb[2]), p2y = fy(cb[3]);
2320
+
2321
+ return h("div",{className:"tl-preview","data-open":open?"true":"false"},
2322
+ h("button",{className:"tl-preview-head","aria-expanded":open,onClick:()=>setOpen(o=>!o)},
2323
+ h("span",{className:"tl-preview-title"},"Preview"),
2324
+ h("span",{className:"tl-preview-chevron"},h(Ic,{name:"chevron",size:16})),
2325
+ ),
2326
+ h("div",{className:"tl-preview-panel"},
2327
+ h("div",{className:"tl-preview-panel-inner"},
2328
+ h("div",{className:"tl-preview-body"},
2329
+ h("div",{className:"tl-curve tl-preview-curve"},
2330
+ spring
2331
+ ? h("svg",{viewBox:`0 0 ${VBW} ${VBH}`},
2332
+ h("line",{x1:PAD_X,y1:springCurve.yOne,x2:VBW-PAD_X,y2:springCurve.yOne,stroke:"#1A7AFF",strokeWidth:1,strokeDasharray:"3 4",style:{opacity:0.5,pointerEvents:"none"}}),
2333
+ h("line",{x1:PAD_X,y1:springCurve.yZero,x2:VBW-PAD_X,y2:springCurve.yZero,stroke:"rgba(0,0,0,0.18)",strokeWidth:1,strokeDasharray:"3 4",style:{pointerEvents:"none"}}),
2334
+ h("path",{d:springCurve.d,fill:"none",stroke:"#181a1e",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}),
2335
+ )
2336
+ : h("svg",{ref:svgRef,viewBox:`0 0 ${VBW} ${VBH}`},
2337
+ h("path",{d:`M ${originX} ${originY} C ${p1x} ${p1y}, ${p2x} ${p2y}, ${endX} ${endY}`,
2338
+ fill:"none",stroke:"#181a1e",strokeWidth:2,strokeLinecap:"round"}),
2339
+ h("line",{x1:p1x,y1:p1y,x2:originX,y2:originY,stroke:"#1A7AFF",strokeWidth:2,strokeLinecap:"round",style:{pointerEvents:"none"}}),
2340
+ h("line",{x1:p2x,y1:p2y,x2:endX,y2:endY,stroke:"#1A7AFF",strokeWidth:2,strokeLinecap:"round",style:{pointerEvents:"none"}}),
2341
+ h("circle",{cx:p1x,cy:p1y,r:5.5,fill:"#1A7AFF",style:{pointerEvents:"none",filter:"drop-shadow(0 1px 3px rgba(0,0,0,.25))"}}),
2342
+ h("circle",{cx:p2x,cy:p2y,r:5.5,fill:"#1A7AFF",style:{pointerEvents:"none",filter:"drop-shadow(0 1px 3px rgba(0,0,0,.25))"}}),
2343
+ h("circle",{className:"tl-curve-handle",cx:p1x,cy:p1y,r:9,fill:"transparent",onMouseDown:e=>startHandleDrag(0,e)}),
2344
+ h("circle",{className:"tl-curve-handle",cx:p2x,cy:p2y,r:9,fill:"transparent",onMouseDown:e=>startHandleDrag(1,e)}),
2345
+ ),
2346
+ ),
2347
+ h("div",{className:"tl-preview-sub"},
2348
+ h("span",{className:"tl-preview-sub-label"},"Position preview"),
2349
+ h("button",{className:"tl-preview-play",onClick:()=>setPlaying(p=>!p)},playing?"Pause":"Play"),
2350
+ ),
2351
+ h("div",{className:"tl-preview-track"},
2352
+ h("div",{className:"tl-preview-rail-wrap",ref:trackRef},
2353
+ h("span",{className:"tl-preview-rail"}),
2354
+ h("span",{className:"tl-preview-dot",ref:dotRef})),
2355
+ ),
2356
+ ),
2357
+ ),
2358
+ ),
2359
+ );
2360
+ }
2361
+
1927
2362
  function EasingEditor({easing, cubic, spring, durationMs, propKey, apply}){
1928
2363
  const [tab, setTab] = useState(spring ? "springs" : "easing");
1929
2364
  // when the user selects a different property, reflect that property's mode
@@ -1966,6 +2401,7 @@
1966
2401
  });
1967
2402
 
1968
2403
  return h("div",{className:"tl-ease"},
2404
+ h("div",{className:"tl-inspector-divider"}),
1969
2405
  h("div",{className:"tl-seg",role:"tablist"},
1970
2406
  h("button",{className:cx("tl-seg-btn",tab==="easing"&&"is-active"),role:"tab",
1971
2407
  "aria-selected":tab==="easing",onClick:()=>selectTab("easing")},"Easing"),
@@ -1978,6 +2414,7 @@
1978
2414
  h("div",{className:"t-page",ref:springPageRef,"data-page-id":"2","aria-hidden":tab!=="springs"},
1979
2415
  h(SpringTab,{spring,applySpring})),
1980
2416
  ),
2417
+ h(Preview,{easing, cubic, spring, durationMs, apply}),
1981
2418
  );
1982
2419
  }
1983
2420
 
@@ -2049,32 +2486,63 @@
2049
2486
  ? h("div",{key:"g"+i,className:"tl-menu-group"},o.group)
2050
2487
  : h(MenuItem,{key:"e"+i,active:selEaseIdx>=0?i===selEaseIdx:(o.value===mode),
2051
2488
  onClick:()=>pickEase(o.value),
2052
- right:(i===selEaseIdx)&&h("span",{className:"tl-menu-check"},h(Ic,{name:"check"}))},
2489
+ right:(i===selEaseIdx)&&h("span",{className:"tl-menu-check"},h(Ic,{name:"accept"}))},
2053
2490
  h("span",{className:"tl-menu-text"},o.label)))),
2054
2491
  h("div",{className:"tl-cubic-row"},
2055
- ...[0,1,2,3].map(i=>h("input",{key:i,type:"number",step:0.05,min:i%2===0?0:undefined,max:i%2===0?1:undefined,
2056
- value:cb[i],onChange:e=>setCubicVal(i,e.target.value)})),
2492
+ ...[0,1,2,3].map(i=>h("div",{key:i,className:"tl-cubic-cell"},
2493
+ h("input",{type:"number",step:0.05,min:i%2===0?0:undefined,max:i%2===0?1:undefined,
2494
+ value:cb[i],onChange:e=>setCubicVal(i,e.target.value)}))),
2057
2495
  ),
2058
2496
  mode==="__custom" && h("input",{className:"tl-custom-input",value:easing,
2059
2497
  placeholder:"e.g. steps(4, end)",
2060
2498
  onChange:e=>setEasing(e.target.value)}),
2061
- h("div",{className:"tl-curve"},
2062
- h("svg",{ref:svgRef,viewBox:`0 0 ${VBW} ${VBH}`},
2063
- h("defs",null,
2064
- h("pattern",{id:"tl-curve-dots",width:14,height:14,patternUnits:"userSpaceOnUse",patternTransform:"translate(7 7)"},
2065
- h("circle",{cx:0,cy:0,r:1,fill:"rgba(0,0,0,0.10)"}))),
2066
- h("rect",{x:0,y:0,width:VBW,height:VBH,fill:"url(#tl-curve-dots)",style:{pointerEvents:"none"}}),
2067
- h("path",{d:`M ${originX} ${originY} C ${p1x} ${p1y}, ${p2x} ${p2y}, ${endX} ${endY}`,
2068
- fill:"none",stroke:"#181a1e",strokeWidth:2,strokeLinecap:"round"}),
2069
- h("line",{x1:p1x,y1:p1y,x2:originX,y2:originY,stroke:"#1A7AFF",strokeWidth:2,strokeLinecap:"round",style:{pointerEvents:"none"}}),
2070
- h("line",{x1:p2x,y1:p2y,x2:endX,y2:endY,stroke:"#1A7AFF",strokeWidth:2,strokeLinecap:"round",style:{pointerEvents:"none"}}),
2071
- h("circle",{cx:p1x,cy:p1y,r:5.5,fill:"#1A7AFF",style:{pointerEvents:"none",filter:"drop-shadow(0 1px 3px rgba(0,0,0,.25))"}}),
2072
- h("circle",{cx:p2x,cy:p2y,r:5.5,fill:"#1A7AFF",style:{pointerEvents:"none",filter:"drop-shadow(0 1px 3px rgba(0,0,0,.25))"}}),
2073
- h("circle",{className:"tl-curve-handle",cx:p1x,cy:p1y,r:9,fill:"transparent",
2074
- onMouseDown:e=>startHandleDrag(0,e)}),
2075
- h("circle",{className:"tl-curve-handle",cx:p2x,cy:p2y,r:9,fill:"transparent",
2076
- onMouseDown:e=>startHandleDrag(1,e)}),
2077
- ),
2499
+ );
2500
+ }
2501
+
2502
+ // Spring physics scrubber — reuses the exact Figma "Value slider and input"
2503
+ // (.tl-field) component used by Duration/Delay, minus the chevron stepper.
2504
+ // Supports fractional values (Mass) via `decimals`.
2505
+ function SpringField({label, value, min, max, step, decimals, onChange}){
2506
+ const trackRef = useRef(null);
2507
+ const inputRef = useRef(null);
2508
+ const [editing,setEditing]=useState(false);
2509
+ const [draft,setDraft]=useState("");
2510
+ const [dragging,setDragging]=useState(false);
2511
+ const pct = Math.min(Math.max((value - min) / (max - min), 0), 1) * 100;
2512
+ const fmt = v => decimals ? Number(v).toFixed(decimals) : String(Math.round(v));
2513
+ const clampSnap = raw => {
2514
+ const snapped = Math.round(raw / step) * step;
2515
+ const c = Math.max(min, Math.min(max, snapped));
2516
+ const p = Math.pow(10, decimals || 0);
2517
+ return Math.round(c * p) / p;
2518
+ };
2519
+ const setFromX = useCallback(clientX => {
2520
+ if(!trackRef.current) return;
2521
+ const rect = trackRef.current.getBoundingClientRect();
2522
+ const ratio = Math.max(0, Math.min((clientX - rect.left) / rect.width, 1));
2523
+ onChange(clampSnap(min + ratio * (max - min)));
2524
+ },[min,max,step,decimals,onChange]);
2525
+ const startDrag = useCallback(e=>{
2526
+ if(editing) return;
2527
+ e.preventDefault(); setDragging(true); setFromX(e.clientX);
2528
+ const onMove = e2 => setFromX(e2.clientX);
2529
+ const onUp = () => { setDragging(false); window.removeEventListener("mousemove",onMove); window.removeEventListener("mouseup",onUp); };
2530
+ window.addEventListener("mousemove",onMove); window.addEventListener("mouseup",onUp);
2531
+ },[setFromX,editing]);
2532
+ const selectAll = useCallback(e=>{ const el=e&&e.target?e.target:inputRef.current; if(!el)return; requestAnimationFrame(()=>el.select()); },[]);
2533
+ const beginEdit = useCallback(e=>{ e.stopPropagation(); setDraft(fmt(value)); setEditing(true); },[value,decimals]);
2534
+ const commit = useCallback(()=>{ const n=parseFloat(draft); if(!isNaN(n)) onChange(clampSnap(n)); setEditing(false); },[draft,min,max,step,decimals,onChange]);
2535
+ return h("div",{className:cx("tl-field-wrap","tl-spring-field")},
2536
+ h("div",{className:cx("tl-field",dragging&&"is-dragging",editing&&"is-editing")},
2537
+ h("div",{className:"tl-field-fill",style:{width:pct+"%"}},
2538
+ h("div",{className:"tl-field-thumb"})),
2539
+ h("div",{ref:trackRef,className:"tl-field-track",onMouseDown:startDrag}),
2540
+ h("span",{className:"tl-field-label"},label),
2541
+ editing
2542
+ ? h("input",{ref:inputRef,className:"tl-field-input",autoFocus:true,value:draft,
2543
+ onChange:e=>setDraft(e.target.value),onBlur:commit,onFocus:selectAll,
2544
+ onKeyDown:e=>{if(e.key==="Enter")commit();if(e.key==="Escape")setEditing(false);}})
2545
+ : h("span",{className:"tl-field-value",onMouseDown:e=>e.stopPropagation(),onClick:beginEdit},fmt(value)),
2078
2546
  ),
2079
2547
  );
2080
2548
  }
@@ -2095,17 +2563,6 @@
2095
2563
  applySpring(next.stiffness, next.damping, next.mass, matchSpringPreset(next));
2096
2564
  },[stiffness,damping,mass,applySpring]);
2097
2565
 
2098
- // spring curve — auto-fit vertically so overshoot above 1 stays in frame.
2099
- const { VBW, VBH, PAD_X, PAD_Y } = CURVE;
2100
- const plotW = VBW - 2*PAD_X, plotH = VBH - 2*PAD_Y;
2101
- const vals = sim.values;
2102
- const hi = Math.max(1, ...vals), lo = Math.min(0, ...vals);
2103
- const range = (hi - lo) || 1;
2104
- const sx = i => PAD_X + (i/(vals.length-1))*plotW;
2105
- const sy = v => (VBH - PAD_Y) - ((v - lo)/range)*plotH;
2106
- const yOne = sy(1), yZero = sy(0);
2107
- const curveD = vals.map((v,i)=>`${i===0?"M":"L"} ${sx(i).toFixed(2)} ${sy(v).toFixed(2)}`).join(" ");
2108
-
2109
2566
  return h(React.Fragment,null,
2110
2567
  h("button",{ref:spRef,className:cx("tl-select",spOpen&&"is-open"),onClick:()=>setSpOpen(v=>!v)},
2111
2568
  h("span",{className:"tl-select-label"},presetName==="Custom"?"Custom spring":presetName),
@@ -2114,47 +2571,19 @@
2114
2571
  width:Math.max(248,(spRef.current&&spRef.current.offsetWidth)||248),align:"left"},
2115
2572
  SPRING_PRESETS.map(p=>h(MenuItem,{key:p.label,active:p.label===presetName,
2116
2573
  onClick:()=>{applySpring(p.stiffness,p.damping,p.mass,p.label);setSpOpen(false);},
2117
- right:p.label===presetName&&h("span",{className:"tl-menu-check"},h(Ic,{name:"check"}))},
2574
+ right:p.label===presetName&&h("span",{className:"tl-menu-check"},h(Ic,{name:"accept"}))},
2118
2575
  h("span",{className:"tl-menu-text"},p.label)))),
2119
- h("div",{className:"tl-bounce"},
2120
- h("div",{className:"tl-bounce-title"},"Spring physics"),
2121
- h("div",{className:"tl-bounce-row"},
2122
- h("label",null,"Stiffness"),
2123
- h("input",{type:"range",min:20,max:400,step:5,value:stiffness,
2124
- onChange:e=>setParam("stiffness",parseFloat(e.target.value))}),
2125
- h("span",{className:"tl-bounce-val"},Math.round(stiffness)),
2126
- ),
2127
- h("div",{className:"tl-bounce-row"},
2128
- h("label",null,"Damping"),
2129
- h("input",{type:"range",min:1,max:120,step:1,value:damping,
2130
- onChange:e=>setParam("damping",parseFloat(e.target.value))}),
2131
- h("span",{className:"tl-bounce-val"},Math.round(damping)),
2132
- ),
2133
- h("div",{className:"tl-bounce-row"},
2134
- h("label",null,"Mass"),
2135
- h("input",{type:"range",min:0.2,max:5,step:0.1,value:mass,
2136
- onChange:e=>setParam("mass",parseFloat(e.target.value))}),
2137
- h("span",{className:"tl-bounce-val"},mass.toFixed(1)),
2138
- ),
2139
- h("div",{className:"tl-spring-dur"},
2140
- h("span",null,"Duration"),
2141
- h("b",null,"~"+sim.durationMs+"ms"),
2142
- h("span",{className:"tl-spring-dur-hint"},"· derived"+(overshoots?" · overshoots":"")),
2143
- ),
2576
+ h("div",{className:"tl-spring-fields"},
2577
+ h(SpringField,{label:"Stiffness",value:stiffness,min:20,max:400,step:5,
2578
+ onChange:v=>setParam("stiffness",v)}),
2579
+ h(SpringField,{label:"Damping",value:damping,min:1,max:120,step:1,
2580
+ onChange:v=>setParam("damping",v)}),
2581
+ h(SpringField,{label:"Mass",value:mass,min:0.2,max:5,step:0.1,decimals:1,
2582
+ onChange:v=>setParam("mass",v)}),
2144
2583
  ),
2145
- h("div",{className:"tl-curve"},
2146
- h("svg",{viewBox:`0 0 ${VBW} ${VBH}`},
2147
- h("defs",null,
2148
- h("pattern",{id:"tl-spring-dots",width:14,height:14,patternUnits:"userSpaceOnUse",patternTransform:"translate(7 7)"},
2149
- h("circle",{cx:0,cy:0,r:1,fill:"rgba(0,0,0,0.10)"}))),
2150
- h("rect",{x:0,y:0,width:VBW,height:VBH,fill:"url(#tl-spring-dots)",style:{pointerEvents:"none"}}),
2151
- // target (value = 1) and baseline (value = 0) reference lines
2152
- h("line",{x1:PAD_X,y1:yOne,x2:VBW-PAD_X,y2:yOne,stroke:"#1A7AFF",strokeWidth:1,
2153
- strokeDasharray:"3 4",style:{opacity:0.5,pointerEvents:"none"}}),
2154
- h("line",{x1:PAD_X,y1:yZero,x2:VBW-PAD_X,y2:yZero,stroke:"rgba(0,0,0,0.18)",strokeWidth:1,
2155
- strokeDasharray:"3 4",style:{pointerEvents:"none"}}),
2156
- h("path",{d:curveD,fill:"none",stroke:"#181a1e",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}),
2157
- ),
2584
+ h("div",{className:"tl-spring-dur"},
2585
+ "Duration ", h("b",null,sim.durationMs+"ms"), " Derived",
2586
+ overshoots&&h("span",{className:"tl-spring-dur-hint"}," · overshoots"),
2158
2587
  ),
2159
2588
  );
2160
2589
  }
@@ -2268,6 +2697,22 @@
2268
2697
  const{setPropOverride}=usePropOverride();
2269
2698
  const{registry}=useContext(TimelineCtx);
2270
2699
  const[copied,setCopied]=useState(false);
2700
+ // tiny transient toast (e.g. "Values copied"); {msg,id,closing,loader}
2701
+ // loader toasts (e.g. "Agent accepting changes") are sticky — they stay up
2702
+ // with a dot-matrix loader until dismissed by the caller, no auto-hide.
2703
+ const[toast,setToast]=useState(null);
2704
+ const showToast=useCallback((msg,opts)=>{setToast({msg,id:Date.now(),closing:false,loader:!!(opts&&opts.loader)});},[]);
2705
+ const dismissToast=useCallback(()=>{setToast(t=>t&&!t.closing?{...t,closing:true}:t);},[]);
2706
+ useEffect(()=>{
2707
+ if(!toast||toast.closing||toast.loader)return;
2708
+ const hide=setTimeout(()=>setToast(t=>t&&t.id===toast.id?{...t,closing:true}:t),1800);
2709
+ return()=>clearTimeout(hide);
2710
+ },[toast&&toast.id,toast&&toast.closing,toast&&toast.loader]);
2711
+ useEffect(()=>{
2712
+ if(!toast||!toast.closing)return;
2713
+ const kill=setTimeout(()=>setToast(t=>t&&t.id===toast.id?null:t),360);
2714
+ return()=>clearTimeout(kill);
2715
+ },[toast&&toast.id,toast&&toast.closing]);
2271
2716
  const[minimized,setMinimized]=useState(false);
2272
2717
  const[panelHeight,setPanelHeight]=useState(440);
2273
2718
  const[resizing,setResizing]=useState(false);
@@ -2285,6 +2730,9 @@
2285
2730
  const[acceptError,setAcceptError]=useState(null);
2286
2731
  // ── grouped scan (agent reads source → Open/Close phases) ──
2287
2732
  const[groupScanState,setGroupScanState]=useState("idle"); // idle | scanning | done | error
2733
+ // Escape hatch for the gate: if a scan errors/times out the user can choose
2734
+ // to proceed with the flat (ungrouped) list instead of being trapped.
2735
+ const[skipGrouping,setSkipGrouping]=useState(false);
2288
2736
  const didGroupScanRef=useRef(false);
2289
2737
  const[refineLabel,setRefineLabel]=useState(null);
2290
2738
  const[appliedIds,setAppliedIds]=useState({});
@@ -2298,6 +2746,17 @@
2298
2746
  setCliInstalled(j.cliInstalled==null?null:!!j.cliInstalled);return j;}
2299
2747
  catch{setLlmAvailable(false);setCliInstalled(false);return null;}
2300
2748
  },[]);
2749
+ // a live agent = the relay reports an agent it can drive (REFINE_AGENT_CMD
2750
+ // wired OR a `/refine live` loop polling). Until then the panel is gated.
2751
+ const live = llmAvailable===true;
2752
+ // poll /health continuously so the panel reacts when `/refine live` is run
2753
+ // (gate → scanning) or when the agent disconnects (re-gate). 2s cadence.
2754
+ useEffect(()=>{
2755
+ let alive=true;
2756
+ refreshHealth();
2757
+ const id=setInterval(()=>{if(alive)refreshHealth();},2000);
2758
+ return()=>{alive=false;clearInterval(id);};
2759
+ },[refreshHealth]);
2301
2760
  // the Refine button toggles the panel; opening drops into the idle state
2302
2761
  // (description + Start scanning) rather than auto-scanning.
2303
2762
  const openRefine=useCallback(()=>{
@@ -2320,7 +2779,7 @@
2320
2779
  if(mode!==refineMode)setRefineMode(mode);
2321
2780
  try{
2322
2781
  const et=active.effectiveTimings||[];
2323
- const timings=et.map(t=>({property:t.property,durationMs:t.durationMs,delayMs:t.delayMs,easing:t.easing}));
2782
+ const timings=et.map(t=>({property:t.property,member:t.member||null,durationMs:t.durationMs,delayMs:t.delayMs,easing:t.easing}));
2324
2783
  const selector=(active.bindings&&active.bindings.selector)||(et[0]&&et[0].selector)||null;
2325
2784
  // always scan for BOTH kinds (token tweaks + a whole-transition replacement)
2326
2785
  // in one pass; the tabs just filter which kind they show.
@@ -2452,8 +2911,9 @@
2452
2911
  }else{
2453
2912
  text="transition: "+et.map(decl).join(",\n ")+";";
2454
2913
  }
2455
- navigator.clipboard.writeText(text).then(()=>{setCopied(true);setTimeout(()=>setCopied(false),1500);});
2456
- },[active]);
2914
+ navigator.clipboard.writeText(text).then(()=>{setCopied(true);setTimeout(()=>setCopied(false),1500);}).catch(()=>{});
2915
+ showToast("Values copied");
2916
+ },[active,showToast]);
2457
2917
  const resetOverrides=useCallback(()=>{if(active)registry.clearOverride(active.id);},[registry,active]);
2458
2918
  // Accept → send an "apply" job so the agent writes the edited timings into
2459
2919
  // the user's source, then reflect saving / done / error on the button.
@@ -2486,6 +2946,15 @@
2486
2946
  },[active]);
2487
2947
  // reset Accept feedback when switching transitions
2488
2948
  useEffect(()=>{setAcceptState("idle");setAcceptError(null);},[active&&active.id]);
2949
+ // while the agent is applying changes, surface the sticky loader toast
2950
+ // ("Agent accepting changes"); dismiss only when leaving saving (not on
2951
+ // every idle tick — that would kill unrelated toasts like "Values copied").
2952
+ const prevAcceptRef=useRef(acceptState);
2953
+ useEffect(()=>{
2954
+ if(acceptState==="saving"){showToast("Agent accepting changes",{loader:true});}
2955
+ else if(prevAcceptRef.current==="saving"){dismissToast();}
2956
+ prevAcceptRef.current=acceptState;
2957
+ },[acceptState,showToast,dismissToast]);
2489
2958
  // The grouped scan asks the agent to read the source and return Open/Close
2490
2959
  // phases — an expensive LLM round-trip. So it runs ONLY:
2491
2960
  // 1. the first time this page is ever opened (nothing cached), or
@@ -2556,18 +3025,21 @@
2556
3025
  const rescanTransitions=useCallback(()=>{
2557
3026
  try{localStorage.removeItem(GROUP_STORE_KEY);}catch{}
2558
3027
  registry.clearGroups();
3028
+ setSkipGrouping(false); // re-engage the gate so a fresh scan can be shown
2559
3029
  runGroupScan();
2560
3030
  },[runGroupScan,GROUP_STORE_KEY,registry]);
2561
- // On mount, run the group resolver once. It waits for the flat scan to
2562
- // settle, then either re-applies cached groups (when the content signature
2563
- // matches a pure cache read, no agent) or kicks off a fresh agent scan
2564
- // (first open OR the page's transitions changed since last cache).
3031
+ // Gate the auto group-scan behind a live agent: the panel stays on the
3032
+ // "Before we start" screen until `/refine live` (or a wired REFINE_AGENT_CMD)
3033
+ // makes the relay report an agent. The first time we see a live agent we run
3034
+ // the resolver once — it settles the flat scan, then re-applies cached groups
3035
+ // (signature match, no agent) or kicks off a fresh agent scan. Rescan is
3036
+ // manual after that.
2565
3037
  useEffect(()=>{
2566
- if(didGroupScanRef.current)return;
3038
+ if(!live||didGroupScanRef.current)return;
2567
3039
  didGroupScanRef.current=true;
2568
3040
  runGroupScan();
2569
- return ()=>{scanTokenRef.current++;};
2570
- },[runGroupScan]);
3041
+ },[live,runGroupScan]);
3042
+ useEffect(()=>()=>{scanTokenRef.current++;},[]);
2571
3043
 
2572
3044
  // whole-component open/close uses the transitions.dev panel reveal:
2573
3045
  // keep the panel mounted while it animates, flip data-open on the next
@@ -2609,23 +3081,73 @@
2609
3081
  return()=>window.removeEventListener("tl-toggle-panel",onToggle);
2610
3082
  },[]);
2611
3083
 
3084
+ // gate: the panel is unusable until a live agent is connected AND it has
3085
+ // scanned the page's transitions.
3086
+ // loading → first /health probe pending (blank, avoids a text flash)
3087
+ // blocked → no live agent → "Before we start" (run /refine live)
3088
+ // scanning → live agent, scan in flight → "Agent is scanning…"
3089
+ // scan-error → scan failed/timed out → recoverable (retry / continue)
3090
+ // ready → live + scan settled (done or idle, or user skipped) → real UI
3091
+ // NOTE: "idle" and "error" must NOT keep us on the scanning screen, or a
3092
+ // timed-out scan (e.g. an older /refine-live skill that can't answer scan
3093
+ // jobs) would trap the panel forever with no way out.
3094
+ const gate = !live
3095
+ ? (llmAvailable===null ? "loading" : "blocked")
3096
+ : groupScanState==="scanning" ? "scanning"
3097
+ : (groupScanState==="error" && !skipGrouping) ? "scan-error"
3098
+ : "ready";
2612
3099
  return h(React.Fragment,null,
2613
3100
  render&&h("div",{className:"t-panel-slide","data-timeline-panel":true,
2614
3101
  "data-open":panelOpen?"true":"false","data-phase":phase,style:{height:panelHeight+"px"}},
2615
3102
  h("div",{className:cx("tl-resize-handle",resizing&&"dragging"),onMouseDown:startResize,title:"Drag to resize"}),
2616
3103
  h("div",{className:"tl-panel-body"},
2617
- h("div",{className:"tl-panel-main"},
2618
- h(Header,{entries,active,onSelect:setActiveId,onReset:resetOverrides,onCopy:copyValues,copied,
2619
- snap,setSnap,onMinimize:()=>setMinimized(true),onRefine:openRefine,refineActive:refineOpen,
2620
- onAccept,acceptState,acceptDisabled:computeChanges(active).length===0,acceptError,
2621
- scanning:groupScanState==="scanning",onRescan:rescanTransitions}),
2622
- active
2623
- ?h(Body,{entry:active,onPropChange:(prop,o)=>setPropOverride(prop,o),snap})
2624
- :h("div",{className:"tl-empty"},"Select a transition to inspect and edit it")),
2625
- h(RefinePanel,{open:refineOpen,onClose:()=>setRefineOpen(false),phase:refinePhase,label:refineLabel,
2626
- refineType,onType:changeRefineType,suggestions:refineSuggestions,summary:refineSummary,error:refineError,
2627
- appliedIds,onApply:applySuggestion,onApplyAll:applyAllSuggestions,
2628
- mode:refineMode,onMode:changeRefineMode,llmAvailable,cliInstalled,onStart:startScan}),
3104
+ gate==="ready"
3105
+ ? h(React.Fragment,null,
3106
+ h("div",{className:"tl-panel-main"},
3107
+ h(Header,{entries,active,onSelect:setActiveId,onReset:resetOverrides,onCopy:copyValues,copied,
3108
+ snap,setSnap,onMinimize:()=>setMinimized(true),onRefine:openRefine,refineActive:refineOpen,
3109
+ onAccept,acceptState,acceptDisabled:computeChanges(active).length===0,acceptError,
3110
+ scanning:groupScanState==="scanning",onRescan:rescanTransitions}),
3111
+ active
3112
+ ?h(Body,{entry:active,onPropChange:(prop,o)=>setPropOverride(prop,o),snap})
3113
+ :h("div",{className:"tl-empty"},"Select a transition to inspect and edit it")),
3114
+ h(RefinePanel,{open:refineOpen,onClose:()=>setRefineOpen(false),phase:refinePhase,label:refineLabel,
3115
+ refineType,onType:changeRefineType,suggestions:refineSuggestions,summary:refineSummary,error:refineError,
3116
+ appliedIds,onApply:applySuggestion,onApplyAll:applyAllSuggestions,
3117
+ mode:refineMode,onMode:changeRefineMode,llmAvailable,cliInstalled,onStart:startScan,
3118
+ lanes:active?.effectiveTimings||[]}))
3119
+ : h("div",{className:"tl-panel-main"},
3120
+ gate==="blocked" && h("div",{className:"tl-gate"},
3121
+ h("div",{className:"tl-gate-col"},
3122
+ h("div",{className:"tl-gate-title"},"Before we start"),
3123
+ h("p",{className:"tl-gate-text"},
3124
+ "Please run the ",h("code",{className:"tl-code"},"/refine live"),
3125
+ " command in your agent to enable live features, such as scanning and refining transitions."))),
3126
+ gate==="scanning" && h("div",{className:"tl-gate"},
3127
+ h("div",{className:"tl-gate-col"},
3128
+ h("div",{className:"tl-gate-pill-wrap"},
3129
+ h(BorderBeam,{size:"sm",colorVariant:"ocean",theme:"light",borderRadius:18,duration:2.6,saturation:2,brightness:1.6,className:"tl-gate-beam"},
3130
+ h("span",{className:"tl-gate-beam-fill"})),
3131
+ h("span",{className:"tl-gate-pill"},
3132
+ h(DotmLoader),
3133
+ h("span",{className:"tl-gate-pill-label"},"Agent is scanning your transitions"))),
3134
+ h("p",{className:"tl-gate-sub"},"Just a moment while we get things ready."))),
3135
+ gate==="scan-error" && h("div",{className:"tl-gate"},
3136
+ h("div",{className:"tl-gate-col"},
3137
+ h("div",{className:"tl-gate-title"},"We couldn’t scan your transitions"),
3138
+ h("p",{className:"tl-gate-text"},
3139
+ "The agent didn’t finish grouping. Make sure ",h("code",{className:"tl-code"},"/refine live"),
3140
+ " is running with the latest skill, then try again — or continue with the ungrouped list."),
3141
+ h("div",{className:"tl-gate-actions"},
3142
+ h("button",{className:"tl-gate-btn tl-gate-btn-primary",onClick:rescanTransitions},"Try again"),
3143
+ h("button",{className:"tl-gate-btn",onClick:()=>setSkipGrouping(true)},"Continue without grouping"))))),
3144
+ toast&&createPortal(
3145
+ h("div",{className:"tl-toast-wrap","aria-live":"polite"},
3146
+ h("div",{className:cx("tl-toast",toast.closing&&"is-closing")},
3147
+ h("span",{className:"tl-toast-ic"},
3148
+ toast.loader?h(DotmLoader):h(Ic,{name:"accept",size:14})),
3149
+ h("span",null,toast.msg))),
3150
+ document.body),
2629
3151
  ),
2630
3152
  ),
2631
3153
  minimized&&h("div",{className:"tl-pill",onClick:()=>setMinimized(false)},