@kitlangton/knobs 0.4.0-next.2 → 0.4.0-next.4

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/dist/ui-solid.js CHANGED
@@ -570,12 +570,20 @@ var compareCss = `
570
570
  display:flex;flex-direction:column;gap:6px;width:100%;height:100%;min-height:160px;outline:none;user-select:none;-webkit-user-select:none;
571
571
  font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg);letter-spacing:normal;text-align:left}
572
572
  :where(.knobs-ui) :is(.knobs-cmp-mode,.knobs-cmp-toggle){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;background:transparent;border:0;margin:0;padding:0;outline:none;box-shadow:none}
573
- .knobs-cmp-bar{flex:none;display:flex;align-items:center;gap:2px;height:24px;padding:0 2px}
574
- .knobs-cmp-modes{display:flex;align-items:center;gap:2px}
575
- .knobs-ui .knobs-cmp-mode,.knobs-ui .knobs-cmp-toggle{height:22px;padding:0 7px;border-radius:max(0px,calc(var(--_k-radius) - 2px));color:var(--_k-fg-3);cursor:pointer;white-space:nowrap;
576
- transition:color .15s,background .15s}
577
- .knobs-ui .knobs-cmp-mode:is(:hover,:focus-visible),.knobs-ui .knobs-cmp-toggle:is(:hover,:focus-visible){color:var(--_k-fg);background:var(--_ku-wash)}
578
- .knobs-ui .knobs-cmp-mode[aria-checked="true"],.knobs-ui .knobs-cmp-toggle[aria-pressed="true"]{color:var(--_k-fg);font-weight:var(--_k-weight-label)}
573
+ .knobs-cmp-bar{flex:none;display:flex;align-items:center;gap:var(--_k-gap);height:var(--_k-control-h)}
574
+ /* The modes: one fill, the selection pill on the one picked; a mode isn't brighter for being
575
+ picked: the pill's bright copy is. */
576
+ .knobs-cmp-modes{position:relative;display:inline-flex;height:var(--_k-control-h);border-radius:var(--_k-radius);background:var(--_k-fill)}
577
+ .knobs-cmp-modes[hidden]{display:none}
578
+ .knobs-ui .knobs-cmp-mode{display:inline-flex;align-items:center;height:100%;padding:0 8px;border-radius:var(--_k-radius);color:var(--_k-fg-2);cursor:pointer;white-space:nowrap;
579
+ transition:color calc(var(--_k-hover-out) * 1ms)}
580
+ .knobs-ui .knobs-cmp-mode:is(:hover,:focus-visible){color:var(--_k-fg)}
581
+ /* Toggles: icon buttons, the accent's wash when on. */
582
+ .knobs-ui .knobs-cmp-toggle{display:inline-flex;align-items:center;justify-content:center;width:var(--_k-control-h);height:var(--_k-control-h);border-radius:var(--_k-radius);color:var(--_k-fg-3);cursor:pointer;
583
+ transition:color calc(var(--_k-hover-out) * 1ms),background calc(var(--_k-hover-out) * 1ms)}
584
+ .knobs-ui .knobs-cmp-toggle:is(:hover,:focus-visible){color:var(--_k-fg);background:var(--_ku-wash)}
585
+ .knobs-ui .knobs-cmp-toggle[aria-pressed="true"]{color:var(--_k-accent-fg);background:var(--_k-accent-soft)}
586
+ .knobs-cmp-toggle svg{display:block}
579
587
  .knobs-cmp-fill{flex:1}
580
588
  .knobs-cmp-toggle[hidden]{display:none}
581
589
  /* One surface: on a card (a walkthrough's step, a split's pane, anything .knobs-surface) or marked
@@ -590,7 +598,8 @@ var compareCss = `
590
598
  /* Unzoomed, a finger still scrolls the page up and down; zoomed, the stage takes every gesture. */
591
599
  .knobs-compare[data-zoomed] .knobs-cmp-stage{cursor:grab;touch-action:none}
592
600
  .knobs-compare[data-panning],.knobs-compare[data-panning] *{cursor:grabbing!important}
593
- .knobs-compare[data-dragging],.knobs-compare[data-dragging] *{cursor:ew-resize!important}
601
+ /* Wiping, the grip stands in for the cursor. */
602
+ .knobs-compare[data-dragging],.knobs-compare[data-dragging] *{cursor:none!important}
594
603
  .knobs-cmp-sizer{position:absolute;left:0;right:0;top:0;height:0;visibility:hidden;pointer-events:none}
595
604
  .knobs-cmp-canvas{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none}
596
605
  .knobs-cmp-media{position:absolute;left:0;top:0;display:block;max-width:none;margin:0;padding:0;border:0;object-fit:fill;transform-origin:0 0;pointer-events:none;will-change:transform}
@@ -598,18 +607,26 @@ var compareCss = `
598
607
  /* Glass: what's behind shows through, blurred, on the knobs tokens. */
599
608
  .knobs-cmp-label,.knobs-cmp-zoom{background:color-mix(in srgb,var(--_k-bg) 52%,transparent);-webkit-backdrop-filter:blur(14px) saturate(1.6);backdrop-filter:blur(14px) saturate(1.6);
600
609
  box-shadow:inset 0 0 0 .5px var(--_k-border),inset 0 1px 0 var(--_k-rim),0 2px 10px rgba(0,0,0,calc(.18 * var(--_k-shadow-k,1)))}
601
- .knobs-cmp-label{position:absolute;left:0;top:0;padding:1px 7px;border-radius:999px;font-size:var(--_k-text-meta);line-height:16px;color:var(--_k-fg-2);white-space:nowrap;pointer-events:none;will-change:transform;transition:color .15s}
602
- .knobs-cmp-label[data-lit]{color:var(--_k-fg)}
603
- .knobs-cmp-label[data-dim]{color:var(--_k-fg-3)}
604
- /* The wipe: a hairline down the line with a grip at its middle; a wider strip takes the hand and
605
- the keys (a slider). It fades with the mode. */
610
+ /* ✗ Before · ✓ After: Before quieter (fg-3), After in full text. Neutral: no colour of its own. */
611
+ .knobs-cmp-label{position:absolute;left:0;top:0;display:flex;align-items:center;gap:4px;padding:1px 7px 1px 5px;border-radius:999px;font-size:var(--_k-text-meta);line-height:16px;font-weight:var(--_k-weight-label);
612
+ white-space:nowrap;pointer-events:none;will-change:transform}
613
+ .knobs-cmp-label[data-side="before"]{color:var(--_k-fg-3)}
614
+ .knobs-cmp-label[data-side="after"]{color:var(--_k-fg)}
615
+ .knobs-cmp-mark svg{display:block;width:12px;height:12px}
616
+ /* The wipe: a hairline down the line with a raised grip at the pointer's height; a wider strip
617
+ takes the hand and the keys (a slider). It fades with the mode; wiping, line and grip brighten. */
606
618
  .knobs-cmp-handle{cursor:ew-resize;position:absolute;left:-12px;top:0;width:24px;height:0;opacity:0;pointer-events:none;outline:none;will-change:transform;transition:opacity .18s cubic-bezier(.4,0,1,1)}
607
619
  .knobs-cmp-handle[data-on]{opacity:1;pointer-events:auto;transition:opacity .22s cubic-bezier(.2,.8,.2,1)}
608
- .knobs-cmp-handle::before{content:"";position:absolute;top:0;bottom:0;left:11.5px;width:1px;background:var(--_ku-cmp-line);box-shadow:0 0 0 .5px rgba(0,0,0,.28),0 0 6px rgba(0,0,0,.25)}
609
- .knobs-cmp-handle::after{content:"";position:absolute;top:50%;left:9px;width:6px;height:28px;margin-top:-14px;border-radius:3px;background:var(--_ku-cmp-line);
610
- box-shadow:0 1px 3px rgba(0,0,0,.35),0 0 0 .5px rgba(0,0,0,.2);transition:scale .18s var(--_ku-ease),box-shadow .15s}
611
- .knobs-compare[data-dragging] .knobs-cmp-handle::after{scale:1.12}
612
- .knobs-cmp-handle:focus-visible::after,.knobs-compare[data-keyboard] .knobs-cmp-handle:focus::after{box-shadow:0 1px 3px rgba(0,0,0,.35),0 0 0 3px color-mix(in srgb,var(--_k-accent) 40%,transparent)}
620
+ .knobs-cmp-handle::before{content:"";position:absolute;top:0;bottom:0;left:11.5px;width:1px;background:color-mix(in srgb,var(--_ku-cmp-line) 72%,transparent);box-shadow:0 0 0 .5px rgba(0,0,0,.28),0 0 6px rgba(0,0,0,.25)}
621
+ .knobs-compare[data-dragging] .knobs-cmp-handle::before{background:var(--_ku-cmp-line)}
622
+ .knobs-cmp-grip{position:absolute;top:0;left:4px;width:16px;height:28px;border-radius:min(8px,calc(var(--_k-radius) * 999));background:var(--_k-bg-overlay);
623
+ box-shadow:var(--_k-shadow-overlay),inset 0 0 0 1px var(--_k-border),inset 0 1px 0 var(--_k-rim);will-change:transform;transition:box-shadow calc(var(--_k-hover-out) * 1ms)}
624
+ .knobs-cmp-grip::before,.knobs-cmp-grip::after{content:"";position:absolute;top:10px;width:1px;height:8px;background:var(--_k-fg-3);transition:background calc(var(--_k-hover-out) * 1ms)}
625
+ .knobs-cmp-grip::before{left:6px}
626
+ .knobs-cmp-grip::after{left:9px}
627
+ .knobs-compare[data-dragging] .knobs-cmp-grip{box-shadow:var(--_k-shadow-overlay),inset 0 0 0 1px var(--_k-border-strong),inset 0 1px 0 var(--_k-rim)}
628
+ .knobs-compare[data-dragging] .knobs-cmp-grip::before,.knobs-compare[data-dragging] .knobs-cmp-grip::after{background:var(--_k-fg)}
629
+ .knobs-cmp-handle:focus-visible .knobs-cmp-grip,.knobs-compare[data-keyboard] .knobs-cmp-handle:focus .knobs-cmp-grip{box-shadow:var(--_k-shadow-overlay),0 0 0 2px var(--_k-accent)}
613
630
  /* The split's hairline, down the middle side by side. */
614
631
  .knobs-cmp-split{position:absolute;left:0;top:0;width:1px;height:0;margin-left:-.5px;background:color-mix(in srgb,var(--_k-bg) 72%,transparent);pointer-events:none;will-change:transform,opacity}
615
632
  /* Zoomed: how far, and a way back, in the stage's corner; it fades in and out, nothing shifts. */
@@ -618,12 +635,12 @@ var compareCss = `
618
635
  opacity:0;scale:.96;pointer-events:none;transition:opacity .16s cubic-bezier(.4,0,1,1),scale .16s cubic-bezier(.4,0,1,1)}
619
636
  .knobs-cmp-zoom[data-on]{opacity:1;scale:1;pointer-events:auto;transition:opacity .2s cubic-bezier(.2,.8,.2,1),scale .2s cubic-bezier(.2,.8,.2,1)}
620
637
  :where(.knobs-ui) .knobs-cmp-zoom-reset{all:revert;box-sizing:border-box;font:inherit;letter-spacing:inherit;margin:0;border:0;outline:none;box-shadow:none;
621
- height:20px;padding:0 8px;border-radius:999px;background:transparent;color:var(--_k-fg-2);cursor:pointer;transition:color .15s,background .15s}
638
+ display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border-radius:999px;background:transparent;color:var(--_k-fg-2);cursor:pointer;transition:color .15s,background .15s}
622
639
  :where(.knobs-ui) .knobs-cmp-zoom-reset:is(:hover,:focus-visible){color:var(--_k-fg);background:color-mix(in srgb,var(--_k-fg) 12%,transparent)}
623
640
  .knobs-cmp-note{position:absolute;left:50%;top:50%;translate:-50% -50%;color:var(--_k-fg-3);font-size:var(--_k-text-meta);pointer-events:none}
624
641
  .knobs-cmp-note:empty{display:none}
625
642
  .knobs-cmp-strip-host{flex:none;height:60px;min-height:0}
626
- .knobs-cmp-timeline-host{flex:none;height:56px;min-height:0}
643
+ .knobs-cmp-timeline-host{flex:none;min-height:0}
627
644
  /* The frame strip: Before over After per column, a difference bar under them, a dot where it peaks. */
628
645
  .knobs-ui.knobs-cmp-strip{--_ku-cmp-diff:var(--k-compare-diff,var(--_k-ok));position:relative;width:100%;height:100%;min-height:40px;overflow:hidden;cursor:col-resize;touch-action:none;
629
646
  font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg)}
@@ -766,7 +783,7 @@ var init_picker_css = __esm(() => {
766
783
  .knobs-picker-suggested{flex:none;color:var(--_k-fg-3);font-size:var(--_k-text-meta)}
767
784
  .knobs-picker-check{flex:none;display:flex;width:14px;height:14px;color:var(--_k-accent);opacity:0;scale:.6;filter:blur(2px);
768
785
  transition:opacity .11s ease-in,scale .11s ease-in,filter .11s ease-in}
769
- .knobs-picker-check svg{display:block;width:14px;height:14px}
786
+ .knobs-picker-check svg{display:block;width:12px;height:12px}
770
787
  .knobs-picker-option[data-chosen] .knobs-picker-check{opacity:1;scale:1;filter:none;transition:opacity .15s ease-out,scale var(--kp-snappy-ms) var(--kp-snappy),filter .15s ease-out}
771
788
  .knobs-picker-body{position:relative;overflow:visible}
772
789
  .knobs-picker-body[data-moving]{overflow:hidden}
@@ -870,6 +887,15 @@ var readoutCss = `
870
887
  @media(prefers-reduced-motion:reduce){.knobs-rd :where(*){transition:none!important}}
871
888
  `;
872
889
 
890
+ // src/ui/selection-pill-css.ts
891
+ var selectionPillCss = `
892
+ .knobs-pick-pill,.knobs-pick-copy{position:absolute;left:0;top:0;width:0;height:0;pointer-events:none;transition:opacity .1s linear}
893
+ .knobs-pick-pill{background:var(--_k-fill-selected);will-change:transform}
894
+ .knobs-pick-copy{overflow:hidden;will-change:transform}
895
+ .knobs-pick-copy-rows{position:absolute;left:0;top:0;width:0;height:0}
896
+ :is(.knobs-pick-pill,.knobs-pick-copy)[data-off]{opacity:0}
897
+ `;
898
+
873
899
  // src/tokens/color.ts
874
900
  function linearToOklch([r, g, b, a]) {
875
901
  const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
@@ -1310,6 +1336,22 @@ var init_schema = __esm(() => {
1310
1336
  source: css("var(--k-warn)"),
1311
1337
  doc: "Modified lines."
1312
1338
  },
1339
+ {
1340
+ name: "before",
1341
+ tier: "semantic",
1342
+ type: "color",
1343
+ group: "Data",
1344
+ source: css("var(--k-fg-3)"),
1345
+ doc: "Before in a comparison, and only Before: its label and side of the seam. Neutral unless a theme gives it a colour."
1346
+ },
1347
+ {
1348
+ name: "after",
1349
+ tier: "semantic",
1350
+ type: "color",
1351
+ group: "Data",
1352
+ source: css("var(--k-fg)"),
1353
+ doc: "After in a comparison, and only After."
1354
+ },
1313
1355
  {
1314
1356
  name: "series-1",
1315
1357
  tier: "semantic",
@@ -1400,6 +1442,150 @@ var init_schema = __esm(() => {
1400
1442
  source: value("288px"),
1401
1443
  doc: "Panel width."
1402
1444
  },
1445
+ {
1446
+ name: "space-inside",
1447
+ tier: "semantic",
1448
+ type: "length",
1449
+ group: "Space",
1450
+ source: css("var(--k-unit)"),
1451
+ doc: "Parts of one thing: an icon and its label, a meta line and its title."
1452
+ },
1453
+ {
1454
+ name: "space-sibling",
1455
+ tier: "semantic",
1456
+ type: "length",
1457
+ group: "Space",
1458
+ source: css("calc(var(--k-unit) * 2)"),
1459
+ doc: "Siblings in a group: buttons in a row, a list item's lines."
1460
+ },
1461
+ {
1462
+ name: "space-title",
1463
+ tier: "semantic",
1464
+ type: "length",
1465
+ group: "Space",
1466
+ source: css("calc(var(--k-unit) * 3)"),
1467
+ doc: "A heading and what it heads."
1468
+ },
1469
+ {
1470
+ name: "space-group",
1471
+ tier: "semantic",
1472
+ type: "length",
1473
+ group: "Space",
1474
+ source: css("calc(var(--k-unit) * 12)"),
1475
+ doc: "Groups inside a section (prose, video, diff), stacked media, feed items (a hairline in the middle)."
1476
+ },
1477
+ {
1478
+ name: "space-section",
1479
+ tier: "semantic",
1480
+ type: "length",
1481
+ group: "Space",
1482
+ source: css("calc(var(--k-unit) * 24)"),
1483
+ doc: "Sections of a page (the intro, the updates, each PR), a hairline in the middle."
1484
+ },
1485
+ {
1486
+ name: "space-column",
1487
+ tier: "semantic",
1488
+ type: "length",
1489
+ group: "Space",
1490
+ source: css("calc(var(--k-unit) * 6)"),
1491
+ doc: "Columns: a contents rail and the page, media side by side."
1492
+ },
1493
+ {
1494
+ name: "pad-page",
1495
+ tier: "semantic",
1496
+ type: "length",
1497
+ group: "Space",
1498
+ source: css("calc(var(--k-unit) * 6)"),
1499
+ doc: "A page's edge; 16px under 640px wide."
1500
+ },
1501
+ {
1502
+ name: "pad-card",
1503
+ tier: "semantic",
1504
+ type: "length",
1505
+ group: "Space",
1506
+ source: css("calc(var(--k-unit) * 3)"),
1507
+ doc: "Inside a card or popover."
1508
+ },
1509
+ {
1510
+ name: "type-heading",
1511
+ tier: "semantic",
1512
+ type: "length",
1513
+ group: "Type",
1514
+ source: value("20px"),
1515
+ doc: "Section headings (a PR's title, under its kicker), weight 500."
1516
+ },
1517
+ {
1518
+ name: "leading-heading",
1519
+ tier: "semantic",
1520
+ type: "length",
1521
+ group: "Type",
1522
+ source: value("28px"),
1523
+ doc: "Line height for type-heading."
1524
+ },
1525
+ {
1526
+ name: "type-title",
1527
+ tier: "semantic",
1528
+ type: "length",
1529
+ group: "Type",
1530
+ source: value("16px"),
1531
+ doc: "A page's title, weight 500."
1532
+ },
1533
+ {
1534
+ name: "leading-title",
1535
+ tier: "semantic",
1536
+ type: "length",
1537
+ group: "Type",
1538
+ source: value("24px"),
1539
+ doc: "Line height for type-title."
1540
+ },
1541
+ {
1542
+ name: "type-body",
1543
+ tier: "semantic",
1544
+ type: "length",
1545
+ group: "Type",
1546
+ source: value("13px"),
1547
+ doc: "Prose, questions, list rows, step titles; 400, or 500 for emphasis."
1548
+ },
1549
+ {
1550
+ name: "leading-body",
1551
+ tier: "semantic",
1552
+ type: "length",
1553
+ group: "Type",
1554
+ source: value("20px"),
1555
+ doc: "Line height for type-body."
1556
+ },
1557
+ {
1558
+ name: "type-label",
1559
+ tier: "semantic",
1560
+ type: "length",
1561
+ group: "Type",
1562
+ source: value("12px"),
1563
+ doc: "Labels over media, weight 500; mono inside body text."
1564
+ },
1565
+ {
1566
+ name: "leading-label",
1567
+ tier: "semantic",
1568
+ type: "length",
1569
+ group: "Type",
1570
+ source: value("16px"),
1571
+ doc: "Line height for type-label."
1572
+ },
1573
+ {
1574
+ name: "type-small",
1575
+ tier: "semantic",
1576
+ type: "length",
1577
+ group: "Type",
1578
+ source: value("11px"),
1579
+ doc: "Meta, controls, keys, timestamps; mono inside labels."
1580
+ },
1581
+ {
1582
+ name: "leading-small",
1583
+ tier: "semantic",
1584
+ type: "length",
1585
+ group: "Type",
1586
+ source: value("16px"),
1587
+ doc: "Line height for type-small."
1588
+ },
1403
1589
  {
1404
1590
  name: "font-size",
1405
1591
  tier: "semantic",
@@ -2364,16 +2550,18 @@ function injectTokens(doc) {
2364
2550
  style.textContent = rootCss;
2365
2551
  doc.head.append(style);
2366
2552
  }
2367
- var ROOT_CLASS = "knobs-root", extras, spring, rootCss;
2553
+ var ROOT_CLASS = "knobs-root", extras, narrow, spring, rootCss;
2368
2554
  var init_root = __esm(() => {
2369
2555
  init_knobs();
2370
2556
  init_css();
2371
2557
  init_glide();
2372
2558
  init_motion();
2373
2559
  extras = `:where(.${ROOT_CLASS}){--_k-shadow-k:calc(var(--k-shadow-strength,calc(.48 - .32 * var(--_k-light,0))) / .48);--_k-glass-tone:var(--k-glass-tone,calc(.5 + .58 * var(--_k-light,0)));--_k-fill-base:var(--k-fill-base,var(--_k-fill-selected));--_k-fill-hover-mix:var(--k-fill-hover-mix,var(--k-fill-rest-mix,20%));--_k-reveal-ease:var(--k-ease,cubic-bezier(.2,.8,.2,1))}`;
2560
+ narrow = `@media(max-width:639.98px){:where(.${ROOT_CLASS}){--_k-pad-page:var(--k-pad-page,calc(var(--_k-unit) * 4))}}`;
2374
2561
  spring = `:where(:root,:host){--k-fill-ease:${DEFAULT_LINEAR.easing};--k-fill-ms:${DEFAULT_LINEAR.ms}}`;
2375
2562
  rootCss = `${tokensCss(knobs)}
2376
2563
  ${extras}
2564
+ ${narrow}
2377
2565
  ${spring}
2378
2566
  ${DRIVERS}
2379
2567
  ${motionCss}`;
@@ -2584,6 +2772,12 @@ var css2 = `
2584
2772
  --_ku-pane-bg and --_ku-pane-ring so this reaches them. */
2585
2773
  :where(.knobs-surface,.knobs-pane:not([data-nested])>.knobs-pane-clip) .knobs-ui:not(.knobs-surface),.knobs-ui[data-nested]{--_ku-gap:var(--k-ui-gap,0px);--_ku-pane-bg:transparent;--_ku-pane-ring:none;
2586
2774
  --_ku-divider:var(--_k-split-gutter);--_kd-bg:transparent}
2775
+ /* What draws its box without those: a picker's card keeps its fill as the highlight but drops its
2776
+ ring; a timeline's track drops its well for a hairline under the lane; an inline panel sheds its
2777
+ card (--k-chrome 0). A full-window step is the one raised exception (it is the surface). */
2778
+ :is(.knobs-surface,.knobs-pane:not([data-nested])>.knobs-pane-clip) .knobs-picker-card,.knobs-ui[data-nested] .knobs-picker-card{box-shadow:none}
2779
+ :is(.knobs-surface,.knobs-pane:not([data-nested])>.knobs-pane-clip) .knobs-tl-track,.knobs-ui[data-nested] .knobs-tl-track{background:transparent;border-radius:0;box-shadow:inset 0 -1px 0 var(--_k-border-subtle)}
2780
+ :where(.knobs-surface,.knobs-pane:not([data-nested])>.knobs-pane-clip) .knobs-panel.knobs-inline:not(.knobs-page){--k-chrome:0}
2587
2781
  /* Only the pieces' own elements take their type and resets: what you put in a pane is yours. */
2588
2782
  .knobs-ui{box-sizing:border-box}
2589
2783
  :where(.knobs-split>*,.knobs-pane-clip,.knobs-pane-inner,.knobs-pane-body,.knobs-pane-head,.knobs-pane-head *,.knobs-pane-rail,.knobs-pane-rail *,.knobs-tree,.knobs-tree *,.knobs-tree-ghost){box-sizing:border-box}
@@ -2718,11 +2912,9 @@ var css2 = `
2718
2912
  .knobs-tree:focus .knobs-tree-row[data-active][aria-selected="true"]::before{background:var(--_ku-select-focus)}
2719
2913
  .knobs-tree-row[data-drop="into"]::before{opacity:1;background:color-mix(in srgb,var(--_k-accent) 30%,transparent)}
2720
2914
  .knobs-tree-row[data-dragged]{opacity:.38}
2721
- /* glide: one highlight under the selected row that glides between rows (the row's own stays off). */
2722
- .knobs-tree-cursor{position:absolute;left:var(--_ku-row-x);right:var(--_ku-row-x);top:0;height:var(--_k-row);border-radius:var(--_ku-row-radius);background:var(--_ku-select);
2723
- opacity:0;pointer-events:none;will-change:transform;transition:opacity .15s ease-out,background .12s}
2724
- .knobs-tree-cursor[data-on]{opacity:1}
2725
- .knobs-tree:focus .knobs-tree-cursor{background:var(--_ku-select-focus)}
2915
+ /* glide: the selection pill on the selected row, in the selection's colour (the row's own stays off). */
2916
+ .knobs-tree>.knobs-pick-pill{background:var(--_ku-select)}
2917
+ .knobs-tree:focus>.knobs-pick-pill{background:var(--_ku-select-focus)}
2726
2918
  .knobs-tree[data-glide] .knobs-tree-row[aria-selected="true"]::before{opacity:0!important}
2727
2919
  .knobs-tree-row>*{position:relative}
2728
2920
  .knobs-tree-caret{flex:none;display:flex;align-items:center;justify-content:center;width:16px;height:100%;color:var(--_k-fg-3);transition:transform var(--_ku-ease-ms) var(--_ku-ease)}
@@ -2754,22 +2946,20 @@ var css2 = `
2754
2946
  box-shadow:0 0 0 1px var(--_k-border-strong),0 6px 18px rgba(0,0,0,calc(.35 * var(--_k-shadow-k,1)));pointer-events:none;white-space:nowrap;font:var(--_k-weight) var(--_k-font-size)/1.6 var(--_k-font)}
2755
2947
  .knobs-tree-empty{padding:10px var(--_k-inset);color:var(--_k-fg-3)}
2756
2948
 
2757
- /* Timeline: a transport (play, the time) beside a track: the ruler's canvas over a lane of
2758
- markers, ranges as soft bands, and the playhead, a hairline in the accent with a cap. */
2949
+ /* Timeline: play beside a track: the ruler's canvas over a lane of markers, ranges as soft bands,
2950
+ and the playhead, a hairline in the accent with a cap. No clock: the time shows under the
2951
+ pointer on the track (the tag), in seconds, while hovering or scrubbing. */
2759
2952
  :where(.knobs-timeline,.knobs-timeline *){box-sizing:border-box}
2760
2953
  .knobs-ui.knobs-timeline{display:flex;align-items:stretch;gap:10px;width:100%;height:100%;min-height:52px;padding:6px var(--_k-inset) 8px 8px;outline:none;user-select:none;-webkit-user-select:none;
2761
2954
  font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg);letter-spacing:normal;text-align:left}
2762
2955
  :where(.knobs-ui) :is(.knobs-tl-play,.knobs-tl-edit){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;background:transparent;border:0;margin:0;padding:0;outline:none;box-shadow:none}
2763
- .knobs-tl-transport{flex:none;display:flex;align-items:center;gap:8px;min-width:96px}
2956
+ .knobs-tl-transport{flex:none;display:flex;align-items:center}
2764
2957
  .knobs-ui .knobs-tl-play{flex:none;display:inline-flex;align-items:center;justify-content:center;width:var(--_k-control-h);height:var(--_k-control-h);min-width:22px;min-height:22px;border-radius:calc(var(--_k-radius) * 999);
2765
2958
  background:var(--_k-fill-selected);color:var(--_k-fg);cursor:pointer;transition:background .15s,transform .15s var(--_ku-ease)}
2766
2959
  .knobs-ui .knobs-tl-play:hover{background:color-mix(in srgb,var(--_k-fg) 16%,transparent)}
2767
2960
  .knobs-ui .knobs-tl-play:active{transform:scale(.94)}
2768
2961
  .knobs-tl-play svg{width:12px;height:12px}
2769
2962
  .knobs-timeline[data-playing] .knobs-tl-play{background:var(--_k-accent);color:var(--_k-bg)}
2770
- .knobs-tl-time{display:flex;flex-direction:column;line-height:1.2;font-variant-numeric:tabular-nums;font-feature-settings:"tnum"}
2771
- .knobs-tl-now{font:var(--_k-weight) var(--_k-value-size)/1.2 var(--_k-value-font);font-variant-numeric:tabular-nums;font-feature-settings:"tnum";color:var(--_k-fg);white-space:nowrap}
2772
- .knobs-tl-sub{font-size:var(--_k-text-meta);color:var(--_k-fg-3);white-space:nowrap}
2773
2963
  .knobs-tl-track{position:relative;flex:1;min-width:0;overflow:hidden;border-radius:var(--_k-radius);background:var(--_k-fill);cursor:col-resize;touch-action:none;
2774
2964
  transition:background calc(var(--_k-hover-out) * 1ms)}
2775
2965
  .knobs-timeline:focus-visible .knobs-tl-track,.knobs-timeline[data-keyboard]:focus .knobs-tl-track{background:color-mix(in srgb,var(--_k-fill-selected) 80%,transparent)}
@@ -2793,7 +2983,8 @@ var css2 = `
2793
2983
  .knobs-tl-pin{position:absolute;top:13px;left:-5px;width:10px;height:10px;pointer-events:auto;cursor:grab}
2794
2984
  .knobs-tl-pin::after{content:"";position:absolute;inset:1.5px;border-radius:min(2px,var(--_k-radius));background:var(--knobs-tl-c);rotate:45deg;transition:background .15s,scale .2s var(--_ku-ease)}
2795
2985
  .knobs-tl-mlabel{position:absolute;top:26px;left:3px;max-width:140px;padding:0 6px;border-radius:max(0px,calc(var(--_k-radius) - 3px));background:var(--_k-fill-selected);color:var(--_k-fg);
2796
- font-size:var(--_k-text-meta);line-height:17px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;pointer-events:auto;cursor:grab;transition:background .15s,color .15s}
2986
+ font-size:var(--_k-text-meta);line-height:17px;white-space:nowrap;pointer-events:auto;cursor:grab;transition:background .15s,color .15s}
2987
+ .knobs-tl-mtext{display:block;overflow:hidden;white-space:nowrap}
2797
2988
  .knobs-tl-marker[data-selected]{--knobs-tl-c:var(--_k-accent)}
2798
2989
  .knobs-tl-marker[data-selected] .knobs-tl-mlabel{background:color-mix(in srgb,var(--_k-accent) 24%,var(--_k-fill-selected))}
2799
2990
  .knobs-tl-marker[data-selected] .knobs-tl-pin::after{scale:1.2}
@@ -2814,7 +3005,22 @@ var css2 = `
2814
3005
  .knobs-timeline[data-scrubbing] .knobs-tl-hover{opacity:0}
2815
3006
  @media(prefers-reduced-motion:reduce){.knobs-timeline :where(*,::before,::after){transition:none!important}}
2816
3007
  @media(prefers-reduced-motion:reduce){:is(.knobs-tree,.knobs-split>.knobs-divider,.knobs-pane-head,.knobs-pane-rail) :where(*,::before,::after){transition:none!important}}
2817
- `, uiCss = () => aliased(css2 + readoutCss + overlayCss + bentoCss + activityCss + pickerCss + compareCss) + rolling_css_default, lookDefaults, tune = (s, m) => {
3008
+ .knobs-tl-tag{position:absolute;left:0;bottom:2px;font-size:var(--_k-text-meta);line-height:16px;color:var(--_k-fg);font-variant-numeric:tabular-nums;font-feature-settings:"tnum";white-space:nowrap;pointer-events:none;opacity:0}
3009
+ .knobs-tl-tag[data-on]{opacity:1}
3010
+ /* The lane (variant: "lane"): one slim track of ticks under a compare or a video; markers are
3011
+ faint chapter ticks, the current one in text colour; nothing to edit. */
3012
+ .knobs-ui.knobs-timeline[data-variant="lane"]{height:auto;min-height:0;padding:0;gap:var(--_k-gap)}
3013
+ .knobs-timeline[data-variant="lane"] .knobs-tl-track{height:var(--_k-control-h)}
3014
+ .knobs-timeline[data-variant="lane"] .knobs-tl-ruler{height:100%}
3015
+ .knobs-timeline[data-variant="lane"] .knobs-tl-lane{top:0}
3016
+ .knobs-timeline[data-variant="lane"] .knobs-tl-marker{top:0}
3017
+ .knobs-timeline[data-variant="lane"] .knobs-tl-marker::before{top:0;bottom:50%;left:-.75px;width:1.5px;background:var(--_k-fg-3)}
3018
+ .knobs-timeline[data-variant="lane"] .knobs-tl-marker[data-current]::before{background:var(--_k-fg)}
3019
+ .knobs-timeline[data-variant="lane"] :is(.knobs-tl-pin,.knobs-tl-mlabel,.knobs-tl-rlabel){display:none}
3020
+ .knobs-timeline[data-variant="lane"] .knobs-tl-head::before{top:0}
3021
+ .knobs-timeline[data-variant="lane"] .knobs-tl-head::after{content:none}
3022
+ .knobs-timeline[data-variant="lane"] .knobs-tl-tag{top:2px;bottom:auto}
3023
+ `, uiCss = () => aliased(css2 + readoutCss + overlayCss + bentoCss + activityCss + pickerCss + compareCss + selectionPillCss) + rolling_css_default, lookDefaults, tune = (s, m) => {
2818
3024
  s.omega = m.omega;
2819
3025
  s.zeta = m.zeta;
2820
3026
  return s;
@@ -2972,33 +3178,29 @@ var init_diff_css = __esm(() => {
2972
3178
  .knobs-diff-fill{flex:1}
2973
3179
  .knobs-diff-nav{flex:none;display:inline-flex;align-items:center;gap:1px;color:var(--_k-fg-3);font-size:var(--_k-text-meta);font-variant-numeric:tabular-nums}
2974
3180
  .knobs-diff-nav>span{min-width:3.6em;text-align:center}
2975
- :where(.knobs-ui) :is(.knobs-diff-btn,.knobs-diff-seg>button,.knobs-diff-viewed,.knobs-diff-filter>input,.knobs-diff-filter>button){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;background:transparent;border:0;border-radius:0;
3181
+ :where(.knobs-ui) :is(.knobs-diff-btn,.knobs-diff-choice,.knobs-diff-viewed,.knobs-diff-filter>input,.knobs-diff-filter>button){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;background:transparent;border:0;border-radius:0;
2976
3182
  margin:0;padding:0;min-width:0;box-shadow:none;outline:none}
2977
- .knobs-ui .knobs-diff-btn{flex:none;display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:calc(var(--_k-radius) * 999);color:var(--_k-fg-3);cursor:pointer;
3183
+ .knobs-ui .knobs-diff-btn{flex:none;display:inline-flex;align-items:center;justify-content:center;width:var(--_k-control-h);height:var(--_k-control-h);border-radius:calc(var(--_k-radius) * 999);color:var(--_k-fg-3);cursor:pointer;
2978
3184
  transition:background .15s,color .15s,opacity .15s}
2979
3185
  .knobs-ui .knobs-diff-btn:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
2980
3186
  .knobs-ui .knobs-diff-btn[aria-pressed="true"]{color:var(--_k-fg);background:var(--_ku-wash)}
2981
3187
  .knobs-ui .knobs-diff-btn:disabled{opacity:.35;cursor:default;background:transparent}
2982
- .knobs-diff-btn svg{width:13px;height:13px}
3188
+ .knobs-diff-btn svg{width:16px;height:16px}
2983
3189
  /* Viewed: a check with its word, as on GitHub; the path dims once it's ticked. */
2984
3190
  .knobs-ui .knobs-diff-viewed{flex:none;display:inline-flex;align-items:center;gap:5px;height:var(--_k-control-h);padding:0 8px 0 6px;border-radius:var(--kd-ctl);color:var(--_k-fg-2);
2985
3191
  font-size:var(--_k-text-meta);font-weight:var(--_k-weight-label);white-space:nowrap;cursor:pointer;transition:background .15s,color .15s}
2986
3192
  .knobs-ui .knobs-diff-viewed:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
2987
3193
  .knobs-ui .knobs-diff-viewed[aria-pressed="true"]{color:var(--_k-fg);background:var(--_ku-wash)}
2988
- .knobs-diff-viewed svg{width:13px;height:13px}
3194
+ .knobs-diff-viewed svg{width:16px;height:16px}
2989
3195
  .knobs-diff-viewed[aria-pressed="true"] svg{color:var(--kd-add)}
2990
3196
  .knobs-diff-path{transition:opacity .2s}
2991
3197
  .knobs-diff[data-viewed]>.knobs-diff-head .knobs-diff-path,.knobs-diff-part[data-viewed] .knobs-diff-path{opacity:.6}
2992
- /* Split | Unified: one pill that glides between the two, raised as a panel's segments are (a
2993
- lighter bg in dark, near white in light). */
2994
- .knobs-diff-seg{position:relative;flex:none;display:grid;grid-template-columns:1fr 1fr;height:var(--_k-control-h);padding:2px;border-radius:var(--kd-ctl);background:var(--_k-segmented-track)}
2995
- .knobs-ui .knobs-diff-seg>button{position:relative;z-index:1;padding:0 9px;border-radius:max(0px,calc(var(--kd-ctl) - 2px));color:var(--_k-fg-2);font-size:var(--_k-text-meta);
2996
- font-weight:var(--_k-weight-label);white-space:nowrap;cursor:pointer;transition:color .15s}
2997
- .knobs-ui .knobs-diff-seg>button:is(:hover,:focus-visible),.knobs-ui .knobs-diff-seg>button[aria-checked="true"]{color:var(--_k-fg)}
2998
- .knobs-diff-pill{position:absolute;top:2px;bottom:2px;left:2px;width:calc(50% - 2px);border-radius:max(0px,calc(var(--kd-ctl) - 2px));background:color-mix(in srgb,var(--_k-accent) 18%,var(--_k-segmented-active));
2999
- box-shadow:0 1px 2px rgba(0,0,0,calc(.16 * var(--k-lift,1)));transition:transform var(--_ku-ease-ms) var(--_ku-ease)}
3000
- .knobs-diff-seg[data-value="unified"]>.knobs-diff-pill{transform:translateX(100%)}
3001
- .knobs-diff[data-frame="flat"] .knobs-diff-pill{box-shadow:none}
3198
+ /* Split | Unified: the selection pill on the one picked, its bright copy the picked one's text; a
3199
+ choice isn't brighter for being picked. Choices are styled by class, as the copy holds them too. */
3200
+ .knobs-diff-seg{position:relative;flex:none;display:grid;grid-template-columns:1fr 1fr;height:var(--_k-control-h);padding:2px;border-radius:var(--kd-ctl);background:var(--_k-fill)}
3201
+ .knobs-ui .knobs-diff-choice{padding:0 9px;border-radius:max(0px,calc(var(--kd-ctl) - 2px));color:var(--_k-fg-2);font-size:var(--_k-text-meta);
3202
+ font-weight:var(--_k-weight-label);white-space:nowrap;cursor:pointer;transition:color calc(var(--_k-hover-out) * 1ms)}
3203
+ .knobs-ui .knobs-diff-choice:is(:hover,:focus-visible){color:var(--_k-fg)}
3002
3204
  /* The diff: one layer per file or layout on show, the old fading under the new. */
3003
3205
  .knobs-diff-body{position:relative;flex:1;min-height:0}
3004
3206
  .knobs-diff-probe{position:absolute;left:0;top:0;width:0;height:var(--kd-line);visibility:hidden;pointer-events:none}
@@ -3093,7 +3295,7 @@ var init_diff_css = __esm(() => {
3093
3295
  .knobs-ui .knobs-diff-filter>input::placeholder{color:var(--_k-fg-3)}
3094
3296
  .knobs-ui .knobs-diff-filter>button{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:999px;color:var(--_k-fg-3);cursor:pointer;transition:background .12s,color .12s,opacity .12s}
3095
3297
  .knobs-ui .knobs-diff-filter>button:hover{background:var(--_ku-wash-hi);color:var(--_k-fg)}
3096
- .knobs-diff-filter>button svg{width:10px;height:10px}
3298
+ .knobs-diff-filter>button svg{width:12px;height:12px}
3097
3299
  .knobs-diff-list{position:relative;flex:1;min-height:0}
3098
3300
  .knobs-diff-files .knobs-diff-counts{font-size:var(--_k-text-meta);align-items:center}
3099
3301
  .knobs-diff-files .knobs-diff-counts{gap:5px}
@@ -3860,6 +4062,283 @@ var init_rgba = __esm(() => {
3860
4062
  cache = new Map;
3861
4063
  });
3862
4064
 
4065
+ // src/ui/icons.ts
4066
+ function icon(name, options = {}) {
4067
+ const size = options.small ? 12 : 16;
4068
+ return `<svg class="knobs-icon" viewBox="0 0 256 256" width="${size}" height="${size}" fill="currentColor" aria-hidden="true" focusable="false"><path d="${PATHS[name]}"/></svg>`;
4069
+ }
4070
+ var PATHS, ICON_NAMES;
4071
+ var init_icons = __esm(() => {
4072
+ PATHS = {
4073
+ arrowLeft: "M224,128a8,8,0,0,1-8,8H59.31l58.35,58.34a8,8,0,0,1-11.32,11.32l-72-72a8,8,0,0,1,0-11.32l72-72a8,8,0,0,1,11.32,11.32L59.31,120H216A8,8,0,0,1,224,128Z",
4074
+ arrowUpRight: "M200,64V168a8,8,0,0,1-16,0V83.31L69.66,197.66a8,8,0,0,1-11.32-11.32L172.69,72H88a8,8,0,0,1,0-16H192A8,8,0,0,1,200,64Z",
4075
+ arrowSquareOut: "M224,104a8,8,0,0,1-16,0V59.32l-66.33,66.34a8,8,0,0,1-11.32-11.32L196.68,48H152a8,8,0,0,1,0-16h64a8,8,0,0,1,8,8Zm-40,24a8,8,0,0,0-8,8v72H48V80h72a8,8,0,0,0,0-16H48A16,16,0,0,0,32,80V208a16,16,0,0,0,16,16H176a16,16,0,0,0,16-16V136A8,8,0,0,0,184,128Z",
4076
+ arrowCounterClockwise: "M224,128a96,96,0,0,1-94.71,96H128A95.38,95.38,0,0,1,62.1,197.8a8,8,0,0,1,11-11.63A80,80,0,1,0,71.43,71.39a3.07,3.07,0,0,1-.26.25L44.59,96H72a8,8,0,0,1,0,16H24a8,8,0,0,1-8-8V56a8,8,0,0,1,16,0V85.8L60.25,60A96,96,0,0,1,224,128Z",
4077
+ arrowUDownLeft: "M232,112a64.07,64.07,0,0,1-64,64H51.31l34.35,34.34a8,8,0,0,1-11.32,11.32l-48-48a8,8,0,0,1,0-11.32l48-48a8,8,0,0,1,11.32,11.32L51.31,160H168a48,48,0,0,0,0-96H80a8,8,0,0,1,0-16h88A64.07,64.07,0,0,1,232,112Z",
4078
+ arrowsHorizontal: "M136,40V216a8,8,0,0,1-16,0V40a8,8,0,0,1,16,0ZM96,120H35.31l18.35-18.34A8,8,0,0,0,42.34,90.34l-32,32a8,8,0,0,0,0,11.32l32,32a8,8,0,0,0,11.32-11.32L35.31,136H96a8,8,0,0,0,0-16Zm149.66,2.34-32-32a8,8,0,0,0-11.32,11.32L220.69,120H160a8,8,0,0,0,0,16h60.69l-18.35,18.34a8,8,0,0,0,11.32,11.32l32-32A8,8,0,0,0,245.66,122.34Z",
4079
+ caretDown: "M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,53.66,90.34L128,164.69l74.34-74.35a8,8,0,0,1,11.32,11.32Z",
4080
+ caretLeft: "M165.66,202.34a8,8,0,0,1-11.32,11.32l-80-80a8,8,0,0,1,0-11.32l80-80a8,8,0,0,1,11.32,11.32L91.31,128Z",
4081
+ caretRight: "M181.66,133.66l-80,80a8,8,0,0,1-11.32-11.32L164.69,128,90.34,53.66a8,8,0,0,1,11.32-11.32l80,80A8,8,0,0,1,181.66,133.66Z",
4082
+ caretUp: "M213.66,165.66a8,8,0,0,1-11.32,0L128,91.31,53.66,165.66a8,8,0,0,1-11.32-11.32l80-80a8,8,0,0,1,11.32,0l80,80A8,8,0,0,1,213.66,165.66Z",
4083
+ check: "M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z",
4084
+ checkCircle: "M173.66,98.34a8,8,0,0,1,0,11.32l-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35A8,8,0,0,1,173.66,98.34ZM232,128A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z",
4085
+ circle: "M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Z",
4086
+ copy: "M216,32H88a8,8,0,0,0-8,8V80H40a8,8,0,0,0-8,8V216a8,8,0,0,0,8,8H168a8,8,0,0,0,8-8V176h40a8,8,0,0,0,8-8V40A8,8,0,0,0,216,32ZM160,208H48V96H160Zm48-48H176V88a8,8,0,0,0-8-8H96V48H208Z",
4087
+ cornersIn: "M152,96V48a8,8,0,0,1,16,0V88h40a8,8,0,0,1,0,16H160A8,8,0,0,1,152,96ZM96,152H48a8,8,0,0,0,0,16H88v40a8,8,0,0,0,16,0V160A8,8,0,0,0,96,152Zm112,0H160a8,8,0,0,0-8,8v48a8,8,0,0,0,16,0V168h40a8,8,0,0,0,0-16ZM96,40a8,8,0,0,0-8,8V88H48a8,8,0,0,0,0,16H96a8,8,0,0,0,8-8V48A8,8,0,0,0,96,40Z",
4088
+ cornersOut: "M216,48V88a8,8,0,0,1-16,0V56H168a8,8,0,0,1,0-16h40A8,8,0,0,1,216,48ZM88,200H56V168a8,8,0,0,0-16,0v40a8,8,0,0,0,8,8H88a8,8,0,0,0,0-16Zm120-40a8,8,0,0,0-8,8v32H168a8,8,0,0,0,0,16h40a8,8,0,0,0,8-8V168A8,8,0,0,0,208,160ZM88,40H48a8,8,0,0,0-8,8V88a8,8,0,0,0,16,0V56H88a8,8,0,0,0,0-16Z",
4089
+ folder: "M216,72H130.67L102.93,51.2a16.12,16.12,0,0,0-9.6-3.2H40A16,16,0,0,0,24,64V200a16,16,0,0,0,16,16H216.89A15.13,15.13,0,0,0,232,200.89V88A16,16,0,0,0,216,72Zm0,128H40V64H93.33L123.2,86.4A8,8,0,0,0,128,88h88Z",
4090
+ keyboard: "M224,48H32A16,16,0,0,0,16,64V192a16,16,0,0,0,16,16H224a16,16,0,0,0,16-16V64A16,16,0,0,0,224,48Zm0,144H32V64H224V192Zm-16-64a8,8,0,0,1-8,8H56a8,8,0,0,1,0-16H200A8,8,0,0,1,208,128Zm0-32a8,8,0,0,1-8,8H56a8,8,0,0,1,0-16H200A8,8,0,0,1,208,96ZM72,160a8,8,0,0,1-8,8H56a8,8,0,0,1,0-16h8A8,8,0,0,1,72,160Zm96,0a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h64A8,8,0,0,1,168,160Zm40,0a8,8,0,0,1-8,8h-8a8,8,0,0,1,0-16h8A8,8,0,0,1,208,160Z",
4091
+ magnifyingGlass: "M229.66,218.34l-50.07-50.06a88.11,88.11,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.32ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z",
4092
+ magnifyingGlassMinus: "M152,112a8,8,0,0,1-8,8H80a8,8,0,0,1,0-16h64A8,8,0,0,1,152,112Zm77.66,117.66a8,8,0,0,1-11.32,0l-50.06-50.07a88.11,88.11,0,1,1,11.31-11.31l50.07,50.06A8,8,0,0,1,229.66,229.66ZM112,184a72,72,0,1,0-72-72A72.08,72.08,0,0,0,112,184Z",
4093
+ magnifyingGlassPlus: "M152,112a8,8,0,0,1-8,8H120v24a8,8,0,0,1-16,0V120H80a8,8,0,0,1,0-16h24V80a8,8,0,0,1,16,0v24h24A8,8,0,0,1,152,112Zm77.66,117.66a8,8,0,0,1-11.32,0l-50.06-50.07a88.11,88.11,0,1,1,11.31-11.31l50.07,50.06A8,8,0,0,1,229.66,229.66ZM112,184a72,72,0,1,0-72-72A72.08,72.08,0,0,0,112,184Z",
4094
+ microphone: "M128,176a48.05,48.05,0,0,0,48-48V64a48,48,0,0,0-96,0v64A48.05,48.05,0,0,0,128,176ZM96,64a32,32,0,0,1,64,0v64a32,32,0,0,1-64,0Zm40,143.6V240a8,8,0,0,1-16,0V207.6A80.11,80.11,0,0,1,48,128a8,8,0,0,1,16,0,64,64,0,0,0,128,0,8,8,0,0,1,16,0A80.11,80.11,0,0,1,136,207.6Z",
4095
+ paperPlaneRight: "M231.87,114l-168-95.89A16,16,0,0,0,40.92,37.34L71.55,128,40.92,218.67A16,16,0,0,0,56,240a16.15,16.15,0,0,0,7.93-2.1l167.92-96.05a16,16,0,0,0,.05-27.89ZM56,224a.56.56,0,0,0,0-.12L85.74,136H144a8,8,0,0,0,0-16H85.74L56.06,32.16A.46.46,0,0,0,56,32l168,95.83Z",
4096
+ pencilSimple: "M227.31,73.37,182.63,28.68a16,16,0,0,0-22.63,0L36.69,152A15.86,15.86,0,0,0,32,163.31V208a16,16,0,0,0,16,16H92.69A15.86,15.86,0,0,0,104,219.31L227.31,96a16,16,0,0,0,0-22.63ZM92.69,208H48V163.31l88-88L180.69,120ZM192,108.68,147.31,64l24-24L216,84.68Z",
4097
+ repeat: "M24,128A72.08,72.08,0,0,1,96,56H204.69L194.34,45.66a8,8,0,0,1,11.32-11.32l24,24a8,8,0,0,1,0,11.32l-24,24a8,8,0,0,1-11.32-11.32L204.69,72H96a56.06,56.06,0,0,0-56,56,8,8,0,0,1-16,0Zm200-8a8,8,0,0,0-8,8,56.06,56.06,0,0,1-56,56H51.31l10.35-10.34a8,8,0,0,0-11.32-11.32l-24,24a8,8,0,0,0,0,11.32l24,24a8,8,0,0,0,11.32-11.32L51.31,200H160a72.08,72.08,0,0,0,72-72A8,8,0,0,0,224,120Z",
4098
+ sidebar: "M216,40H40A16,16,0,0,0,24,56V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A16,16,0,0,0,216,40ZM40,56H80V200H40ZM216,200H96V56H216V200Z",
4099
+ x: "M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"
4100
+ };
4101
+ ICON_NAMES = Object.keys(PATHS);
4102
+ });
4103
+
4104
+ // src/ui/selection-pill.ts
4105
+ function pillSpring(distance, base = SPRINGS.default) {
4106
+ return distance > 400 ? { duration: base.duration * 1.4, bounce: base.bounce } : base;
4107
+ }
4108
+ function pillBoxOf(row, container) {
4109
+ return {
4110
+ x: row.left - container.left - container.clientLeft + container.scrollLeft,
4111
+ y: row.top - container.top - container.clientTop + container.scrollTop,
4112
+ w: row.width,
4113
+ h: row.height
4114
+ };
4115
+ }
4116
+ function pillRadius(from, to, h, k) {
4117
+ const t = Math.abs(to.h - from.h) > 0.5 ? (h - from.h) / (to.h - from.h) : Number.isFinite(k) ? k : 1;
4118
+ return from.r + (to.r - from.r) * Math.min(1, Math.max(0, t));
4119
+ }
4120
+ function pillProgress(from, to, at) {
4121
+ const d = Math.hypot(to.x - from.x, to.y - from.y);
4122
+ if (d < 0.5)
4123
+ return 1;
4124
+ const p = ((at.x - from.x) * (to.x - from.x) + (at.y - from.y) * (to.y - from.y)) / (d * d);
4125
+ return Math.min(1, Math.max(0, p));
4126
+ }
4127
+ function planPill(state, next, flags = {}) {
4128
+ if (!next)
4129
+ return { kind: "hide" };
4130
+ if (!state.shown || !state.at || flags.first || flags.reduced || flags.snap)
4131
+ return { kind: "land", box: next };
4132
+ const t = state.target;
4133
+ if (t && t.x === next.x && t.y === next.y && t.w === next.w && t.h === next.h)
4134
+ return { kind: "stay" };
4135
+ const d = Math.hypot(next.x - state.at.x, next.y - state.at.y);
4136
+ return { kind: "travel", box: next, spring: pillSpring(d) };
4137
+ }
4138
+ function selectionPill(container, options) {
4139
+ const doc = container.ownerDocument;
4140
+ const win = doc.defaultView;
4141
+ injectUiStyles(doc);
4142
+ if (win.getComputedStyle(container).position === "static")
4143
+ container.style.position = "relative";
4144
+ const pill = doc.createElement("i");
4145
+ pill.className = "knobs-pick-pill";
4146
+ pill.setAttribute("aria-hidden", "true");
4147
+ pill.dataset.off = "";
4148
+ const copyOn = options.copy !== false;
4149
+ const copy = doc.createElement("div");
4150
+ copy.className = "knobs-pick-copy";
4151
+ copy.setAttribute("aria-hidden", "true");
4152
+ copy.inert = true;
4153
+ copy.dataset.off = "";
4154
+ const copyRows = doc.createElement("div");
4155
+ copyRows.className = "knobs-pick-copy-rows";
4156
+ copy.append(copyRows);
4157
+ container.prepend(pill);
4158
+ if (copyOn)
4159
+ container.append(copy);
4160
+ const { omega, zeta } = springMotion(SPRINGS.default);
4161
+ const springs = [0, 0, 0, 0].map(() => new Spring(0, omega, zeta, 0.01));
4162
+ const [sx, sy, sw, sh] = springs;
4163
+ let shown = false;
4164
+ let first = true;
4165
+ let target = null;
4166
+ let from = { x: 0, y: 0, w: 0, h: 0 };
4167
+ let fromR = 0;
4168
+ let toR = 0;
4169
+ let rowBoxes = [];
4170
+ const clones = new Map;
4171
+ let disposed = false;
4172
+ const radiusOf = (row) => options.radius?.(row) ?? (Number.parseFloat(win.getComputedStyle(row).borderTopLeftRadius) || 0);
4173
+ const rowsNow = () => options.rows ? [...options.rows()] : [...container.children].filter((n) => n !== pill && n !== copy);
4174
+ const measure = (row) => {
4175
+ const c = container.getBoundingClientRect();
4176
+ const r = row.getBoundingClientRect();
4177
+ return pillBoxOf(r, {
4178
+ left: c.left,
4179
+ top: c.top,
4180
+ scrollLeft: container.scrollLeft,
4181
+ scrollTop: container.scrollTop,
4182
+ clientLeft: container.clientLeft,
4183
+ clientTop: container.clientTop
4184
+ });
4185
+ };
4186
+ const at = () => ({ x: sx.x, y: sy.x, w: sw.x, h: sh.x });
4187
+ const syncCopy = (span) => {
4188
+ if (!copyOn)
4189
+ return;
4190
+ const keep = new Set;
4191
+ for (const { row, box } of rowBoxes) {
4192
+ if (!boxesMeet(box, span))
4193
+ continue;
4194
+ keep.add(row);
4195
+ let n = clones.get(row);
4196
+ if (!n) {
4197
+ n = row.cloneNode(true);
4198
+ n.removeAttribute("id");
4199
+ for (const d of n.querySelectorAll("[id]"))
4200
+ d.removeAttribute("id");
4201
+ clones.set(row, n);
4202
+ copyRows.append(n);
4203
+ }
4204
+ const s = n.style;
4205
+ s.position = "absolute";
4206
+ s.inset = "auto";
4207
+ s.left = `${box.x}px`;
4208
+ s.top = `${box.y}px`;
4209
+ s.width = `${box.w}px`;
4210
+ s.height = `${box.h}px`;
4211
+ s.margin = "0";
4212
+ s.transform = "none";
4213
+ s.clipPath = "";
4214
+ s.opacity = "";
4215
+ s.boxSizing = "border-box";
4216
+ s.color = options.color ?? "var(--_k-fg)";
4217
+ }
4218
+ for (const [row, n] of clones)
4219
+ if (!keep.has(row)) {
4220
+ n.remove();
4221
+ clones.delete(row);
4222
+ }
4223
+ };
4224
+ const paint = () => {
4225
+ const b = at();
4226
+ const x0 = grid(b.x);
4227
+ const y0 = grid(b.y);
4228
+ const w = grid(b.x + b.w) - x0;
4229
+ const h = grid(b.y + b.h) - y0;
4230
+ const r = target ? pillRadius({ h: from.h, r: fromR }, { h: target.h, r: toR }, b.h, pillProgress(from, target, b)) : toR;
4231
+ const radius = `${Math.max(0, Math.min(r, h / 2, w / 2)).toFixed(2)}px`;
4232
+ for (const n of copyOn ? [pill, copy] : [pill]) {
4233
+ n.style.transform = `translate3d(${x0}px,${y0}px,0)`;
4234
+ n.style.width = `${w}px`;
4235
+ n.style.height = `${h}px`;
4236
+ n.style.borderRadius = radius;
4237
+ }
4238
+ if (copyOn)
4239
+ copyRows.style.transform = `translate3d(${-x0}px,${-y0}px,0)`;
4240
+ };
4241
+ const loop = frameLoop(win, (dt) => {
4242
+ for (const s of springs)
4243
+ s.step(dt);
4244
+ paint();
4245
+ return springs.some((s) => !s.resting);
4246
+ });
4247
+ const show = (on) => {
4248
+ shown = on;
4249
+ pill.toggleAttribute("data-off", !on);
4250
+ copy.toggleAttribute("data-off", !on);
4251
+ container.toggleAttribute("data-pick-empty", !on);
4252
+ };
4253
+ const update = (o) => {
4254
+ if (disposed)
4255
+ return;
4256
+ const row = options.selected();
4257
+ const next = row?.isConnected ? measure(row) : null;
4258
+ const plan = planPill({ shown, at: shown ? at() : null, target }, next, {
4259
+ first,
4260
+ reduced: reducedMotion(win),
4261
+ ...o?.snap ? { snap: true } : {}
4262
+ });
4263
+ first = false;
4264
+ for (const n of clones.values())
4265
+ n.remove();
4266
+ clones.clear();
4267
+ rowBoxes = copyOn ? rowsNow().filter((r) => r.isConnected).map((r) => ({ row: r, box: measure(r) })) : [];
4268
+ if (plan.kind === "hide") {
4269
+ if (shown)
4270
+ show(false);
4271
+ return;
4272
+ }
4273
+ if (plan.kind === "stay") {
4274
+ syncCopy(boxUnion(at(), target));
4275
+ return;
4276
+ }
4277
+ const r = radiusOf(row);
4278
+ if (plan.kind === "land") {
4279
+ sx.snap(plan.box.x);
4280
+ sy.snap(plan.box.y);
4281
+ sw.snap(plan.box.w);
4282
+ sh.snap(plan.box.h);
4283
+ from = plan.box;
4284
+ fromR = r;
4285
+ toR = r;
4286
+ target = plan.box;
4287
+ syncCopy(plan.box);
4288
+ paint();
4289
+ loop.stop();
4290
+ if (!shown)
4291
+ show(true);
4292
+ return;
4293
+ }
4294
+ const m = springMotion(plan.spring);
4295
+ for (const s of springs) {
4296
+ s.omega = m.omega;
4297
+ s.zeta = m.zeta;
4298
+ }
4299
+ const now = at();
4300
+ fromR = target ? pillRadius({ h: from.h, r: fromR }, { h: target.h, r: toR }, now.h, pillProgress(from, target, now)) : r;
4301
+ from = now;
4302
+ toR = r;
4303
+ target = plan.box;
4304
+ sx.set(plan.box.x);
4305
+ sy.set(plan.box.y);
4306
+ sw.set(plan.box.w);
4307
+ sh.set(plan.box.h);
4308
+ syncCopy(boxUnion(now, plan.box));
4309
+ loop.wake();
4310
+ };
4311
+ const ro = "ResizeObserver" in win ? new win.ResizeObserver(() => update({ snap: true })) : null;
4312
+ ro?.observe(container);
4313
+ update();
4314
+ return {
4315
+ element: pill,
4316
+ update,
4317
+ dispose() {
4318
+ disposed = true;
4319
+ loop.stop();
4320
+ ro?.disconnect();
4321
+ pill.remove();
4322
+ copy.remove();
4323
+ container.removeAttribute("data-pick-empty");
4324
+ }
4325
+ };
4326
+ }
4327
+ var boxesMeet = (a, b) => a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h, boxUnion = (a, b) => {
4328
+ const x = Math.min(a.x, b.x);
4329
+ const y = Math.min(a.y, b.y);
4330
+ return {
4331
+ x,
4332
+ y,
4333
+ w: Math.max(a.x + a.w, b.x + b.w) - x,
4334
+ h: Math.max(a.y + a.h, b.y + b.h) - y
4335
+ };
4336
+ }, grid = (v) => Math.round(v * 64) / 64;
4337
+ var init_selection_pill = __esm(() => {
4338
+ init_motion();
4339
+ init_look();
4340
+ });
4341
+
3863
4342
  // src/panel/hints.ts
3864
4343
  function extractWords(text) {
3865
4344
  return text.toLowerCase().match(/[a-z]+/g) ?? [];
@@ -4688,34 +5167,27 @@ function tree(container, options) {
4688
5167
  const empty = doc.createElement("div");
4689
5168
  empty.className = "knobs-tree-empty";
4690
5169
  empty.hidden = true;
4691
- const cursor = doc.createElement("div");
4692
- cursor.className = "knobs-tree-cursor";
4693
- root.append(space, cursor, line, empty);
4694
- const cursorY = new Spring(0, MOVE.omega, MOVE.zeta, 0.1);
4695
- let cursorOn = false;
4696
- const drawCursor = () => {
4697
- cursor.style.transform = `translate3d(0,${Math.round(cursorY.x * 2) / 2}px,0)`;
5170
+ root.append(space, line, empty);
5171
+ let pill = null;
5172
+ let picked = null;
5173
+ let travelUntil = 0;
5174
+ const pickedRow = () => {
5175
+ const id = sel.ids.length === 1 ? sel.ids[0] : null;
5176
+ const v = id === null ? undefined : views.get(id);
5177
+ return v && !v.leaving && rowIndex.has(v.id) ? v : null;
4698
5178
  };
4699
5179
  const placeCursor = (animate) => {
4700
- if (!options.glide)
5180
+ if (!pill)
4701
5181
  return;
4702
5182
  const id = sel.ids.length === 1 ? sel.ids[0] : null;
4703
- const i = id === null ? undefined : rowIndex.get(id);
4704
- root.toggleAttribute("data-glide", i !== undefined);
4705
- if (i === undefined) {
4706
- cursor.removeAttribute("data-on");
4707
- cursorOn = false;
4708
- return;
4709
- }
4710
- if (cursorOn && animate && !reducedMotion(win))
4711
- cursorY.set(i * h);
4712
- else
4713
- cursorY.snap(i * h);
4714
- cursorOn = true;
4715
- cursor.setAttribute("data-on", "");
4716
- drawCursor();
4717
- if (!cursorY.resting)
4718
- loop.wake();
5183
+ root.toggleAttribute("data-glide", id !== null && rowIndex.has(id));
5184
+ const fresh = id !== picked;
5185
+ picked = id;
5186
+ if (animate && fresh) {
5187
+ travelUntil = performance.now() + 450;
5188
+ pill.update();
5189
+ } else if (performance.now() >= travelUntil)
5190
+ pill.update({ snap: true });
4719
5191
  };
4720
5192
  container.append(root);
4721
5193
  const scrollbar = overlayScrollbar(root, space);
@@ -4744,6 +5216,12 @@ function tree(container, options) {
4744
5216
  };
4745
5217
  const views = new Map;
4746
5218
  const pool = [];
5219
+ if (options.glide)
5220
+ pill = selectionPill(root, {
5221
+ rows: () => [...views.values()].filter((v) => !v.leaving).map((v) => v.el),
5222
+ selected: () => pickedRow()?.el ?? null,
5223
+ radius: (row) => Number.parseFloat(win.getComputedStyle(row, "::before").borderTopLeftRadius) || 0
5224
+ });
4747
5225
  const makeView = (id) => {
4748
5226
  const v = pool.pop() ?? createView();
4749
5227
  v.id = id;
@@ -4924,12 +5402,6 @@ function tree(container, options) {
4924
5402
  function frame(dt) {
4925
5403
  let moving = false;
4926
5404
  let bottom = rows.length * h;
4927
- if (options.glide && !cursorY.resting) {
4928
- cursorY.step(dt);
4929
- drawCursor();
4930
- if (!cursorY.resting)
4931
- moving = true;
4932
- }
4933
5405
  for (const v of views.values()) {
4934
5406
  v.y.step(dt);
4935
5407
  if (!v.y.resting)
@@ -4949,6 +5421,8 @@ function tree(container, options) {
4949
5421
  }
4950
5422
  setSpace(moving ? bottom : rows.length * h);
4951
5423
  trim();
5424
+ if (pill && moving && performance.now() >= travelUntil)
5425
+ pill.update({ snap: true });
4952
5426
  return moving;
4953
5427
  }
4954
5428
  const trim = () => {
@@ -5577,6 +6051,7 @@ function tree(container, options) {
5577
6051
  autoScroll.stop();
5578
6052
  ro.disconnect();
5579
6053
  scrollbar.dispose();
6054
+ pill?.dispose();
5580
6055
  dragging?.ghost.remove();
5581
6056
  root.remove();
5582
6057
  }
@@ -5595,6 +6070,7 @@ var init_tree = __esm(() => {
5595
6070
  init_motion();
5596
6071
  init_page_hints();
5597
6072
  init_look();
6073
+ init_selection_pill();
5598
6074
  init_tree_model();
5599
6075
  MOVE = springMotion(SPRINGS.default);
5600
6076
  });
@@ -5731,7 +6207,7 @@ function codeDiff(container, options) {
5731
6207
  };
5732
6208
  const nav = make("span", "knobs-diff-nav");
5733
6209
  const prev = button("Previous file", PREV);
5734
- const next = button("Next file", ICONS.caret);
6210
+ const next = button("Next file", NEXT);
5735
6211
  const place = make("span", "");
5736
6212
  nav.append(prev, place, next);
5737
6213
  const wrapButton = button("Wrap lines", WRAP);
@@ -5752,9 +6228,8 @@ function codeDiff(container, options) {
5752
6228
  const seg = make("div", "knobs-diff-seg");
5753
6229
  seg.setAttribute("role", "radiogroup");
5754
6230
  seg.setAttribute("aria-label", "Layout");
5755
- const pill = make("i", "knobs-diff-pill");
5756
6231
  const choice = (value, label) => {
5757
- const b = make("button", "");
6232
+ const b = make("button", "knobs-diff-choice");
5758
6233
  b.type = "button";
5759
6234
  b.setAttribute("role", "radio");
5760
6235
  b.dataset.value = value;
@@ -5763,7 +6238,7 @@ function codeDiff(container, options) {
5763
6238
  };
5764
6239
  const splitButton = choice("split", "Split");
5765
6240
  const unifiedButton = choice("unified", "Unified");
5766
- seg.append(pill, splitButton, unifiedButton);
6241
+ seg.append(splitButton, unifiedButton);
5767
6242
  if (options.lead)
5768
6243
  head.append(options.lead);
5769
6244
  head.append(path, stat, make("span", "knobs-diff-fill"), nav, wrapButton, seg, viewedButton);
@@ -5774,6 +6249,11 @@ function codeDiff(container, options) {
5774
6249
  body.append(note, probe);
5775
6250
  root.append(head, body);
5776
6251
  container.append(root);
6252
+ const segPill = selectionPill(seg, {
6253
+ rows: () => [splitButton, unifiedButton],
6254
+ selected: () => layout === "split" ? splitButton : unifiedButton
6255
+ });
6256
+ let pilled = layout;
5777
6257
  const fileNow = () => files.find((f) => f.name === current) ?? null;
5778
6258
  const paintHead = () => {
5779
6259
  const file = fileNow();
@@ -5806,6 +6286,10 @@ function codeDiff(container, options) {
5806
6286
  next.disabled = at >= files.length - 1;
5807
6287
  wrapButton.setAttribute("aria-pressed", String(wrap));
5808
6288
  seg.dataset.value = layout;
6289
+ if (pilled !== layout) {
6290
+ pilled = layout;
6291
+ segPill.update();
6292
+ }
5809
6293
  splitButton.setAttribute("aria-checked", String(layout === "split"));
5810
6294
  unifiedButton.setAttribute("aria-checked", String(layout === "unified"));
5811
6295
  splitButton.tabIndex = layout === "split" ? 0 : -1;
@@ -6640,6 +7124,7 @@ function codeDiff(container, options) {
6640
7124
  dispose() {
6641
7125
  disposed = true;
6642
7126
  loop.stop();
7127
+ segPill.dispose();
6643
7128
  sizes.disconnect();
6644
7129
  attaching.disconnect();
6645
7130
  resized.disconnect();
@@ -6995,7 +7480,7 @@ function diffReview(container, options) {
6995
7480
  }
6996
7481
  };
6997
7482
  }
6998
- var lib, load = () => lib ??= import("@pierre/diffs"), SYNTAX, IN, OUT, FADE4, HEIGHT, DRAWER, BLUR = 3, WRAP, PREV, UNVIEWED, VIEWED, SEARCH, CLEAR2, SIDEBAR, SEPARATOR = 32;
7483
+ var lib, load = () => lib ??= import("@pierre/diffs"), SYNTAX, IN, OUT, FADE4, HEIGHT, DRAWER, BLUR = 3, WRAP, PREV, NEXT, UNVIEWED, VIEWED, SEARCH, CLEAR2, SIDEBAR, SEPARATOR = 32;
6999
7484
  var init_diff = __esm(() => {
7000
7485
  init_glide();
7001
7486
  init_motion();
@@ -7003,6 +7488,8 @@ var init_diff = __esm(() => {
7003
7488
  init_diff_model();
7004
7489
  init_look();
7005
7490
  init_rgba();
7491
+ init_icons();
7492
+ init_selection_pill();
7006
7493
  init_tree();
7007
7494
  SYNTAX = { dark: "pierre-dark", light: "pierre-light" };
7008
7495
  IN = springMotion(SPRINGS.default);
@@ -7010,13 +7497,14 @@ var init_diff = __esm(() => {
7010
7497
  FADE4 = springMotion(FADE);
7011
7498
  HEIGHT = springMotion(SPRINGS.calm);
7012
7499
  DRAWER = springMotion(SPRINGS.calm);
7013
- WRAP = svg('<path d="M2.5 4h11"/><path d="M2.5 8h9a2 2 0 0 1 0 4H8.5"/><path d="M10 10.5 8.5 12l1.5 1.5"/><path d="M2.5 12h3"/>');
7014
- PREV = svg('<path d="M10 3.5 5.5 8l4.5 4.5"/>');
7015
- UNVIEWED = svg('<circle cx="8" cy="8" r="5.5"/>');
7016
- VIEWED = svg('<circle cx="8" cy="8" r="5.5"/><path d="m5.6 8.1 1.7 1.7 3.2-3.5"/>');
7017
- SEARCH = svg('<circle cx="7" cy="7" r="4.5"/><path d="m10.5 10.5 3 3"/>');
7018
- CLEAR2 = svg('<path d="m5 5 6 6M11 5l-6 6"/>');
7019
- SIDEBAR = svg('<rect x="2" y="3" width="12" height="10" rx="2"/><path d="M6.5 3v10"/>');
7500
+ WRAP = icon("arrowUDownLeft");
7501
+ PREV = icon("caretLeft");
7502
+ NEXT = icon("caretRight");
7503
+ UNVIEWED = icon("circle");
7504
+ VIEWED = icon("checkCircle");
7505
+ SEARCH = icon("magnifyingGlass", { small: true });
7506
+ CLEAR2 = icon("x", { small: true });
7507
+ SIDEBAR = icon("sidebar");
7020
7508
  });
7021
7509
 
7022
7510
  // src/ui/layer.ts
@@ -7999,10 +8487,25 @@ function advance(t, dt, o) {
7999
8487
  }
8000
8488
  return { t: hi, ended: true, wrapped: false };
8001
8489
  }
8002
- var SECOND_STEPS, FRAME_STEPS, toFrame = (t, fps) => Math.round(t * fps), frameTime = (t, fps) => Math.round(t * fps) / fps, round = (t) => Math.round(t * 1e6) / 1e6;
8490
+ function moveHead(head, t, motion, catching, minGap = 0) {
8491
+ if (motion === "jump" && Math.abs(t - head.x) > minGap)
8492
+ return { x: head.x, target: t, catching: true };
8493
+ if (catching)
8494
+ return { x: head.x + (t - head.target), target: t, catching: true };
8495
+ return { x: t, target: t, catching: false };
8496
+ }
8497
+ function currentMarker(markers, t, fps) {
8498
+ let best = null;
8499
+ for (const m of markers)
8500
+ if (m.time <= t + 0.5 / fps && (!best || m.time >= best.time))
8501
+ best = m;
8502
+ return best;
8503
+ }
8504
+ var SECOND_STEPS, FRAME_STEPS, toFrame = (t, fps) => Math.round(t * fps), frameTime = (t, fps) => Math.round(t * fps) / fps, round = (t) => Math.round(t * 1e6) / 1e6, CATCH_UP;
8003
8505
  var init_timeline_model = __esm(() => {
8004
8506
  SECOND_STEPS = [0.1, 0.2, 0.25, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 900, 1800, 3600];
8005
8507
  FRAME_STEPS = [1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 900, 1800, 3600, 9000, 18000];
8508
+ CATCH_UP = { duration: 0.15, bounce: 0 };
8006
8509
  });
8007
8510
 
8008
8511
  // src/ui/timeline.ts
@@ -8015,6 +8518,7 @@ function timeline(container, options) {
8015
8518
  let speed = options.speed ?? 1;
8016
8519
  let repeat = options.repeat ?? true;
8017
8520
  const arrows = options.arrows ?? "frames";
8521
+ const lane = options.variant === "lane";
8018
8522
  let time = Math.min(duration, Math.max(0, options.time ?? 0));
8019
8523
  let playing = false;
8020
8524
  let markers = [...options.markers ?? []];
@@ -8048,27 +8552,27 @@ function timeline(container, options) {
8048
8552
  root.setAttribute("aria-label", options.label ?? "Timeline");
8049
8553
  root.setAttribute("aria-keyshortcuts", "Space ArrowLeft ArrowRight , . Home End [ ] I O M");
8050
8554
  applyLook2(root, options);
8555
+ if (lane)
8556
+ root.dataset.variant = "lane";
8051
8557
  const transport = el("div", "knobs-tl-transport");
8052
8558
  const playBtn = el("button", "knobs-tl-play");
8053
8559
  playBtn.type = "button";
8054
8560
  playBtn.tabIndex = -1;
8055
- const readout = el("div", "knobs-tl-time");
8056
- const now = el("span", "knobs-tl-now");
8057
- const sub = el("span", "knobs-tl-sub");
8058
- readout.append(now, sub);
8059
- transport.append(playBtn, readout);
8561
+ transport.append(playBtn);
8060
8562
  const track = el("div", "knobs-tl-track");
8061
8563
  const canvas = el("canvas", "knobs-tl-ruler");
8062
8564
  canvas.setAttribute("aria-hidden", "true");
8063
- const lane = el("div", "knobs-tl-lane");
8565
+ const markerLane = el("div", "knobs-tl-lane");
8064
8566
  const hover = el("div", "knobs-tl-hover");
8567
+ const tag = el("span", "knobs-tl-tag");
8568
+ tag.setAttribute("aria-hidden", "true");
8065
8569
  const head = el("div", "knobs-tl-head");
8066
8570
  head.id = `${id}-head`;
8067
8571
  head.setAttribute("role", "slider");
8068
8572
  head.setAttribute("aria-label", "Playhead");
8069
8573
  head.setAttribute("aria-valuemin", "0");
8070
8574
  head.setAttribute("aria-valuemax", String(duration));
8071
- track.append(canvas, lane, hover, head);
8575
+ track.append(canvas, markerLane, hover, head, tag);
8072
8576
  root.append(transport, track);
8073
8577
  container.append(root);
8074
8578
  let width = 1;
@@ -8115,13 +8619,14 @@ function timeline(container, options) {
8115
8619
  let drawn = "";
8116
8620
  const drawRuler = () => {
8117
8621
  const v = view();
8118
- const key = `${v.start.toFixed(6)}:${v.span.toFixed(6)}:${width}:${units}:${fps}`;
8622
+ const key = `${v.start.toFixed(6)}:${v.span.toFixed(6)}:${width}:${height}:${units}:${fps}`;
8119
8623
  if (key === drawn)
8120
8624
  return;
8121
8625
  drawn = key;
8122
8626
  const dpr = win.devicePixelRatio || 1;
8123
8627
  const w = Math.max(1, Math.round(width * dpr));
8124
- const h = Math.round(RULER * dpr);
8628
+ const rh = lane ? height : RULER;
8629
+ const h = Math.round(rh * dpr);
8125
8630
  if (canvas.width !== w || canvas.height !== h) {
8126
8631
  canvas.width = w;
8127
8632
  canvas.height = h;
@@ -8130,8 +8635,19 @@ function timeline(container, options) {
8130
8635
  if (!ctx)
8131
8636
  return;
8132
8637
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
8133
- ctx.clearRect(0, 0, width, RULER);
8638
+ ctx.clearRect(0, 0, width, rh);
8134
8639
  const ticks = rulerTicks(v, width, { fps, units });
8640
+ if (lane) {
8641
+ ctx.fillStyle = colors.tick;
8642
+ for (const t of ticks.minor)
8643
+ if (t >= -0.000000001 && t <= duration + 0.000000001)
8644
+ ctx.fillRect(xOf(t) - 0.5, rh - 4, 1, 4);
8645
+ ctx.fillStyle = colors.major;
8646
+ for (const m of ticks.major)
8647
+ if (m.t >= -0.000000001 && m.t <= duration + 0.000000001)
8648
+ ctx.fillRect(xOf(m.t) - 0.5, rh - 7, 1, 7);
8649
+ return;
8650
+ }
8135
8651
  const px = (t) => Math.round(xOf(t)) + 0.5;
8136
8652
  ctx.fillStyle = colors.line;
8137
8653
  ctx.fillRect(0, RULER - 1, width, 1);
@@ -8171,18 +8687,21 @@ function timeline(container, options) {
8171
8687
  n.dataset.id = m.id;
8172
8688
  const pin = el("span", "knobs-tl-pin");
8173
8689
  const label = el("span", "knobs-tl-mlabel");
8690
+ label.append(el("span", "knobs-tl-mtext knobs-scroll-fade-x"));
8174
8691
  n.append(pin, label);
8175
- lane.append(n);
8692
+ markerLane.append(n);
8176
8693
  markerEls.set(m.id, n);
8177
8694
  if (built)
8178
8695
  enter(n);
8179
8696
  }
8180
8697
  const label = n.querySelector(".knobs-tl-mlabel");
8181
- if (label.textContent !== (m.label ?? ""))
8182
- label.textContent = m.label ?? "";
8698
+ const text = label.firstElementChild;
8699
+ if (text.textContent !== (m.label ?? ""))
8700
+ text.textContent = m.label ?? "";
8183
8701
  label.hidden = !m.label;
8184
8702
  n.style.setProperty("--knobs-tl-color", m.color ?? "");
8185
8703
  n.toggleAttribute("data-selected", selected?.kind === "marker" && selected.id === m.id);
8704
+ n.toggleAttribute("data-current", m.id === currentId);
8186
8705
  n.title = `${m.label ? `${m.label} · ` : ""}${clock(m.time)}`;
8187
8706
  }
8188
8707
  placeMarkers();
@@ -8210,7 +8729,7 @@ function timeline(container, options) {
8210
8729
  b.dataset.edge = "end";
8211
8730
  const label = el("span", "knobs-tl-rlabel");
8212
8731
  n.append(label, a, b);
8213
- track.insertBefore(n, lane);
8732
+ track.insertBefore(n, markerLane);
8214
8733
  rangeEls.set(r.id, n);
8215
8734
  if (built)
8216
8735
  enter(n);
@@ -8259,16 +8778,17 @@ function timeline(container, options) {
8259
8778
  };
8260
8779
  const clock = (t) => units === "frames" ? `f${toFrame(t, fps)}` : `${timeLabel(t, "seconds", fps, 0.01)}`;
8261
8780
  let shownHead = time;
8781
+ let currentId = null;
8262
8782
  const renderHead = () => {
8263
8783
  head.style.transform = `translateX(${xOf(shownHead).toFixed(2)}px)`;
8264
- const main = units === "frames" ? `f${toFrame(time, fps)}` : `${time.toFixed(2)}s`;
8265
- const minor = units === "frames" ? `${time.toFixed(2)}s` : `f${toFrame(time, fps)}`;
8266
- if (now.textContent !== main)
8267
- now.textContent = main;
8268
- if (sub.textContent !== minor)
8269
- sub.textContent = minor;
8270
8784
  head.setAttribute("aria-valuenow", time.toFixed(3));
8271
8785
  head.setAttribute("aria-valuetext", `${time.toFixed(2)} seconds, frame ${toFrame(time, fps)}`);
8786
+ const cur = currentMarker(markers, time, fps)?.id ?? null;
8787
+ if (cur !== currentId) {
8788
+ markerEls.get(currentId ?? "")?.removeAttribute("data-current");
8789
+ currentId = cur;
8790
+ markerEls.get(cur ?? "")?.setAttribute("data-current", "");
8791
+ }
8272
8792
  };
8273
8793
  let viewSent = "";
8274
8794
  const render = () => {
@@ -8294,18 +8814,27 @@ function timeline(container, options) {
8294
8814
  };
8295
8815
  const caret = new Spring(time, GLIDE.omega, GLIDE.zeta, 0.00001);
8296
8816
  let gliding = false;
8297
- const setTime = (t, source, shown = t, glide = false) => {
8298
- const next = Math.min(duration, Math.max(0, t));
8299
- if (glide && !reducedMotion(win)) {
8817
+ const headTo = (shown, motion) => {
8818
+ const from = gliding ? { x: caret.x, target: caret.target } : { x: shownHead, target: shownHead };
8819
+ const jump = motion === "jump" && !reducedMotion(win);
8820
+ const next = moveHead(from, shown, jump ? "jump" : "follow", gliding, 2 / pxPerSec());
8821
+ if (next.catching) {
8300
8822
  if (!gliding)
8301
- caret.snap(shownHead);
8302
- caret.set(next);
8823
+ caret.snap(from.x);
8824
+ caret.x = next.x;
8825
+ caret.target = next.target;
8303
8826
  gliding = true;
8827
+ shownHead = caret.x;
8304
8828
  loop.wake();
8305
8829
  } else {
8306
8830
  gliding = false;
8307
- shownHead = shown;
8831
+ caret.snap(next.x);
8832
+ shownHead = next.x;
8308
8833
  }
8834
+ };
8835
+ const setTime = (t, source, shown = t, glide = false) => {
8836
+ const next = Math.min(duration, Math.max(0, t));
8837
+ headTo(Math.min(duration, Math.max(0, shown)), glide ? "jump" : "follow");
8309
8838
  const moved = next !== time;
8310
8839
  time = next;
8311
8840
  renderHead();
@@ -8346,8 +8875,7 @@ function timeline(container, options) {
8346
8875
  options.onCross?.(m);
8347
8876
  }
8348
8877
  time = step.t;
8349
- shownHead = time;
8350
- gliding = false;
8878
+ headTo(time, step.wrapped ? "jump" : "follow");
8351
8879
  emitSeek(time, "play");
8352
8880
  keepInView();
8353
8881
  if (step.ended) {
@@ -8360,7 +8888,7 @@ function timeline(container, options) {
8360
8888
  shownHead = caret.step(dt);
8361
8889
  if (caret.resting) {
8362
8890
  gliding = false;
8363
- shownHead = time;
8891
+ shownHead = caret.target;
8364
8892
  }
8365
8893
  }
8366
8894
  vStart.step(dt);
@@ -8373,9 +8901,9 @@ function timeline(container, options) {
8373
8901
  return;
8374
8902
  const r = loopRange();
8375
8903
  if (r && (time < r.start || time >= r.end - 0.5 / fps))
8376
- setTime(r.start, "code");
8904
+ setTime(r.start, "code", r.start, true);
8377
8905
  else if (!r && time >= duration - 0.5 / fps)
8378
- setTime(0, "code");
8906
+ setTime(0, "code", 0, true);
8379
8907
  playing = true;
8380
8908
  syncPlay();
8381
8909
  emitPlay(true);
@@ -8504,24 +9032,35 @@ function timeline(container, options) {
8504
9032
  };
8505
9033
  const localX = (e) => e.clientX - track.getBoundingClientRect().left;
8506
9034
  let drag = null;
8507
- const scrubTo = (x) => {
9035
+ const scrubTo = (x, press = false) => {
8508
9036
  const shownX = softClamp(x, xOf(0), xOf(duration), 10);
8509
9037
  const s = scrubTime(tOf(shownX), {
8510
9038
  duration,
8511
9039
  fps,
8512
9040
  pxPerSec: pxPerSec(),
8513
- stops: snapMarkers ? markers.map((m) => m.time) : [],
9041
+ stops: snapMarkers && !lane ? markers.map((m) => m.time) : [],
8514
9042
  frames: snapFrames
8515
9043
  });
8516
- setTime(s.t, "scrub", s.shown);
9044
+ setTime(s.t, "scrub", s.shown, press);
8517
9045
  root.toggleAttribute("data-stuck", s.stop !== null);
8518
9046
  };
9047
+ const showTag = (x) => {
9048
+ const at = Math.min(duration, Math.max(0, tOf(x)));
9049
+ const text = `${at.toFixed(2)} s`;
9050
+ if (tag.textContent !== text)
9051
+ tag.textContent = text;
9052
+ const cx = Math.min(width, Math.max(0, x));
9053
+ const flip = cx > width - tag.offsetWidth - 12;
9054
+ tag.style.transform = `translateX(${cx.toFixed(1)}px) translateX(${flip ? "calc(-100% - 4px)" : "4px"})`;
9055
+ tag.dataset.on = "";
9056
+ };
9057
+ const hideTag = () => delete tag.dataset.on;
8519
9058
  track.addEventListener("pointerdown", (e) => {
8520
9059
  if (e.button !== 0 || editing)
8521
9060
  return;
8522
9061
  root.focus({ preventScroll: true });
8523
9062
  const t = e.target;
8524
- const markerEl = t.closest(".knobs-tl-marker");
9063
+ const markerEl = lane ? null : t.closest(".knobs-tl-marker");
8525
9064
  const edgeEl = t.closest(".knobs-tl-edge");
8526
9065
  const rangeEl = t.closest(".knobs-tl-range");
8527
9066
  track.setPointerCapture(e.pointerId);
@@ -8566,13 +9105,16 @@ function timeline(container, options) {
8566
9105
  root.dataset.scrubbing = "";
8567
9106
  for (const l of listeners)
8568
9107
  l.onScrub?.(true);
8569
- scrubTo(localX(e));
9108
+ scrubTo(localX(e), true);
9109
+ showTag(localX(e));
8570
9110
  }
8571
9111
  e.preventDefault();
8572
9112
  });
8573
9113
  track.addEventListener("pointermove", (e) => {
8574
9114
  const x = localX(e);
8575
9115
  hover.style.transform = `translateX(${x.toFixed(1)}px)`;
9116
+ if (!drag || drag.kind === "scrub")
9117
+ showTag(x);
8576
9118
  if (!drag)
8577
9119
  return;
8578
9120
  if (drag.kind === "scrub")
@@ -8618,8 +9160,10 @@ function timeline(container, options) {
8618
9160
  delete root.dataset.dragging;
8619
9161
  root.removeAttribute("data-stuck");
8620
9162
  if (d.kind === "scrub") {
8621
- shownHead = time;
9163
+ headTo(time, "follow");
8622
9164
  renderHead();
9165
+ if (!track.matches(":hover"))
9166
+ hideTag();
8623
9167
  for (const l of listeners)
8624
9168
  l.onScrub?.(false);
8625
9169
  if (d.wasPlaying)
@@ -8632,7 +9176,7 @@ function timeline(container, options) {
8632
9176
  } else {
8633
9177
  const m = markers.find((x) => x.id === d.id);
8634
9178
  if (m)
8635
- setTime(m.time, "marker");
9179
+ setTime(m.time, "marker", m.time, true);
8636
9180
  }
8637
9181
  } else if (d.kind === "edge" || d.moved)
8638
9182
  emitRanges();
@@ -8641,10 +9185,14 @@ function timeline(container, options) {
8641
9185
  track.addEventListener("pointercancel", endDrag);
8642
9186
  track.addEventListener("lostpointercapture", endDrag);
8643
9187
  track.addEventListener("pointerleave", () => {
8644
- if (!drag)
8645
- hover.style.transform = "translateX(-100px)";
9188
+ if (drag)
9189
+ return;
9190
+ hover.style.transform = "translateX(-100px)";
9191
+ hideTag();
8646
9192
  });
8647
9193
  track.addEventListener("dblclick", (e) => {
9194
+ if (lane)
9195
+ return;
8648
9196
  const markerEl = e.target.closest(".knobs-tl-marker");
8649
9197
  if (markerEl)
8650
9198
  return rename(markerEl.dataset.id);
@@ -8655,6 +9203,8 @@ function timeline(container, options) {
8655
9203
  rename(m.id);
8656
9204
  });
8657
9205
  track.addEventListener("wheel", (e) => {
9206
+ if (lane)
9207
+ return;
8658
9208
  e.preventDefault();
8659
9209
  const scale = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? height : 1;
8660
9210
  if (e.ctrlKey || e.metaKey) {
@@ -8718,7 +9268,7 @@ function timeline(container, options) {
8718
9268
  if (!m)
8719
9269
  return;
8720
9270
  select({ kind: "marker", id: m.id });
8721
- return setTime(m.time, "marker");
9271
+ return setTime(m.time, "marker", m.time, true);
8722
9272
  }
8723
9273
  case "i":
8724
9274
  case "I":
@@ -8730,11 +9280,13 @@ function timeline(container, options) {
8730
9280
  return setEdge("end", time);
8731
9281
  case "m":
8732
9282
  case "M":
9283
+ if (lane)
9284
+ return;
8733
9285
  claim();
8734
9286
  addMarker(time);
8735
9287
  return;
8736
9288
  case "Enter":
8737
- if (selected?.kind === "marker") {
9289
+ if (selected?.kind === "marker" && !lane) {
8738
9290
  claim();
8739
9291
  rename(selected.id);
8740
9292
  }
@@ -8752,19 +9304,25 @@ function timeline(container, options) {
8752
9304
  return;
8753
9305
  case "=":
8754
9306
  case "+":
9307
+ if (lane)
9308
+ return;
8755
9309
  claim();
8756
9310
  return zoom(1.5);
8757
9311
  case "-":
9312
+ if (lane)
9313
+ return;
8758
9314
  claim();
8759
9315
  return zoom(1 / 1.5);
8760
9316
  case "0":
9317
+ if (lane)
9318
+ return;
8761
9319
  claim();
8762
9320
  return fit();
8763
9321
  }
8764
9322
  });
8765
9323
  const unregisterHints = options.hints === false ? undefined : hints.register(root, () => [
8766
9324
  { el: playBtn, label: playing ? "Pause" : "Play", activate: toggle },
8767
- ...markers.flatMap((m) => {
9325
+ ...(lane ? [] : markers).flatMap((m) => {
8768
9326
  const n = markerEls.get(m.id);
8769
9327
  if (!n || n.dataset.leaving !== undefined)
8770
9328
  return [];
@@ -8776,7 +9334,7 @@ function timeline(container, options) {
8776
9334
  ...label && !label.hidden ? { labelEl: label } : {},
8777
9335
  activate: () => {
8778
9336
  select({ kind: "marker", id: m.id });
8779
- setTime(m.time, "marker");
9337
+ setTime(m.time, "marker", m.time, true);
8780
9338
  root.focus({ preventScroll: true });
8781
9339
  }
8782
9340
  }
@@ -8788,7 +9346,7 @@ function timeline(container, options) {
8788
9346
  activate: () => root.focus({ preventScroll: true })
8789
9347
  }
8790
9348
  ], { listen: false, rank: 1 });
8791
- const menu = contextMenu(track, (e) => {
9349
+ const menu = lane ? null : contextMenu(track, (e) => {
8792
9350
  const t = e?.target ?? null;
8793
9351
  const markerEl = t?.closest(".knobs-tl-marker");
8794
9352
  const rangeEl = t?.closest(".knobs-tl-range");
@@ -8803,7 +9361,7 @@ function timeline(container, options) {
8803
9361
  run: () => {
8804
9362
  const m = markers.find((x) => x.id === mid);
8805
9363
  if (m)
8806
- setTime(m.time, "marker");
9364
+ setTime(m.time, "marker", m.time, true);
8807
9365
  }
8808
9366
  },
8809
9367
  { label: "Rename…", shortcut: "Enter", run: () => rename(mid) },
@@ -8967,7 +9525,7 @@ function timeline(container, options) {
8967
9525
  listeners.clear();
8968
9526
  loop.stop();
8969
9527
  ro.disconnect();
8970
- menu.dispose();
9528
+ menu?.dispose();
8971
9529
  root.remove();
8972
9530
  }
8973
9531
  };
@@ -8984,10 +9542,21 @@ var init_timeline = __esm(() => {
8984
9542
  init_timeline_model();
8985
9543
  INSTANT2 = springMotion(SPRINGS.instant);
8986
9544
  CALM = springMotion(SPRINGS.calm);
8987
- GLIDE = springMotion(SPRINGS.snappy);
9545
+ GLIDE = springMotion(CATCH_UP);
8988
9546
  });
8989
9547
 
8990
9548
  // src/ui/compare-model.ts
9549
+ function compareModes(modes, mode) {
9550
+ const list = [...new Set((modes ?? ["side", "wipe"]).filter((m) => ALL_MODES.includes(m)))];
9551
+ const offered = list.length ? list : ["side", "wipe"];
9552
+ return { modes: offered, mode: mode && offered.includes(mode) ? mode : offered[0] };
9553
+ }
9554
+ function modeForKey(key, modes) {
9555
+ const k = key.toLowerCase();
9556
+ if (/^[1-9]$/.test(k))
9557
+ return modes[Number(k) - 1] ?? null;
9558
+ return modes.find((m) => MODE_KEYS[m] === k) ?? null;
9559
+ }
8991
9560
  function mediaKind(src, kind) {
8992
9561
  if (kind)
8993
9562
  return kind;
@@ -9066,6 +9635,18 @@ function wipeGrab(x, rect, wipe, slop = 12) {
9066
9635
  const onLine = Math.abs(x - line) <= slop;
9067
9636
  return { onLine, offset: onLine ? x - line : 0 };
9068
9637
  }
9638
+ function seamLabels(x, left, right, wb, wa, gap = 8) {
9639
+ const room = (space, w) => Math.min(1, Math.max(0, (space - w - 2 * gap) / gap));
9640
+ return {
9641
+ before: { x: x - gap - wb, opacity: room(x - left, wb) },
9642
+ after: { x: x + gap, opacity: room(right - x, wa) }
9643
+ };
9644
+ }
9645
+ function gripTop(y, rect, grip, margin = 8) {
9646
+ const lo = rect.y + margin;
9647
+ const hi = rect.y + rect.h - margin - grip;
9648
+ return hi < lo ? rect.y + (rect.h - grip) / 2 : Math.min(hi, Math.max(lo, y - grip / 2));
9649
+ }
9069
9650
  function stripColumns(view, width, o) {
9070
9651
  const { fps } = o;
9071
9652
  const total = Math.max(1, Math.round(o.duration * fps));
@@ -9116,7 +9697,7 @@ function stripPeaks(diffs, o = {}) {
9116
9697
  });
9117
9698
  return out;
9118
9699
  }
9119
- var VIDEO, frameOf = (t, fps) => Math.max(0, Math.floor(t * fps + 0.0001)), lastFrame = (duration, fps) => Math.max(0, Math.ceil(duration * fps - 0.0001) - 1), sameFrame = (a, b, fps) => frameOf(a, fps) === frameOf(b, fps), SEEK_IDLE, splitHeight = (w, media) => media.w > 0 && media.h > 0 ? w / 2 * (media.h / media.w) : w / 2 * (9 / 16), ZOOM_FIT, ZOOM_MAX = 16, panBy = (r, dx, dy, base) => clampZoom({
9700
+ var ALL_MODES, MODE_KEYS, modeKey = (mode) => MODE_KEYS[mode].toUpperCase(), VIDEO, frameOf = (t, fps) => Math.max(0, Math.floor(t * fps + 0.0001)), lastFrame = (duration, fps) => Math.max(0, Math.ceil(duration * fps - 0.0001) - 1), sameFrame = (a, b, fps) => frameOf(a, fps) === frameOf(b, fps), SEEK_IDLE, splitHeight = (w, media) => media.w > 0 && media.h > 0 ? w / 2 * (media.h / media.w) : w / 2 * (9 / 16), ZOOM_FIT, ZOOM_MAX = 16, panBy = (r, dx, dy, base) => clampZoom({
9120
9701
  u: r.u - dx / Math.max(1, base.w) * r.w,
9121
9702
  v: r.v - dy / Math.max(1, base.h) * r.h,
9122
9703
  w: r.w
@@ -9135,6 +9716,8 @@ var VIDEO, frameOf = (t, fps) => Math.max(0, Math.floor(t * fps + 0.0001)), last
9135
9716
  h: a.h + (b.h - a.h) * k
9136
9717
  }), wipeX = (rect, wipe) => rect.x + Math.min(1, Math.max(0, wipe)) * rect.w, wipeAt = (x, rect, offset = 0) => rect.w <= 0 ? 0.5 : Math.min(1, Math.max(0, (x - offset - rect.x) / rect.w)), wipeKey = (wipe, dir, big = false) => Math.min(1, Math.max(0, Math.round((wipe + dir * (big ? 0.1 : 0.01)) * 1000) / 1000));
9137
9718
  var init_compare_model = __esm(() => {
9719
+ ALL_MODES = ["side", "wipe", "difference"];
9720
+ MODE_KEYS = { side: "s", wipe: "w", difference: "d" };
9138
9721
  VIDEO = /\.(mp4|m4v|mov|webm|ogv|mkv)$/i;
9139
9722
  SEEK_IDLE = { seeking: false, pending: null };
9140
9723
  ZOOM_FIT = { u: 0, v: 0, w: 1, h: 1 };
@@ -9934,8 +10517,8 @@ async function createCompareGpu(canvas, onLost) {
9934
10517
  r(0, f.before);
9935
10518
  r(4, f.after);
9936
10519
  data.set([f.width, f.height, f.dpr, 0], 8);
9937
- data.set([f.wipeX, f.soft, 0, f.phase ? 1 : 0], 12);
9938
- data.set([f.from, f.to, f.mix, f.flick ? 1 : 0], 16);
10520
+ data.set([f.wipeX, f.soft, 0, 0], 12);
10521
+ data.set([f.from, f.to, f.mix, 0], 16);
9939
10522
  data.set([f.threshold, f.gain, ready[0], ready[1]], 20);
9940
10523
  data.set([f.accent[0], f.accent[1], f.accent[2], 1], 24);
9941
10524
  const l = f.loupe;
@@ -9977,8 +10560,8 @@ var MODE_INDEX, WGSL = `
9977
10560
  struct U {
9978
10561
  ra: vec4f, rb: vec4f,
9979
10562
  view: vec4f, // w, h, dpr, _
9980
- wipe: vec4f, // x, soft, _, phase
9981
- modes: vec4f, // from, to, mix, flick
10563
+ wipe: vec4f, // x, soft, _, _
10564
+ modes: vec4f, // from, to, mix, _
9982
10565
  diff: vec4f, // threshold, gain, readyA, readyB
9983
10566
  accent: vec4f,
9984
10567
  loupe: vec4f, // x, y, r, zoom (0: none)
@@ -10029,21 +10612,14 @@ fn takeB(p: vec2f, r: vec4f, c0: vec4f, near: bool) -> vec4f {
10029
10612
 
10030
10613
  fn pm(c: vec4f) -> vec4f { return vec4f(c.rgb * c.a, c.a); }
10031
10614
 
10032
- fn composite(mode: u32, p: vec2f, a: vec4f, b: vec4f, a2: vec4f) -> vec4f {
10033
- let flick = u.modes.w > 0.5;
10615
+ fn composite(mode: u32, p: vec2f, a: vec4f, b: vec4f) -> vec4f {
10034
10616
  switch mode {
10035
- case 0u: {
10036
- let bb = select(b, vec4f(a2.rgb, b.a), flick);
10037
- return vec4f(a.rgb * a.a + bb.rgb * bb.a * (1.0 - a.a), a.a + bb.a * (1.0 - a.a));
10038
- }
10039
10617
  case 1u: {
10040
- if (flick) { return pm(a); }
10041
10618
  let s = max(u.wipe.y, 0.5);
10042
10619
  let m = smoothstep(u.wipe.x - s, u.wipe.x + s, p.x);
10043
10620
  return pm(vec4f(mix(a.rgb, b.rgb, m), mix(a.a, b.a, m)));
10044
10621
  }
10045
10622
  case 2u: {
10046
- if (flick) { return pm(a); }
10047
10623
  let d = abs(a.rgb - b.rgb);
10048
10624
  let h = smoothstep(u.diff.x, u.diff.x + 0.06, max(d.r, max(d.g, d.b)) * u.diff.y) * min(a.a, b.a);
10049
10625
  let c = mix(vec3f(luma(b.rgb) * 0.35), u.accent.rgb, h);
@@ -10051,8 +10627,8 @@ fn composite(mode: u32, p: vec2f, a: vec4f, b: vec4f, a2: vec4f) -> vec4f {
10051
10627
  }
10052
10628
  default: {}
10053
10629
  }
10054
- // Flicker: After, or Before while X is held or on the flicker's other beat.
10055
- return pm(select(b, a, flick || u.wipe.w > 0.5));
10630
+ // Side by side: each side in its own place, Before over After where they meet.
10631
+ return vec4f(a.rgb * a.a + b.rgb * b.a * (1.0 - a.a), a.a + b.a * (1.0 - a.a));
10056
10632
  }
10057
10633
 
10058
10634
  @fragment fn fs(@builtin(position) q: vec4f) -> @location(0) vec4f {
@@ -10070,16 +10646,14 @@ fn composite(mode: u32, p: vec2f, a: vec4f, b: vec4f, a2: vec4f) -> vec4f {
10070
10646
  }
10071
10647
  let a = takeA(p, u.ra, u.ca, near);
10072
10648
  let b = takeB(p, u.rb, u.cb, near);
10073
- // Before in After's place (X held, side by side): After's rect, as if Before were zoomed there.
10074
- let a2 = takeA(p, u.rb, u.cb, near);
10075
- var o = mix(composite(u32(u.modes.x), p, a, b, a2), composite(u32(u.modes.y), p, a, b, a2), u.modes.z);
10649
+ var o = mix(composite(u32(u.modes.x), p, a, b), composite(u32(u.modes.y), p, a, b), u.modes.z);
10076
10650
  if (near) { o = o + vec4f(0.05, 0.05, 0.06, 1.0) * (1.0 - o.a); }
10077
10651
  o = mix(o, vec4f(0.92), ring * 0.9);
10078
10652
  return o;
10079
10653
  }
10080
10654
  `, TEXTURE_BINDING = 4, COPY_DST = 2, RENDER_ATTACHMENT = 16, shared = null, lostListeners;
10081
10655
  var init_compare_gpu = __esm(() => {
10082
- MODE_INDEX = { side: 0, wipe: 1, difference: 2, flicker: 3 };
10656
+ MODE_INDEX = { side: 0, wipe: 1, difference: 2 };
10083
10657
  lostListeners = new Set;
10084
10658
  });
10085
10659
 
@@ -10465,7 +11039,9 @@ function compare(container, options) {
10465
11039
  ...options.theme ? { theme: options.theme } : {},
10466
11040
  ...options.appearance ? { appearance: options.appearance } : {}
10467
11041
  };
10468
- let mode = options.mode ?? "side";
11042
+ const offered = compareModes(options.modes, options.mode);
11043
+ const modeList = offered.modes;
11044
+ let mode = offered.mode;
10469
11045
  let wipe = Math.min(1, Math.max(0, options.wipe ?? 0.5));
10470
11046
  let loupeOn = options.loupe ?? false;
10471
11047
  let threshold = options.threshold ?? 4 / 255;
@@ -10483,7 +11059,7 @@ function compare(container, options) {
10483
11059
  root.setAttribute("role", "group");
10484
11060
  root.setAttribute("aria-roledescription", "compare");
10485
11061
  root.setAttribute("aria-label", options.label ?? "Compare");
10486
- root.setAttribute("aria-keyshortcuts", "Space ArrowLeft ArrowRight Shift+ArrowLeft Shift+ArrowRight , . 1 2 3 4 S W D X Z L 0 = -");
11062
+ root.setAttribute("aria-keyshortcuts", `Space ArrowLeft ArrowRight Shift+ArrowLeft Shift+ArrowRight , . ${modeList.map((m, i) => `${i + 1} ${modeKey(m)}`).join(" ")} Z L 0 = -`);
10487
11063
  applyLook2(root, options);
10488
11064
  root.dataset.mode = mode;
10489
11065
  if (auto)
@@ -10492,35 +11068,37 @@ function compare(container, options) {
10492
11068
  const modes = el("div", "knobs-cmp-modes");
10493
11069
  modes.setAttribute("role", "radiogroup");
10494
11070
  modes.setAttribute("aria-label", "Mode");
10495
- const modeBtns = MODES2.map((m, i) => {
11071
+ modes.hidden = modeList.length < 2;
11072
+ const modeBtns = modeList.map((m, i) => {
10496
11073
  const b = el("button", "knobs-cmp-mode");
10497
11074
  b.type = "button";
10498
11075
  b.tabIndex = -1;
10499
- b.textContent = m.label;
10500
- b.title = `${m.label} · ${i + 1}${m.key ? ` or ${m.key}` : " (hold X to flick)"}`;
11076
+ b.textContent = MODE_LABEL[m].short;
11077
+ b.title = `${MODE_LABEL[m].long} · ${i + 1} or ${modeKey(m)}`;
10501
11078
  b.setAttribute("role", "radio");
10502
- b.setAttribute("aria-keyshortcuts", m.key ? `${i + 1} ${m.key}` : `${i + 1}`);
11079
+ b.setAttribute("aria-keyshortcuts", `${i + 1} ${modeKey(m)}`);
10503
11080
  b.addEventListener("click", () => {
10504
- setMode(m.mode);
11081
+ setMode(m);
10505
11082
  root.focus({ preventScroll: true });
10506
11083
  });
10507
11084
  modes.append(b);
10508
11085
  return b;
10509
11086
  });
10510
- const loupeBtn = el("button", "knobs-cmp-toggle");
10511
- loupeBtn.type = "button";
10512
- loupeBtn.tabIndex = -1;
10513
- loupeBtn.textContent = "Loupe";
10514
- loupeBtn.title = "Loupe · hold Z";
11087
+ const toggleBtn = (name, label, title) => {
11088
+ const b = el("button", "knobs-cmp-toggle");
11089
+ b.type = "button";
11090
+ b.tabIndex = -1;
11091
+ b.innerHTML = icon(name);
11092
+ b.setAttribute("aria-label", label);
11093
+ b.title = title;
11094
+ return b;
11095
+ };
11096
+ const loupeBtn = toggleBtn("magnifyingGlass", "Loupe", "Loupe · hold Z");
10515
11097
  loupeBtn.addEventListener("click", () => {
10516
11098
  setLoupe(!loupeOn);
10517
11099
  root.focus({ preventScroll: true });
10518
11100
  });
10519
- const loopBtn = el("button", "knobs-cmp-toggle");
10520
- loopBtn.type = "button";
10521
- loopBtn.tabIndex = -1;
10522
- loopBtn.textContent = "Loop";
10523
- loopBtn.title = "Loop · L (drag its ends on the timeline)";
11101
+ const loopBtn = toggleBtn("repeat", "Loop", "Loop · L (drag its ends on the timeline)");
10524
11102
  loopBtn.setAttribute("aria-keyshortcuts", "L");
10525
11103
  loopBtn.addEventListener("click", () => {
10526
11104
  api.setLoop(!loopNow());
@@ -10537,7 +11115,8 @@ function compare(container, options) {
10537
11115
  const zoomReset = el("button", "knobs-cmp-zoom-reset");
10538
11116
  zoomReset.type = "button";
10539
11117
  zoomReset.tabIndex = -1;
10540
- zoomReset.textContent = "Reset";
11118
+ zoomReset.innerHTML = icon("arrowCounterClockwise", { small: true });
11119
+ zoomReset.setAttribute("aria-label", "Reset zoom");
10541
11120
  zoomReset.title = "Fit · 0 or double-click";
10542
11121
  zoomReset.addEventListener("click", () => {
10543
11122
  api.setZoom(null);
@@ -10547,11 +11126,25 @@ function compare(container, options) {
10547
11126
  const labels = [el("div", "knobs-cmp-label"), el("div", "knobs-cmp-label")];
10548
11127
  labels[0].dataset.side = "before";
10549
11128
  labels[1].dataset.side = "after";
11129
+ const setLabel = (index, text) => {
11130
+ const l = labels[index];
11131
+ if (!l.firstElementChild) {
11132
+ const mark = el("span", "knobs-cmp-mark");
11133
+ mark.innerHTML = icon(index === 0 ? "x" : "check", { small: true });
11134
+ l.append(mark, el("span", "knobs-cmp-name"));
11135
+ }
11136
+ const name = l.lastElementChild;
11137
+ if (name.textContent !== text)
11138
+ name.textContent = text;
11139
+ };
10550
11140
  const handle = el("div", "knobs-cmp-handle");
10551
11141
  handle.setAttribute("role", "slider");
10552
11142
  handle.setAttribute("aria-label", "Wipe");
10553
11143
  handle.setAttribute("aria-valuemin", "0");
10554
11144
  handle.setAttribute("aria-valuemax", "100");
11145
+ const grip = el("span", "knobs-cmp-grip");
11146
+ grip.setAttribute("aria-hidden", "true");
11147
+ handle.append(grip);
10555
11148
  const note = el("div", "knobs-cmp-note");
10556
11149
  stage.append(canvas, split, labels[0], labels[1], handle, zoomPill, note);
10557
11150
  root.append(bar, stage);
@@ -10568,6 +11161,7 @@ function compare(container, options) {
10568
11161
  root.append(host);
10569
11162
  tl = timeline(host, {
10570
11163
  ...look,
11164
+ variant: "lane",
10571
11165
  duration: 1,
10572
11166
  fps: fps(),
10573
11167
  arrows: { step: 0.5, shift: 2 },
@@ -10586,9 +11180,9 @@ function compare(container, options) {
10586
11180
  const fade = new Spring(1, FADE6.omega, FADE6.zeta, 0.001);
10587
11181
  let fromMode = mode;
10588
11182
  let toMode = mode;
10589
- let phase = false;
10590
- let flickerTimer = 0;
10591
- let flickHeld = false;
11183
+ const wipeLine = new Spring(wipe, WIPE_CATCH.omega, WIPE_CATCH.zeta, 0.0001);
11184
+ let wipeCatching = false;
11185
+ let gripY = null;
10592
11186
  let loupeHeld = false;
10593
11187
  let pointer = null;
10594
11188
  let drag = null;
@@ -10906,25 +11500,16 @@ function compare(container, options) {
10906
11500
  gpu = g;
10907
11501
  setRenderer("gpu");
10908
11502
  };
10909
- const syncFlicker = () => {
10910
- win.clearInterval(flickerTimer);
10911
- flickerTimer = 0;
10912
- phase = false;
10913
- if (mode === "flicker")
10914
- flickerTimer = win.setInterval(() => {
10915
- phase = !phase;
10916
- requestDraw();
10917
- }, FLICKER_MS);
10918
- };
10919
11503
  const syncModeUi = () => {
10920
11504
  root.dataset.mode = mode;
10921
- MODES2.forEach((m, i) => {
10922
- modeBtns[i].setAttribute("aria-checked", String(m.mode === mode));
11505
+ modeList.forEach((m, i) => {
11506
+ modeBtns[i].setAttribute("aria-checked", String(m === mode));
10923
11507
  });
10924
11508
  handle.tabIndex = mode === "wipe" ? 0 : -1;
11509
+ modePill?.update();
10925
11510
  };
10926
11511
  const setMode = (next, emit = true) => {
10927
- if (next === mode)
11512
+ if (next === mode || !modeList.includes(next))
10928
11513
  return;
10929
11514
  mode = next;
10930
11515
  const reduced = reducedMotion(win);
@@ -10940,19 +11525,29 @@ function compare(container, options) {
10940
11525
  else
10941
11526
  lay.set(next === "side" ? 0 : 1);
10942
11527
  syncModeUi();
10943
- syncFlicker();
10944
11528
  if (emit)
10945
11529
  options.onMode?.(next);
10946
11530
  requestDraw();
10947
11531
  };
10948
- const setWipe = (next, emit = true) => {
11532
+ const setWipe = (next, emit = true, jump = false) => {
10949
11533
  const w = Math.min(1, Math.max(0, next));
10950
- if (w === wipe)
11534
+ if (w === wipe && !jump)
10951
11535
  return;
10952
11536
  wipe = w;
11537
+ const minGap = 2 / Math.max(1, wipeRect(layoutNow().L.overlay).w);
11538
+ const head = moveHead({ x: wipeLine.x, target: wipeLine.target }, w, jump && !reducedMotion(win) ? "jump" : "follow", wipeCatching, minGap);
11539
+ wipeCatching = head.catching;
11540
+ if (head.catching) {
11541
+ wipeLine.x = head.x;
11542
+ wipeLine.target = head.target;
11543
+ } else
11544
+ wipeLine.snap(head.x);
10953
11545
  if (emit)
10954
11546
  options.onWipe?.(w);
10955
- drawNow();
11547
+ if (head.catching)
11548
+ requestDraw();
11549
+ else
11550
+ drawNow();
10956
11551
  };
10957
11552
  const setLoupe = (on) => {
10958
11553
  loupeOn = on;
@@ -11105,28 +11700,44 @@ function compare(container, options) {
11105
11700
  viewSent = key;
11106
11701
  options.onView({ zoom: clampZoom(z), loop: l });
11107
11702
  };
11703
+ const labelsIn = (m, r, wr, wx) => {
11704
+ if (m === "wipe") {
11705
+ const at = seamLabels(wx, wr.x, wr.x + wr.w, labelW[0], labelW[1], INSET);
11706
+ return [at.before, at.after];
11707
+ }
11708
+ const ax = r.after.x + INSET + (r.after.w - labelW[1] - INSET * 2) * lay.x;
11709
+ return [
11710
+ { x: r.before.x + INSET, opacity: 1 },
11711
+ { x: ax, opacity: 1 }
11712
+ ];
11713
+ };
11714
+ const placeGrip = (wr) => {
11715
+ const gy = gripTop(gripY ?? wr.y + wr.h / 2, wr, GRIP.h) - wr.y;
11716
+ grip.style.transform = `translate3d(0,${gy.toFixed(2)}px,0)`;
11717
+ };
11108
11718
  const draw = () => {
11109
11719
  dirty = false;
11110
11720
  const r = layoutNow();
11111
11721
  const wr = wipeRect(r.L.overlay);
11112
- const wx = wipeX(wr, wipe);
11722
+ const wx = wipeX(wr, wipeLine.x);
11113
11723
  const k = lay.x;
11114
- const ax = r.after.x + INSET + (r.after.w - labelW[1] - INSET * 2) * k;
11115
- labels[0].style.transform = `translate(${(r.before.x + INSET).toFixed(2)}px,${(r.before.y + INSET).toFixed(2)}px)`;
11116
- labels[1].style.transform = `translate(${ax.toFixed(2)}px,${(r.after.y + INSET).toFixed(2)}px)`;
11117
- const showsBefore = flickHeld || mode === "flicker" && phase;
11118
- const flickering = flickHeld || mode === "flicker";
11119
- labels[0].toggleAttribute("data-lit", flickering && showsBefore);
11120
- labels[1].toggleAttribute("data-lit", flickering && !showsBefore);
11121
- labels[0].toggleAttribute("data-dim", flickering && !showsBefore || mode === "wipe" && wipe < 0.02);
11122
- labels[1].toggleAttribute("data-dim", flickering && showsBefore || mode === "wipe" && wipe > 0.98);
11724
+ const a = labelsIn(fromMode, r, wr, wx);
11725
+ const b = labelsIn(toMode, r, wr, wx);
11726
+ const f = Math.min(1, Math.max(0, fade.x));
11727
+ const ys = [r.before.y + INSET, r.after.y + INSET];
11728
+ for (const i of [0, 1]) {
11729
+ const x = a[i].x + (b[i].x - a[i].x) * f;
11730
+ labels[i].style.transform = `translate(${x.toFixed(2)}px,${ys[i].toFixed(2)}px)`;
11731
+ labels[i].style.opacity = (a[i].opacity + (b[i].opacity - a[i].opacity) * f).toFixed(3);
11732
+ }
11123
11733
  const top = Math.min(r.L.side.before.y, r.L.side.after.y);
11124
11734
  split.style.transform = `translate(${r.L.side.split.toFixed(2)}px,${top.toFixed(2)}px)`;
11125
11735
  split.style.height = `${Math.max(0, r.L.side.after.h).toFixed(2)}px`;
11126
11736
  split.style.opacity = String(Math.max(0, 1 - k * 2));
11127
- handle.toggleAttribute("data-on", mode === "wipe" && !flickHeld);
11737
+ handle.toggleAttribute("data-on", mode === "wipe");
11128
11738
  handle.style.transform = `translate(${wx.toFixed(2)}px,${wr.y.toFixed(2)}px)`;
11129
11739
  handle.style.height = `${Math.max(0, wr.h).toFixed(2)}px`;
11740
+ placeGrip(wr);
11130
11741
  handle.setAttribute("aria-valuenow", String(Math.round(wipe * 100)));
11131
11742
  handle.setAttribute("aria-valuetext", `${Math.round(wipe * 100)}% Before`);
11132
11743
  const pct = `${Math.round(100 / zw.x)}%`;
@@ -11149,7 +11760,7 @@ function compare(container, options) {
11149
11760
  const right = Math.max(0, rect.x + rect.w - Math.min(clip.x + clip.w, cutAt));
11150
11761
  st.clipPath = t + l + b + right > 0.01 ? `inset(${t.toFixed(2)}px ${right.toFixed(2)}px ${b.toFixed(2)}px ${l.toFixed(2)}px)` : "";
11151
11762
  };
11152
- const cut = mode === "wipe" && !flickHeld ? wx : Number.POSITIVE_INFINITY;
11763
+ const cut = mode === "wipe" ? wx : Number.POSITIVE_INFINITY;
11153
11764
  for (const s of sides) {
11154
11765
  const rect = s.index === 0 ? r.zBefore : r.zAfter;
11155
11766
  const clip = s.index === 0 ? r.before : r.after;
@@ -11158,10 +11769,8 @@ function compare(container, options) {
11158
11769
  place(s.preview, rect, clip, at);
11159
11770
  if (s.index !== 0)
11160
11771
  continue;
11161
- for (const n of [s.el, s.preview]) {
11162
- n.style.mixBlendMode = mode === "difference" && !flickHeld ? "difference" : "";
11163
- n.style.opacity = mode === "flicker" && !showsBefore ? "0" : "";
11164
- }
11772
+ for (const n of [s.el, s.preview])
11773
+ n.style.mixBlendMode = mode === "difference" ? "difference" : "";
11165
11774
  }
11166
11775
  return;
11167
11776
  }
@@ -11182,8 +11791,6 @@ function compare(container, options) {
11182
11791
  from: MODE_INDEX[fromMode],
11183
11792
  to: MODE_INDEX[toMode],
11184
11793
  mix: Math.min(1, Math.max(0, fade.x)),
11185
- flick: flickHeld,
11186
- phase,
11187
11794
  threshold,
11188
11795
  gain: 1,
11189
11796
  accent,
@@ -11199,6 +11806,11 @@ function compare(container, options) {
11199
11806
  const loop = frameLoop(win, (dt) => {
11200
11807
  lay.step(dt);
11201
11808
  fade.step(dt);
11809
+ if (wipeCatching) {
11810
+ wipeLine.step(dt);
11811
+ if (wipeLine.resting)
11812
+ wipeCatching = false;
11813
+ }
11202
11814
  zu.step(dt);
11203
11815
  zv.step(dt);
11204
11816
  zw.step(dt);
@@ -11232,7 +11844,7 @@ function compare(container, options) {
11232
11844
  pushFrame(s);
11233
11845
  }
11234
11846
  }
11235
- const moving = !lay.resting || !fade.resting || !zoomResting();
11847
+ const moving = !lay.resting || !fade.resting || !zoomResting() || wipeCatching;
11236
11848
  if (moving)
11237
11849
  dirty = true;
11238
11850
  if (auto && !lay.resting)
@@ -11375,16 +11987,19 @@ function compare(container, options) {
11375
11987
  e.preventDefault();
11376
11988
  return startPinch();
11377
11989
  }
11378
- if (mode === "wipe" && !flickHeld) {
11990
+ if (mode === "wipe") {
11379
11991
  const wr = wipeRect(layoutNow().L.overlay);
11380
11992
  const inside = p.x >= wr.x - SLOP && p.x <= wr.x + wr.w + SLOP && p.y >= wr.y && p.y <= wr.y + wr.h;
11381
- const grab = wipeGrab(p.x, wr, wipe, SLOP);
11993
+ const grab = wipeGrab(p.x, wr, wipeLine.x, SLOP);
11382
11994
  if (inside && (grab.onLine || !zoomed())) {
11383
11995
  drag = { kind: "wipe", offset: grab.offset };
11384
11996
  root.dataset.dragging = "";
11997
+ gripY = p.y;
11385
11998
  e.preventDefault();
11386
11999
  if (!grab.onLine)
11387
- setWipe(wipeAt(p.x, wr));
12000
+ setWipe(wipeAt(p.x, wr), true, true);
12001
+ else
12002
+ drawNow();
11388
12003
  return;
11389
12004
  }
11390
12005
  }
@@ -11410,6 +12025,11 @@ function compare(container, options) {
11410
12025
  pinch = { d, x: mid.x, y: mid.y };
11411
12026
  return handZoom(zoomAbout(panned, k, q.pu, q.pv));
11412
12027
  }
12028
+ if (mode === "wipe" && drag?.kind !== "pan") {
12029
+ gripY = p.y;
12030
+ if (!drag)
12031
+ placeGrip(wipeRect(layoutNow().L.overlay));
12032
+ }
11413
12033
  if (drag?.kind === "wipe")
11414
12034
  setWipe(wipeAt(p.x, wipeRect(layoutNow().L.overlay), drag.offset));
11415
12035
  else if (drag?.kind === "pan") {
@@ -11482,12 +12102,6 @@ function compare(container, options) {
11482
12102
  const z = { u: zu.target, v: zv.target, w: zw.target, h: zw.target };
11483
12103
  glideZoom(zoomAbout(z, factor, 0.5, 0.5));
11484
12104
  };
11485
- const holdFlick = (on) => {
11486
- if (flickHeld === on)
11487
- return;
11488
- flickHeld = on;
11489
- drawNow();
11490
- };
11491
12105
  const holdLoupe = (on) => {
11492
12106
  if (loupeHeld === on)
11493
12107
  return;
@@ -11533,25 +12147,17 @@ function compare(container, options) {
11533
12147
  case "s":
11534
12148
  case "w":
11535
12149
  case "d":
11536
- if (e.altKey || e.shiftKey || key === "s" && claimsHintKey(doc, key))
11537
- return;
11538
- claim();
11539
- return setMode(KEY_MODE[key]);
11540
12150
  case "1":
11541
12151
  case "2":
11542
- case "3":
11543
- case "4":
11544
- if (e.altKey || e.shiftKey)
12152
+ case "3": {
12153
+ if (e.altKey || e.shiftKey || key === "s" && claimsHintKey(doc, key))
11545
12154
  return;
11546
- claim();
11547
- return setMode(MODES2[Number(key) - 1].mode);
11548
- case "x":
11549
- if (e.altKey)
12155
+ const m = modeForKey(key, modeList);
12156
+ if (!m)
11550
12157
  return;
11551
12158
  claim();
11552
- if (!e.repeat)
11553
- holdFlick(true);
11554
- return;
12159
+ return setMode(m);
12160
+ }
11555
12161
  case "z":
11556
12162
  if (e.altKey)
11557
12163
  return;
@@ -11592,7 +12198,7 @@ function compare(container, options) {
11592
12198
  old.preview.replaceWith(s.preview);
11593
12199
  sides[index] = s;
11594
12200
  note.textContent = "";
11595
- labels[index].textContent = labelOf(index);
12201
+ setLabel(index, labelOf(index));
11596
12202
  if (near)
11597
12203
  loadSide(s);
11598
12204
  readColors();
@@ -11603,12 +12209,15 @@ function compare(container, options) {
11603
12209
  stage.insertBefore(sides[1].preview, labels[0]);
11604
12210
  stage.insertBefore(sides[0].el, labels[0]);
11605
12211
  stage.insertBefore(sides[0].preview, labels[0]);
11606
- labels[0].textContent = labelOf(0);
11607
- labels[1].textContent = labelOf(1);
12212
+ setLabel(0, labelOf(0));
12213
+ setLabel(1, labelOf(1));
11608
12214
  loupeBtn.setAttribute("aria-pressed", String(loupeOn));
11609
12215
  loopBtn.setAttribute("aria-pressed", String(loopNow() !== null));
12216
+ const modePill = modeList.length > 1 ? selectionPill(modes, {
12217
+ rows: () => modeBtns,
12218
+ selected: () => modeBtns[modeList.indexOf(mode)] ?? null
12219
+ }) : null;
11610
12220
  syncModeUi();
11611
- syncFlicker();
11612
12221
  const unlisten = tl?.listen({
11613
12222
  onSeek: (t, source) => {
11614
12223
  if (source === "play") {
@@ -11666,24 +12275,20 @@ function compare(container, options) {
11666
12275
  });
11667
12276
  root.addEventListener("keydown", onKey);
11668
12277
  root.addEventListener("keyup", (e) => {
11669
- const key = e.key.toLowerCase();
11670
- if (key === "x")
11671
- holdFlick(false);
11672
- else if (key === "z")
12278
+ if (e.key.toLowerCase() === "z")
11673
12279
  holdLoupe(false);
11674
12280
  });
11675
12281
  root.addEventListener("focusout", (e) => {
11676
12282
  if (root.contains(e.relatedTarget))
11677
12283
  return;
11678
- holdFlick(false);
11679
12284
  holdLoupe(false);
11680
12285
  });
11681
12286
  const unregisterHints = options.hints === false ? undefined : hints.register(root, () => [
11682
- ...MODES2.map((m, i) => ({
12287
+ ...modeList.length > 1 ? modeList.map((m, i) => ({
11683
12288
  el: modeBtns[i],
11684
- label: m.label,
11685
- activate: () => setMode(m.mode)
11686
- })),
12289
+ label: MODE_LABEL[m].long,
12290
+ activate: () => setMode(m)
12291
+ })) : [],
11687
12292
  { el: loopBtn, label: "Loop", activate: () => api.setLoop(!loopNow()) },
11688
12293
  ...loupeBtn.hidden ? [] : [{ el: loupeBtn, label: "Loupe", activate: () => setLoupe(!loupeOn) }],
11689
12294
  ...zoomed() ? [{ el: zoomReset, label: "Reset zoom", activate: () => api.setZoom(null) }] : [],
@@ -11708,6 +12313,7 @@ function compare(container, options) {
11708
12313
  get mode() {
11709
12314
  return mode;
11710
12315
  },
12316
+ modes: modeList,
11711
12317
  get wipe() {
11712
12318
  return wipe;
11713
12319
  },
@@ -11744,11 +12350,11 @@ function compare(container, options) {
11744
12350
  if (before)
11745
12351
  replace(0, before);
11746
12352
  else if (next.before)
11747
- labels[0].textContent = next.before.label ?? "Before";
12353
+ setLabel(0, next.before.label ?? "Before");
11748
12354
  if (after)
11749
12355
  replace(1, after);
11750
12356
  else if (next.after)
11751
- labels[1].textContent = next.after.label ?? "After";
12357
+ setLabel(1, next.after.label ?? "After");
11752
12358
  if (before || after)
11753
12359
  strip?.update({ ...before ? { before } : {}, ...after ? { after } : {} });
11754
12360
  if (next.before || next.after)
@@ -11763,7 +12369,7 @@ function compare(container, options) {
11763
12369
  tl?.update({ fps: next.fps });
11764
12370
  requestDraw();
11765
12371
  },
11766
- seek: (t) => tl ? tl.seek(t) : ownSeek(t),
12372
+ seek: (t, o) => tl ? tl.seek(t, o?.glide ? { glide: true } : undefined) : ownSeek(t),
11767
12373
  seekBy(d) {
11768
12374
  const total = duration || tl?.duration || 0;
11769
12375
  const t = Math.min(total, Math.max(0, now() + d));
@@ -11804,7 +12410,7 @@ function compare(container, options) {
11804
12410
  dispose() {
11805
12411
  disposed = true;
11806
12412
  loop.stop();
11807
- win.clearInterval(flickerTimer);
12413
+ modePill?.dispose();
11808
12414
  win.clearTimeout(releaseTimer);
11809
12415
  ro.disconnect();
11810
12416
  nearIo?.disconnect();
@@ -11834,7 +12440,7 @@ function compare(container, options) {
11834
12440
  wake();
11835
12441
  return api;
11836
12442
  }
11837
- var LAYOUT2, FADE6, FADE_REDUCED, ZOOM, FLICKER_MS = 400, SLOP = 12, INSET = 8, LOUPE, FAST_SEEK_FRAMES = 15, RELEASE_MS = 3000, MODES2, KEY_MODE;
12443
+ var LAYOUT2, FADE6, FADE_REDUCED, ZOOM, WIPE_CATCH, SLOP = 12, INSET = 8, GRIP, LOUPE, FAST_SEEK_FRAMES = 15, RELEASE_MS = 3000, MODE_LABEL;
11838
12444
  var init_compare = __esm(() => {
11839
12445
  init_motion();
11840
12446
  init_page_hints();
@@ -11842,21 +12448,24 @@ var init_compare = __esm(() => {
11842
12448
  init_compare_gpu();
11843
12449
  init_compare_model();
11844
12450
  init_compare_strip();
12451
+ init_icons();
11845
12452
  init_look();
11846
12453
  init_rgba();
12454
+ init_selection_pill();
11847
12455
  init_timeline();
12456
+ init_timeline_model();
11848
12457
  LAYOUT2 = springMotion({ duration: 0.35, bounce: 0 });
11849
12458
  FADE6 = springMotion(SPRINGS.snappy);
11850
12459
  FADE_REDUCED = springMotion(FADE);
11851
12460
  ZOOM = springMotion({ duration: 0.35, bounce: 0 });
12461
+ WIPE_CATCH = springMotion(CATCH_UP);
12462
+ GRIP = { w: 16, h: 28 };
11852
12463
  LOUPE = { r: 88, zoom: 4 };
11853
- MODES2 = [
11854
- { mode: "side", label: "Side by side", key: "S" },
11855
- { mode: "wipe", label: "Wipe", key: "W" },
11856
- { mode: "difference", label: "Difference", key: "D" },
11857
- { mode: "flicker", label: "Flicker", key: "" }
11858
- ];
11859
- KEY_MODE = { s: "side", w: "wipe", d: "difference" };
12464
+ MODE_LABEL = {
12465
+ side: { short: "Side", long: "Side by side" },
12466
+ wipe: { short: "Wipe", long: "Wipe" },
12467
+ difference: { short: "Difference", long: "Difference" }
12468
+ };
11860
12469
  });
11861
12470
 
11862
12471
  // src/ui/solid.ts
@@ -14757,6 +15366,7 @@ init_diff();
14757
15366
  // src/ui/picker.ts
14758
15367
  init_motion();
14759
15368
  init_look();
15369
+ init_icons();
14760
15370
 
14761
15371
  // src/ui/picker-model.ts
14762
15372
  function digitPick(ids, key) {
@@ -14811,7 +15421,7 @@ var CLOSE3 = springMotion(CLOSE);
14811
15421
  var CARD = springMotion(SPRINGS.default);
14812
15422
  var FADE5 = springMotion(FADE);
14813
15423
  var DRIFT = 6;
14814
- var CHECK = svg('<path d="m3.5 8.4 3 3 6-6.6"/>');
15424
+ var CHECK = icon("check", { small: true });
14815
15425
  function optionPicker(container, options) {
14816
15426
  const doc = container.ownerDocument;
14817
15427
  const win = doc.defaultView;
@@ -16288,7 +16898,9 @@ function confirm(options) {
16288
16898
  }
16289
16899
 
16290
16900
  // src/ui/walkthrough.ts
16901
+ init_icons();
16291
16902
  init_look();
16903
+ init_selection_pill();
16292
16904
 
16293
16905
  // src/ui/toast.ts
16294
16906
  init_look();
@@ -16354,399 +16966,11 @@ function toast(message, options = {}) {
16354
16966
  };
16355
16967
  }
16356
16968
 
16357
- // src/ui/walkthrough-media.ts
16358
- init_compare_model();
16359
- init_glide();
16360
- init_look();
16361
-
16362
- // src/ui/walkthrough-css.ts
16969
+ // src/ui/walkthrough-blocks.ts
16363
16970
  init_motion();
16364
- var snappy3 = springCss(SPRINGS.snappy);
16365
- var calm = springCss(SPRINGS.calm);
16366
- var walkthroughCss = `
16367
- :where(.knobs-walkthrough,.knobs-walkthrough *,.knobs-wt-slot,.knobs-wt-slot *){box-sizing:border-box}
16368
- .knobs-ui.knobs-walkthrough{--kw-snappy:${snappy3.easing};--kw-snappy-ms:${snappy3.duration}ms;--kw-calm:${calm.easing};--kw-calm-ms:${calm.duration}ms;
16369
- --kw-text:var(--k-walkthrough-text,14px);--kw-measure:var(--k-walkthrough-measure,68ch);--kw-good:var(--k-walkthrough-good,var(--_k-ok));--kw-info:var(--k-walkthrough-info,var(--_k-info));
16370
- --kw-card:var(--_ku-pane-bg);--kw-card-ring:var(--_ku-pane-ring);--kw-pad:16px;--kw-ink:var(--k-walkthrough-ink,#ff375f);
16371
- container:knobs-wt / inline-size;display:block;width:100%;color:var(--_k-fg);font:var(--_k-weight) var(--kw-text)/1.5 var(--_k-font);text-align:left;
16372
- letter-spacing:normal;-webkit-font-smoothing:antialiased}
16373
- .knobs-walkthrough [hidden]{display:none!important}
16374
- .knobs-walkthrough :is(h1,h2,h3,p){margin:0;font:inherit;letter-spacing:inherit}
16375
- .knobs-walkthrough a{color:inherit;text-decoration:underline;text-decoration-color:color-mix(in srgb,currentColor 35%,transparent);text-underline-offset:2px;transition:text-decoration-color .15s}
16376
- .knobs-walkthrough a:hover{text-decoration-color:currentColor}
16377
- .knobs-walkthrough code,.knobs-wt-slot code{font:calc(1em - 1px) var(--_k-mono);padding:1px 5px;border-radius:5px;background:var(--_k-fill)}
16378
- :where(.knobs-ui) :is(.knobs-wt-btn,.knobs-wt-pick,.knobs-wt-toc-step,button.knobs-wt-stitle,.knobs-wt-chapter){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;
16379
- background:transparent;border:0;border-radius:0;margin:0;padding:0;min-width:0;box-shadow:none;outline:none}
16380
-
16381
- /* ——— layout: the rail beside the column; one column where it's narrow ——— */
16382
- .knobs-wt-layout{display:grid;grid-template-columns:200px minmax(0,1fr);gap:40px;align-items:start}
16383
- .knobs-wt-main{display:flex;flex-direction:column;gap:56px;min-width:0;max-width:1120px;padding-bottom:40vh}
16384
- @container knobs-wt (max-width:880px){.knobs-wt-layout{grid-template-columns:minmax(0,1fr);gap:0}.knobs-wt-main{gap:44px}}
16385
-
16386
- /* ——— the page's head ——— */
16387
- .knobs-wt-head{display:flex;flex-direction:column;gap:10px}
16388
- .knobs-wt-title{font-size:calc(var(--kw-text) + 10px)!important;font-weight:var(--_k-weight-title)!important;line-height:1.2;letter-spacing:-.01em!important}
16389
- .knobs-wt-summary{max-width:var(--kw-measure);color:var(--_k-fg-2)}
16390
- .knobs-wt-meta{display:flex;flex-wrap:wrap;gap:6px 16px;color:var(--_k-fg-3);font-size:calc(var(--kw-text) - 2px)}
16391
- .knobs-wt-meta>span{display:inline-flex;gap:6px;white-space:nowrap}
16392
- .knobs-wt-meta b{font-weight:var(--_k-weight);color:var(--_k-fg-2);font-family:var(--_k-mono);font-size:calc(1em - .5px)}
16393
-
16394
- /* ——— a section: a PR or topic ——— */
16395
- .knobs-wt-section{display:flex;flex-direction:column;gap:14px;min-width:0}
16396
- .knobs-wt-shead{display:flex;flex-direction:column;gap:6px;scroll-margin-top:16px}
16397
- .knobs-wt-kicker{display:flex;align-items:center;gap:10px;font-size:calc(var(--kw-text) - 2px);color:var(--_k-fg-3)}
16398
- .knobs-wt-kicker a{font-family:var(--_k-mono);color:var(--_k-fg-2);text-decoration:none}
16399
- .knobs-wt-kicker a:hover{color:var(--_k-fg)}
16400
- .knobs-wt-stitle{font-size:calc(var(--kw-text) + 4px)!important;font-weight:var(--_k-weight-title)!important;line-height:1.3}
16401
- .knobs-wt-ssum{max-width:var(--kw-measure);color:var(--_k-fg-2)}
16402
- .knobs-wt-chip{display:inline-flex;align-items:center;gap:6px;height:20px;padding:0 8px;border-radius:999px;font-size:calc(var(--kw-text) - 3px);font-weight:var(--_k-weight-label);white-space:nowrap;
16403
- --kw-tone:var(--_k-fg-2);color:var(--kw-tone);background:color-mix(in srgb,var(--kw-tone) 14%,transparent)}
16404
- .knobs-wt-chip::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor}
16405
- :is(.knobs-wt-chip,.knobs-wt-toc-glyph)[data-tone="good"]{--kw-tone:var(--kw-good)}
16406
- :is(.knobs-wt-chip,.knobs-wt-toc-glyph)[data-tone="warn"]{--kw-tone:var(--_k-warn)}
16407
- :is(.knobs-wt-chip,.knobs-wt-toc-glyph)[data-tone="bad"]{--kw-tone:var(--_k-danger)}
16408
- :is(.knobs-wt-chip,.knobs-wt-toc-glyph)[data-tone="info"]{--kw-tone:var(--kw-info)}
16409
- :is(.knobs-wt-chip,.knobs-wt-toc-glyph)[data-tone="dim"]{--kw-tone:var(--_k-fg-2)}
16410
-
16411
- /* ——— a step: one card ——— */
16412
- .knobs-wt-step{position:relative;display:flex;flex-direction:column;gap:12px;padding:var(--kw-pad);border-radius:var(--_k-radius-panel);background:var(--kw-card);box-shadow:var(--kw-card-ring);
16413
- scroll-margin-top:16px;outline:none;container:knobs-wt-step / inline-size}
16414
- .knobs-walkthrough[data-frame="flat"] .knobs-wt-step{box-shadow:inset 0 0 0 1px var(--_k-border)}
16415
- .knobs-wt-step-head{display:flex;align-items:baseline;gap:10px;min-width:0}
16416
- .knobs-wt-num{flex:none;font:var(--_k-weight) calc(var(--kw-text) - 2px)/1 var(--_k-mono);color:var(--_k-fg-3);font-variant-numeric:tabular-nums}
16417
- .knobs-wt-stepTitle{flex:1;min-width:0;font-weight:var(--_k-weight-title)!important}
16418
- .knobs-wt-caption{max-width:var(--kw-measure);color:var(--_k-fg-2);margin-top:-6px!important}
16419
- .knobs-wt-question{font-weight:var(--_k-weight-label)!important}
16420
- /* The step Kit is on: its number lights, and its choices show their keys. */
16421
- .knobs-wt-step[data-current] .knobs-wt-num{color:var(--_k-fg)}
16422
- .knobs-wt-media{display:grid;grid-template-columns:repeat(var(--kw-n,1),minmax(0,1fr));gap:12px;min-width:0}
16423
- @container knobs-wt-step (max-width:640px){.knobs-wt-media{grid-template-columns:minmax(0,1fr)}}
16424
-
16425
- /* ——— the feed: the agent's updates and answers, newest first, compact ——— */
16426
- .knobs-wt-feed{margin-top:-24px}
16427
- .knobs-wt-feed-list{list-style:none;margin:0;padding:0;max-height:260px;overflow:auto;scrollbar-width:none;display:flex;flex-direction:column}
16428
- .knobs-wt-feed-list::-webkit-scrollbar{display:none}
16429
- .knobs-wt-fi{display:grid;grid-template-columns:64px 56px minmax(0,1fr) auto;gap:10px;align-items:baseline;padding:6px 0;font-size:calc(var(--kw-text) - 1px)}
16430
- .knobs-wt-fi+.knobs-wt-fi{box-shadow:inset 0 1px 0 var(--_k-border)}
16431
- .knobs-wt-fi-when{color:var(--_k-fg-3);font-size:calc(var(--kw-text) - 2.5px);font-variant-numeric:tabular-nums;white-space:nowrap}
16432
- .knobs-wt-fi-kind{color:var(--_k-fg-2);font-size:calc(var(--kw-text) - 2.5px);font-weight:var(--_k-weight-label)}
16433
- .knobs-wt-fi[data-kind="qa"] .knobs-wt-fi-kind{color:var(--kw-info)}
16434
- .knobs-wt-fi-body{max-width:var(--kw-measure);color:var(--_k-fg)}
16435
- .knobs-wt-fi-q{color:var(--_k-fg-2)}
16436
- .knobs-wt-fi-q::before{content:"You asked: ";color:var(--_k-fg-3)}
16437
- :where(.knobs-ui) :is(.knobs-wt-fi-step,.knobs-wt-steplink){all:revert;box-sizing:border-box;font:var(--_k-weight) calc(var(--k-walkthrough-text,14px) - 2.5px)/1 var(--_k-mono);color:var(--_k-fg-2);background:transparent;border:0;
16438
- padding:3px 6px;margin:0;border-radius:6px;cursor:pointer;outline:none;transition:background .15s,color .15s}
16439
- .knobs-ui :is(.knobs-wt-fi-step,.knobs-wt-steplink):is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
16440
-
16441
- /* ——— a step's sent notes ——— */
16442
- .knobs-wt-notes{display:flex;flex-direction:column}
16443
- .knobs-wt-notes:empty{display:none}
16444
- .knobs-wt-n{display:flex;align-items:flex-start;gap:10px;padding:6px 0;overflow:hidden;
16445
- transition:height var(--kw-snappy-ms) var(--kw-snappy),opacity var(--kw-snappy-ms) var(--kw-snappy),filter var(--kw-snappy-ms) var(--kw-snappy)}
16446
- .knobs-wt-n[data-entering]{opacity:0;filter:blur(3px)}
16447
- .knobs-wt-n[data-leaving]{opacity:0;filter:blur(3px);padding-block:0}
16448
- .knobs-wt-n+.knobs-wt-n{box-shadow:inset 0 1px 0 var(--_k-border)}
16449
- .knobs-wt-n-meta{flex:none;min-width:44px;padding-top:2px;color:var(--_k-fg-3);font:var(--_k-weight) calc(var(--kw-text) - 2.5px)/1.4 var(--_k-mono);font-variant-numeric:tabular-nums}
16450
- .knobs-wt-n-meta[data-seek]{cursor:pointer}
16451
- .knobs-wt-n-meta[data-seek]:hover{color:var(--_k-fg)}
16452
- .knobs-wt-n-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:6px;font-size:calc(var(--kw-text) - 1px)}
16453
- .knobs-wt-n-voice{height:30px;max-width:320px}
16454
- .knobs-wt-n-ink{display:block;width:120px;max-height:80px;border-radius:6px;background:var(--_k-fill)}
16455
- .knobs-wt-n-ink path{fill:none;stroke:var(--kw-ink);stroke-width:2px;vector-effect:non-scaling-stroke;stroke-linecap:round;stroke-linejoin:round}
16456
-
16457
- /* ——— the note bar, docked at the bottom of the column ——— */
16458
- .knobs-wt-dock{position:sticky;bottom:12px;z-index:40;margin-top:-24px}
16459
- .knobs-wt-notebar{display:flex;flex-direction:column;gap:2px;padding:6px 6px 6px 14px;border-radius:calc(var(--_k-radius-panel) + 2px);background:var(--_k-bg);
16460
- box-shadow:var(--_ku-pane-ring),var(--_k-shadow-overlay)}
16461
- .knobs-wt-on{padding-top:3px;color:var(--_k-fg-3);font-size:calc(var(--kw-text) - 2.5px);white-space:nowrap;overflow:hidden;
16462
- -webkit-mask-image:linear-gradient(to right,#000 calc(100% - 1.5em),transparent);mask-image:linear-gradient(to right,#000 calc(100% - 1.5em),transparent)}
16463
- .knobs-wt-row{display:flex;align-items:flex-end;gap:4px}
16464
- :where(.knobs-ui) :is(.knobs-wt-input,.knobs-wt-tool,.knobs-wt-x,.knobs-wt-back){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;background:transparent;border:0;
16465
- border-radius:0;margin:0;padding:0;min-width:0;box-shadow:none;outline:none}
16466
- .knobs-ui .knobs-wt-input{flex:1;min-width:0;height:32px;max-height:140px;padding:6px 0;resize:none;color:var(--_k-fg);caret-color:var(--_k-accent);line-height:20px;font-size:var(--kw-text)}
16467
- .knobs-ui .knobs-wt-input::placeholder{color:var(--_k-fg-3)}
16468
- .knobs-wt-attach{display:flex;gap:4px;align-items:center;align-self:center}
16469
- .knobs-wt-attach:empty{display:none}
16470
- .knobs-wt-chip-a{display:inline-flex;align-items:center;gap:2px;height:24px;padding:0 2px 0 9px;border-radius:999px;background:var(--_k-fill-selected);font-size:calc(var(--kw-text) - 2.5px);white-space:nowrap}
16471
- .knobs-ui .knobs-wt-x{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:50%;color:var(--_k-fg-3);cursor:pointer;transition:background .15s,color .15s}
16472
- .knobs-ui .knobs-wt-x:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
16473
- .knobs-wt-x svg{width:10px;height:10px}
16474
- .knobs-ui .knobs-wt-tool{flex:none;display:inline-flex;align-items:center;justify-content:center;gap:5px;min-width:32px;height:32px;padding:0 8px;border-radius:calc(var(--_k-radius) + 2px);
16475
- color:var(--_k-fg-2);cursor:pointer;transition:background .15s,color .15s}
16476
- .knobs-ui .knobs-wt-tool:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
16477
- .knobs-ui .knobs-wt-tool[aria-pressed="true"]{background:var(--_k-fill-selected);color:var(--_k-fg)}
16478
- .knobs-wt-tool svg{width:15px;height:15px}
16479
- .knobs-wt-rec{font-size:calc(var(--kw-text) - 2.5px);font-variant-numeric:tabular-nums}
16480
- .knobs-wt-rec:empty{display:none}
16481
- .knobs-ui .knobs-wt-tool[data-send]{color:var(--_k-fg-3)}
16482
- .knobs-ui .knobs-wt-tool[data-send][data-ready]{background:var(--_k-accent);color:var(--_k-bg)}
16483
- /* Back: in place over the bar for a few seconds after a note moves the page on. */
16484
- .knobs-ui .knobs-wt-back{position:absolute;left:8px;bottom:calc(100% + 8px);height:28px;padding:0 12px;border-radius:999px;background:var(--_k-bg);color:var(--_k-fg);
16485
- box-shadow:var(--_ku-pane-ring),var(--_k-shadow-overlay);font-size:calc(var(--kw-text) - 1.5px);font-weight:var(--_k-weight-label);cursor:pointer;
16486
- opacity:0;scale:.96;filter:blur(3px);pointer-events:none;transition:opacity .11s cubic-bezier(.4,0,1,1),scale .11s cubic-bezier(.4,0,1,1),filter .11s cubic-bezier(.4,0,1,1)}
16487
- .knobs-ui .knobs-wt-back[data-on]{opacity:1;scale:1;filter:none;pointer-events:auto;transition:opacity .15s cubic-bezier(.2,.8,.2,1),scale .15s cubic-bezier(.2,.8,.2,1),filter .15s cubic-bezier(.2,.8,.2,1)}
16488
- .knobs-ui .knobs-wt-back:hover{background:var(--_k-fill-selected)}
16489
-
16490
- /* ——— ink over a step's media ——— */
16491
- .knobs-wt-media,.knobs-wt-options{position:relative}
16492
- .knobs-wt-ink{position:absolute;inset:0;z-index:5;width:100%;height:100%;overflow:visible;pointer-events:none;touch-action:none}
16493
- .knobs-wt-ink[data-on]{pointer-events:auto;cursor:crosshair}
16494
- .knobs-wt-ink path{fill:none;stroke:var(--kw-ink);stroke-width:3px;vector-effect:non-scaling-stroke;stroke-linecap:round;stroke-linejoin:round}
16495
-
16496
- /* ——— full-window: the step over the page, its media filling it; its place held in the page ——— */
16497
- .knobs-wt-scrim{position:fixed;inset:0;z-index:30;background:color-mix(in srgb,var(--_k-bg-page) 82%,transparent);opacity:0;pointer-events:none}
16498
- .knobs-wt-scrim[data-on]{pointer-events:auto}
16499
- .knobs-wt-hold-card{flex:none}
16500
- .knobs-wt-step[data-full]{position:fixed;z-index:35;margin:0;overflow:hidden;overscroll-behavior:contain;padding-bottom:84px;box-shadow:var(--kw-card-ring),var(--_k-shadow-overlay);background:var(--_k-bg)}
16501
- .knobs-wt-step[data-full]>:not(.knobs-wt-media){flex:none}
16502
- .knobs-wt-step[data-full] .knobs-wt-notes{display:none}
16503
- .knobs-wt-step[data-full]>.knobs-wt-media{flex:1;min-height:0;grid-auto-rows:minmax(0,1fr)}
16504
- .knobs-wt-step[data-full] .knobs-wt-slot{display:flex;flex-direction:column;min-height:0}
16505
- .knobs-wt-step[data-full] .knobs-wt-body{flex:1;min-height:0}
16506
- .knobs-wt-step[data-full] .knobs-wt-fit{height:100%;transition:none}
16507
- .knobs-wt-step[data-full] .knobs-wt-frame{height:100%;aspect-ratio:auto}
16508
-
16509
- /* ——— a taste test: the options side by side, Kit's pick lit and the rest stepping back ——— */
16510
- .knobs-wt-options{display:grid;grid-template-columns:repeat(var(--kw-n,2),minmax(0,1fr));gap:10px}
16511
- @container knobs-wt-step (max-width:640px){.knobs-wt-options{grid-template-columns:minmax(0,1fr)}}
16512
- .knobs-wt-option{position:relative;display:flex;flex-direction:column;gap:8px;min-width:0;padding:8px;border-radius:calc(var(--_k-radius-panel) - 4px);background:var(--_ku-wash);
16513
- transition:background var(--kw-snappy-ms) var(--kw-snappy)}
16514
- .knobs-wt-option[data-picked]{background:color-mix(in srgb,var(--_k-accent) 12%,var(--_ku-wash))}
16515
- .knobs-wt-option>:not(.knobs-wt-pick){transition:opacity var(--kw-snappy-ms) var(--kw-snappy)}
16516
- .knobs-wt-options[data-has-pick] .knobs-wt-option:not([data-picked])>:not(.knobs-wt-pick){opacity:.55}
16517
- .knobs-ui .knobs-wt-pick{display:flex;align-items:center;gap:8px;width:100%;min-height:28px;padding:0 4px;border-radius:calc(var(--_k-radius) - 2px);cursor:pointer;color:var(--_k-fg-2);text-align:left}
16518
- .knobs-ui .knobs-wt-pick:is(:hover,:focus-visible){color:var(--_k-fg)}
16519
- .knobs-wt-letter{flex:none;font:var(--_k-weight-title) calc(var(--kw-text) - 1px)/1 var(--_k-font);color:var(--_k-fg)}
16520
- .knobs-wt-olabel{flex:1;min-width:0;font-weight:var(--_k-weight-label);color:var(--_k-fg)}
16521
- .knobs-wt-suggested{flex:none;font-size:calc(var(--kw-text) - 3px);color:var(--_k-fg-3)}
16522
- .knobs-wt-check{flex:none;display:flex;width:18px;height:18px;align-items:center;justify-content:center;border-radius:50%;background:var(--_k-fill);color:transparent;
16523
- transition:background var(--kw-snappy-ms) var(--kw-snappy),color var(--kw-snappy-ms) var(--kw-snappy)}
16524
- .knobs-wt-check svg{width:12px;height:12px}
16525
- .knobs-wt-option[data-picked] .knobs-wt-check{background:var(--_k-accent);color:var(--_k-bg)}
16526
- .knobs-wt-key{display:inline-flex;align-items:center;justify-content:center;min-width:15px;height:15px;padding:0 3px;border-radius:4px;background:var(--_k-fill);color:var(--_k-fg-3);
16527
- font:var(--_k-weight-label) calc(var(--kw-text) - 4px)/1 var(--_k-font);font-variant-numeric:tabular-nums;opacity:.5;transition:opacity .15s}
16528
- .knobs-wt-step[data-current] .knobs-wt-key{opacity:1}
16529
- .knobs-wt-ocap{color:var(--_k-fg-2);font-size:calc(var(--kw-text) - 1px);padding:0 4px}
16530
-
16531
- /* ——— the rail: progress, contents, export ——— */
16532
- .knobs-wt-rail{position:sticky;top:16px;display:flex;flex-direction:column;gap:14px;max-height:calc(100vh - 32px);min-height:0;font-size:calc(var(--kw-text) - 1.5px)}
16533
- .knobs-wt-progress{display:flex;flex-direction:column;gap:7px}
16534
- .knobs-wt-count{display:flex;align-items:baseline;justify-content:space-between;color:var(--_k-fg-2);font-variant-numeric:tabular-nums}
16535
- .knobs-wt-count b{color:var(--_k-fg);font-weight:var(--_k-weight-label)}
16536
- .knobs-wt-bar{position:relative;height:4px;border-radius:2px;overflow:hidden;background:var(--_k-fill)}
16537
- .knobs-wt-fill{position:absolute;inset:0;border-radius:inherit;background:var(--_k-accent);transform-origin:0 50%;transform:scaleX(var(--kw-done,0))}
16538
- .knobs-walkthrough[data-settled] .knobs-wt-fill{transition:transform var(--kw-calm-ms) var(--kw-calm)}
16539
- /* The contents: no box, one text spine for sections and steps (the steps' dots hang in the
16540
- gutter left of it), and one pill behind the current step that springs between rows. */
16541
- .knobs-wt-toc{position:relative;flex:1 1 auto;min-height:0;overflow:auto;scrollbar-width:none;margin:0 -8px;padding:0 0 8px}
16542
- .knobs-wt-toc::-webkit-scrollbar{display:none}
16543
- .knobs-wt-toc-section{display:block;padding:12px 8px 4px 26px;color:var(--_k-fg-3);font-size:calc(var(--kw-text) - 3px);font-weight:var(--_k-weight-label);white-space:nowrap;overflow:hidden;
16544
- -webkit-mask-image:linear-gradient(to right,#000 calc(100% - 1.5em),transparent);mask-image:linear-gradient(to right,#000 calc(100% - 1.5em),transparent)}
16545
- .knobs-wt-toc-section:first-of-type{padding-top:2px}
16546
- .knobs-wt-toc-mark{position:absolute;left:0;right:0;top:0;height:0;border-radius:calc(var(--_k-radius) - 1px);background:var(--_ku-wash-hi);pointer-events:none;opacity:0;will-change:transform}
16547
- .knobs-wt-toc-mark[data-on]{opacity:1}
16548
- .knobs-walkthrough[data-settled] .knobs-wt-toc-mark{transition:opacity .15s}
16549
- .knobs-ui .knobs-wt-toc-step{position:relative;display:flex;align-items:center;width:100%;text-align:left;height:26px;padding:0 8px 0 26px;color:var(--_k-fg-2);cursor:pointer;white-space:nowrap}
16550
- .knobs-ui .knobs-wt-toc-step:is(:hover,:focus-visible){color:var(--_k-fg)}
16551
- .knobs-wt-toc-step[aria-current="step"]{color:var(--_k-fg)}
16552
- .knobs-wt-toc-label{flex:1;min-width:0;overflow:hidden;-webkit-mask-image:linear-gradient(to right,#000 calc(100% - 1.5em),transparent);mask-image:linear-gradient(to right,#000 calc(100% - 1.5em),transparent)}
16553
- /* A step's state: an open ring, a filled dot once answered, nothing to answer a faint ring. */
16554
- .knobs-wt-toc-glyph{--kw-tone:var(--_k-fg-3);position:absolute;left:11px;top:50%;width:7px;height:7px;margin-top:-3.5px;border-radius:50%;box-shadow:inset 0 0 0 1.5px var(--kw-tone);
16555
- transition:background var(--kw-snappy-ms) var(--kw-snappy),box-shadow var(--kw-snappy-ms) var(--kw-snappy)}
16556
- .knobs-wt-toc-glyph[data-tone="accent"]{--kw-tone:var(--_k-accent)}
16557
- .knobs-wt-toc-glyph[data-done]{background:var(--kw-tone)}
16558
- .knobs-wt-toc-glyph[data-tone="none"]{box-shadow:inset 0 0 0 1px var(--_k-border)}
16559
- .knobs-wt-actions{display:flex;flex-wrap:wrap;gap:4px}
16560
- .knobs-ui .knobs-wt-btn{display:inline-flex;align-items:center;gap:6px;height:26px;padding:0 9px;border-radius:var(--_k-radius);color:var(--_k-fg-2);cursor:pointer;white-space:nowrap;
16561
- transition:background .15s,color .15s}
16562
- .knobs-ui .knobs-wt-btn:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
16563
- .knobs-ui .knobs-wt-btn[data-primary]{background:var(--_k-fill-selected);color:var(--_k-fg)}
16564
- .knobs-ui .knobs-wt-btn[data-primary]:is(:hover,:focus-visible){background:color-mix(in srgb,var(--_k-fg) 16%,transparent)}
16565
- .knobs-wt-keys{color:var(--_k-fg-3);font-size:calc(var(--kw-text) - 3px);line-height:1.6}
16566
- .knobs-wt-keys kbd{font:inherit;color:var(--_k-fg-2)}
16567
- /* Narrow: the rail becomes a bar along the top, with the progress and the export only. */
16568
- @container knobs-wt (max-width:880px){
16569
- .knobs-wt-rail{z-index:4;flex-direction:row;align-items:center;gap:12px;max-height:none;margin:0 calc(var(--kw-pad) * -1) 20px;padding:8px var(--kw-pad);top:0;background:var(--_k-bg-page)}
16570
- .knobs-wt-progress{flex:1}.knobs-wt-toc,.knobs-wt-keys{display:none}
16571
- .knobs-wt-step,.knobs-wt-shead{scroll-margin-top:60px}
16572
- }
16573
-
16574
- /* ——— media slots ——— */
16575
- .knobs-ui.knobs-wt-slot{position:relative;min-width:0;container:knobs-wt-slot / inline-size;color:var(--_k-fg);font:var(--_k-weight) var(--k-walkthrough-text,14px)/1.5 var(--_k-font);
16576
- --kw-good:var(--k-walkthrough-good,var(--_k-ok));--kw-info:var(--k-walkthrough-info,var(--_k-info))}
16577
- .knobs-wt-slot-label{display:block;margin:0 0 6px 2px;font:var(--_k-weight-label) calc(var(--k-walkthrough-text,14px) - 3px)/1.3 var(--_k-font);color:var(--_k-fg-2);letter-spacing:.04em;text-transform:uppercase}
16578
- /* A compare is exactly as tall as its pictures at this width, so there's no letterbox: the two
16579
- sides edge to edge (side) or one (overlaid), plus its mode bar, the timeline and their two gaps
16580
- (92 px), and the frame strip with its gap (158 px). */
16581
- .knobs-wt-fit{width:100%;height:calc(100cqw / 2 / var(--kw-aspect,1.7778) + var(--kw-chrome,92px))}
16582
- .knobs-wt-fit[data-overlay]{height:calc(100cqw / var(--kw-aspect,1.7778) + var(--kw-chrome,92px))}
16583
- .knobs-wt-fit[data-strip]{--kw-chrome:158px}
16584
- .knobs-wt-fit[data-settled]{transition:height var(--k-wt-calm-ms,${calm.duration}ms) ${calm.easing}}
16585
- .knobs-wt-frame{position:relative;width:100%;overflow:hidden;border-radius:calc(var(--_k-radius-panel) - 4px);background:var(--_k-fill);aspect-ratio:var(--kw-aspect,16/9)}
16586
- .knobs-wt-frame>:is(video,img){position:absolute;inset:0;display:block;width:100%;height:100%;object-fit:contain;margin:0;border:0}
16587
- .knobs-wt-frame[data-natural]{aspect-ratio:auto}
16588
- .knobs-wt-frame[data-natural]>img{position:static;height:auto}
16589
- .knobs-wt-code{margin:0;padding:12px 14px;max-height:420px;overflow:auto;border-radius:calc(var(--_k-radius-panel) - 4px);background:var(--_k-fill);color:var(--_k-fg);
16590
- font:calc(var(--k-walkthrough-text,14px) - 2px)/1.55 var(--_k-mono);white-space:pre;tab-size:2;scrollbar-width:thin}
16591
- .knobs-wt-code[data-terminal]{white-space:pre-wrap;overflow-wrap:anywhere}
16592
- .knobs-wt-prompt{color:var(--_k-fg-3);user-select:none;-webkit-user-select:none}
16593
- .knobs-wt-cmd{color:var(--_k-fg);font-weight:var(--_k-weight-label)}
16594
- .knobs-wt-out{color:var(--_k-fg-2)}
16595
- .knobs-wt-metric{display:flex;flex-direction:column;gap:8px;padding:12px 14px;border-radius:calc(var(--_k-radius-panel) - 4px);background:var(--_k-fill)}
16596
- .knobs-wt-mlabel{color:var(--_k-fg-2);font-size:calc(var(--k-walkthrough-text,14px) - 2px)}
16597
- .knobs-wt-mvalue{display:flex;align-items:baseline;flex-wrap:wrap;gap:8px;font-variant-numeric:tabular-nums;font-feature-settings:"tnum"}
16598
- .knobs-wt-mfrom{color:var(--_k-fg-3);font-size:calc(var(--k-walkthrough-text,14px) + 2px)}
16599
- .knobs-wt-mto{font-size:calc(var(--k-walkthrough-text,14px) + 12px);font-weight:var(--_k-weight-title);line-height:1}
16600
- .knobs-wt-mof{color:var(--_k-fg-3)}
16601
- .knobs-wt-mdelta{--kw-tone:var(--_k-fg-2);padding:1px 7px;border-radius:999px;font-size:calc(var(--k-walkthrough-text,14px) - 3px);font-weight:var(--_k-weight-label);color:var(--kw-tone);background:color-mix(in srgb,var(--kw-tone) 14%,transparent)}
16602
- .knobs-wt-mdelta[data-tone="good"]{--kw-tone:var(--kw-good)}
16603
- .knobs-wt-mdelta[data-tone="bad"]{--kw-tone:var(--_k-danger)}
16604
- .knobs-wt-mtrack{position:relative;height:4px;border-radius:2px;background:color-mix(in srgb,var(--_k-fg) 8%,transparent)}
16605
- .knobs-wt-mtrack>span{position:absolute;top:0;bottom:0;left:0;border-radius:inherit}
16606
- .knobs-wt-mtrack>.knobs-wt-mafter{background:var(--_k-accent)}
16607
- .knobs-wt-mtrack>.knobs-wt-mbefore{background:color-mix(in srgb,var(--_k-fg) 30%,transparent)}
16608
- .knobs-wt-callout{--kw-tone:var(--kw-info);display:flex;gap:10px;padding:10px 14px;border-radius:calc(var(--_k-radius-panel) - 4px);background:color-mix(in srgb,var(--kw-tone) 11%,transparent);color:var(--_k-fg)}
16609
- .knobs-wt-callout::before{content:"";flex:none;width:6px;height:6px;margin-top:.62em;border-radius:50%;background:var(--kw-tone)}
16610
- .knobs-wt-callout[data-tone="good"]{--kw-tone:var(--kw-good)}
16611
- .knobs-wt-callout[data-tone="warn"]{--kw-tone:var(--_k-warn)}
16612
- .knobs-wt-callout[data-tone="bad"]{--kw-tone:var(--_k-danger)}
16613
- .knobs-wt-text{max-width:var(--k-walkthrough-measure,68ch);color:var(--_k-fg-2)}
16614
- .knobs-wt-hold{border-radius:calc(var(--_k-radius-panel) - 4px);background:var(--_k-fill)}
16615
- .knobs-wt-failed{display:flex;align-items:center;justify-content:center;min-height:80px;color:var(--_k-fg-3);font-size:calc(var(--k-walkthrough-text,14px) - 2px)}
16616
-
16617
- /* ——— data blocks: history, stack, table, chapters. On the step's card, so no card of their own:
16618
- hairlines and fills only. Tones read the status tokens; hover fades are the panel's own. ——— */
16619
- :where(.knobs-wt-history,.knobs-wt-stack,.knobs-wt-table,.knobs-wt-chapters){--kw-t:var(--k-walkthrough-text,14px);--kw-line:calc(var(--kw-t) + 6px);--kw-rail:var(--_k-border-strong)}
16620
- :where(.knobs-wt-slot) [data-tone="good"]{--kw-tone:var(--kw-good)}
16621
- :where(.knobs-wt-slot) [data-tone="warn"]{--kw-tone:var(--_k-warn)}
16622
- :where(.knobs-wt-slot) [data-tone="bad"]{--kw-tone:var(--_k-danger)}
16623
- :where(.knobs-wt-slot) [data-tone="info"]{--kw-tone:var(--kw-info)}
16624
- :where(.knobs-wt-slot) [data-tone="dim"]{--kw-tone:var(--_k-fg-3)}
16625
- .knobs-wt-tags{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}
16626
- .knobs-ui .knobs-wt-tag{display:inline-flex;align-items:center;height:20px;padding:0 6px;border-radius:5px;background:var(--_k-fill);color:var(--_k-fg-2);font:var(--_k-weight) calc(var(--kw-t) - 3px)/1 var(--_k-mono);
16627
- text-decoration:none;white-space:nowrap;transition:background calc(var(--_k-hover-out) * 1ms),color calc(var(--_k-hover-out) * 1ms)}
16628
- .knobs-ui a.knobs-wt-tag:is(:hover,:focus-visible){background:var(--_k-fill-selected);color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
16629
-
16630
- /* A history: dates down the left, a spine of tone dots joined by a hairline (broken around each
16631
- dot), the event beside it. Inferred events take a ring for a dot and a dashed tag. */
16632
- .knobs-wt-history{--kw-hy:calc(5px + var(--kw-line) / 2);display:flex;flex-direction:column;gap:18px;font-size:calc(var(--kw-t) - 1px);line-height:var(--kw-line);color:var(--_k-fg)}
16633
- .knobs-wt-hgroup{display:flex;flex-direction:column;gap:6px;min-width:0}
16634
- .knobs-wt-hhead{display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 10px}
16635
- .knobs-ui .knobs-wt-hheading{margin:0;font:var(--_k-weight-label) calc(var(--kw-t) - 3px)/1.3 var(--_k-font);letter-spacing:.04em;text-transform:uppercase;color:var(--_k-fg-2)}
16636
- .knobs-wt-hsum{color:var(--_k-fg-3);font-size:calc(var(--kw-t) - 2px);font-variant-numeric:tabular-nums}
16637
- .knobs-wt-hlist{list-style:none;margin:0;padding:0;display:flex;flex-direction:column}
16638
- .knobs-wt-hev{display:grid;grid-template-columns:var(--kw-hw,11ch) 14px minmax(0,1fr);grid-template-areas:"when spine main";column-gap:10px;align-items:baseline;padding:5px 0}
16639
- .knobs-wt-hwhen{grid-area:when;display:flex;flex-direction:column;min-width:0;font:var(--_k-weight) calc(var(--kw-t) - 2px)/var(--kw-line) var(--_k-mono);font-variant-numeric:tabular-nums;color:var(--_k-fg-2)}
16640
- .knobs-wt-hage{color:var(--_k-fg-3);font-size:calc(var(--kw-t) - 3px);line-height:1.35}
16641
- .knobs-wt-hspine{grid-area:spine;position:relative;align-self:stretch;margin-block:-5px}
16642
- .knobs-wt-hspine::before,.knobs-wt-hspine::after{content:"";position:absolute;left:6.5px;width:1px;background:var(--kw-rail)}
16643
- .knobs-wt-hspine::before{top:0;height:calc(var(--kw-hy) - 6px)}
16644
- .knobs-wt-hspine::after{top:calc(var(--kw-hy) + 6px);bottom:0}
16645
- .knobs-wt-hev:first-child .knobs-wt-hspine::before,.knobs-wt-hev:last-child .knobs-wt-hspine::after{content:none}
16646
- .knobs-wt-hdot{position:absolute;left:3.5px;top:calc(var(--kw-hy) - 3.5px);width:7px;height:7px;border-radius:50%;background:var(--kw-tone,var(--_k-fg-3))}
16647
- .knobs-wt-hev[data-type="release"] .knobs-wt-hdot{border-radius:1.5px;rotate:45deg;scale:.9}
16648
- .knobs-wt-hev[data-type="audit"] .knobs-wt-hdot{border-radius:1.5px}
16649
- .knobs-wt-hev[data-provenance="inferred"] .knobs-wt-hdot{background:transparent;box-shadow:inset 0 0 0 1.5px var(--kw-tone,var(--_k-fg-3))}
16650
- .knobs-wt-hmain{grid-area:main;display:flex;flex-direction:column;gap:2px;min-width:0}
16651
- .knobs-wt-htitle{display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 8px;font-weight:var(--_k-weight-label);color:var(--_k-fg)}
16652
- .knobs-wt-prov{padding:0 6px;border-radius:5px;background:var(--_k-fill);color:var(--_k-fg-3);font:var(--_k-weight) calc(var(--kw-t) - 3.5px)/18px var(--_k-mono);white-space:nowrap}
16653
- .knobs-wt-hev[data-provenance="inferred"] .knobs-wt-prov{background:transparent;color:var(--_k-warn);outline:1px dashed color-mix(in oklch,var(--_k-warn) 50%,transparent);outline-offset:-1px}
16654
- .knobs-ui .knobs-wt-hdetail{margin:0;max-width:var(--k-walkthrough-measure,68ch);color:var(--_k-fg-2)}
16655
- @container knobs-wt-slot (max-width:520px){
16656
- .knobs-wt-hev{grid-template-columns:14px minmax(0,1fr);grid-template-areas:"spine when" "spine main"}
16657
- .knobs-wt-hwhen{flex-direction:row;flex-wrap:wrap;align-items:baseline;column-gap:8px}
16658
- }
16659
-
16660
- /* A stack: the trunk at the top, each PR hanging off the branch it merges into, rails drawn as a
16661
- git graph (solid, so where two meet they don't darken). A PR with a step goes there on a click. */
16662
- .knobs-wt-stack{--kw-spt:7px;--kw-spb:7px;--kw-sy:calc(var(--kw-spt) + var(--kw-line) / 2);display:flex;flex-direction:column;font-size:calc(var(--kw-t) - 1px);line-height:var(--kw-line);color:var(--_k-fg)}
16663
- .knobs-wt-srow{display:grid;grid-template-columns:auto minmax(0,1fr) auto;column-gap:10px;align-items:start;margin:0 -8px;padding:var(--kw-spt) 8px var(--kw-spb);
16664
- border-radius:max(0px,calc(var(--_k-radius-panel) - 8px));transition:background calc(var(--_k-hover-out) * 1ms)}
16665
- .knobs-wt-srow[data-trunk]{--kw-spb:0px;grid-template-columns:auto minmax(0,1fr)}
16666
- .knobs-wt-srow[data-go]{cursor:pointer}
16667
- .knobs-wt-srow[data-go]:hover{background:var(--_ku-wash);transition-duration:calc(var(--_k-hover-in) * 1ms)}
16668
- .knobs-wt-lanes{display:flex;align-self:stretch;margin-block:calc(var(--kw-spt) * -1) calc(var(--kw-spb) * -1)}
16669
- .knobs-wt-lane{position:relative;flex:none;width:16px}
16670
- .knobs-wt-lane:is([data-rail="pass"],[data-rail="tee"],[data-down])::before{content:"";position:absolute;left:7.5px;top:0;bottom:0;width:1px;background:var(--kw-rail)}
16671
- .knobs-wt-lane[data-down]::before{top:calc(var(--kw-sy) + 3.5px)}
16672
- .knobs-wt-lane:is([data-rail="tee"],[data-rail="end"])::after{content:"";position:absolute;left:7.5px;top:0;width:13px;height:calc(var(--kw-sy) + .5px);border:solid var(--kw-rail);border-width:0 0 1px 1px;border-bottom-left-radius:6px}
16673
- .knobs-wt-lane[data-rail="node"]::after{content:"";position:absolute;left:4.5px;top:calc(var(--kw-sy) - 3.5px);width:7px;height:7px;border-radius:50%;background:var(--_k-fg-3)}
16674
- .knobs-wt-srow[data-trunk] .knobs-wt-lane[data-rail="node"]::after{background:var(--_k-fg-2)}
16675
- .knobs-wt-trunk{font:var(--_k-weight-label) calc(var(--kw-t) - 2px)/var(--kw-line) var(--_k-mono);color:var(--_k-fg-2)}
16676
- .knobs-wt-smain{display:flex;flex-direction:column;gap:1px;min-width:0}
16677
- .knobs-wt-sline{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 8px}
16678
- .knobs-wt-sord{font:var(--_k-weight) calc(var(--kw-t) - 2px)/var(--kw-line) var(--_k-mono);font-variant-numeric:tabular-nums;color:var(--_k-fg-3);white-space:nowrap}
16679
- .knobs-ui .knobs-wt-spr{font:var(--_k-weight-label) calc(var(--kw-t) - 1.5px)/var(--kw-line) var(--_k-mono);color:var(--_k-fg-2);text-decoration:none;white-space:nowrap}
16680
- .knobs-ui a.knobs-wt-spr:is(:hover,:focus-visible){color:var(--_k-fg);text-decoration:underline}
16681
- .knobs-ui .knobs-wt-stitle{font-weight:var(--_k-weight-label);line-height:var(--kw-line);color:var(--_k-fg);text-align:left}
16682
- .knobs-ui button.knobs-wt-stitle{cursor:pointer;text-decoration:underline;text-decoration-color:transparent;text-underline-offset:3px;transition:text-decoration-color calc(var(--_k-hover-out) * 1ms)}
16683
- .knobs-ui :is(.knobs-wt-srow[data-go]:hover button.knobs-wt-stitle,button.knobs-wt-stitle:focus-visible){text-decoration-color:color-mix(in oklch,currentColor 45%,transparent);transition-duration:calc(var(--_k-hover-in) * 1ms)}
16684
- .knobs-ui .knobs-wt-ssummary{margin:0;max-width:var(--k-walkthrough-measure,68ch);color:var(--_k-fg-2)}
16685
- .knobs-wt-smeta{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 12px;font:var(--_k-weight) calc(var(--kw-t) - 2px)/var(--kw-line) var(--_k-mono);font-variant-numeric:tabular-nums;color:var(--_k-fg-3)}
16686
- .knobs-wt-delta{display:inline-flex;gap:6px}
16687
- .knobs-wt-add{color:var(--_k-diff-add)}
16688
- .knobs-wt-del{color:var(--_k-diff-del)}
16689
- .knobs-wt-sstatus{margin-top:calc((var(--kw-line) - 20px) / 2)}
16690
- .knobs-wt-stack[data-flat] .knobs-wt-srow{grid-template-columns:minmax(0,1fr) auto}
16691
- .knobs-wt-stack[data-flat] .knobs-wt-srow+.knobs-wt-srow{box-shadow:inset 0 1px 0 var(--_k-border)}
16692
- @container knobs-wt-slot (max-width:520px){
16693
- .knobs-wt-srow:not([data-trunk]){grid-template-columns:auto minmax(0,1fr)}
16694
- .knobs-wt-stack[data-flat] .knobs-wt-srow{grid-template-columns:minmax(0,1fr)}
16695
- .knobs-wt-sstatus{grid-column:-2;justify-self:start;margin-top:4px}
16696
- }
16697
-
16698
- /* A table: a real one, its head stuck while it scrolls, both ways; edges fade where there's more
16699
- (the top one under the head), and thin bars show on hover. */
16700
- @property --kw-fade-l{syntax:"<number>";inherits:false;initial-value:1}
16701
- @property --kw-fade-r{syntax:"<number>";inherits:false;initial-value:1}
16702
- @keyframes kw-fade-l{from{--kw-fade-l:1}to{--kw-fade-l:0}}
16703
- @keyframes kw-fade-r{from{--kw-fade-r:0}to{--kw-fade-r:1}}
16704
- .knobs-wt-table{max-height:520px;overflow:auto;overscroll-behavior-x:contain;outline:none;scrollbar-width:thin;scrollbar-color:transparent transparent;
16705
- transition:scrollbar-color calc(var(--_k-hover-out) * 1ms)}
16706
- .knobs-wt-table:is(:hover,:focus-visible){scrollbar-color:color-mix(in oklch,var(--_k-fg) 28%,transparent) transparent;transition-duration:calc(var(--_k-hover-in) * 1ms)}
16707
- .knobs-wt-table:focus-visible{box-shadow:inset 0 0 0 2px var(--_k-focus);border-radius:calc(var(--_k-radius-panel) - 4px)}
16708
- @supports (animation-timeline:scroll()){
16709
- .knobs-wt-table{animation:knobs-fade-a linear both,knobs-fade-b linear both,kw-fade-l linear both,kw-fade-r linear both;animation-duration:auto;
16710
- animation-timeline:scroll(self block),scroll(self block),scroll(self inline),scroll(self inline);animation-range:0 24px,calc(100% - 24px) 100%,0 8px,calc(100% - 8px) 100%;
16711
- --kw-table-mask-y:linear-gradient(to bottom,#000 var(--kw-thead,0px),rgb(0 0 0/var(--knobs-fade-a)) var(--kw-thead,0px),#000 calc(var(--kw-thead,0px) + 20px),#000 calc(100% - 20px),rgb(0 0 0/var(--knobs-fade-b)));
16712
- --kw-table-mask-x:linear-gradient(to right,rgb(0 0 0/var(--kw-fade-l)),#000 24px,#000 calc(100% - 24px),rgb(0 0 0/var(--kw-fade-r)));
16713
- -webkit-mask-image:var(--kw-table-mask-y),var(--kw-table-mask-x);mask-image:var(--kw-table-mask-y),var(--kw-table-mask-x);-webkit-mask-composite:source-in;mask-composite:intersect}
16714
- }
16715
- .knobs-wt-table table{width:100%;margin:0;border-collapse:separate;border-spacing:0;font-size:calc(var(--kw-t) - 1.5px);line-height:1.45;font-variant-numeric:tabular-nums;color:var(--_k-fg-2)}
16716
- .knobs-wt-table caption{caption-side:bottom;padding-top:8px;text-align:left;color:var(--_k-fg-3);font-size:calc(var(--kw-t) - 2px)}
16717
- .knobs-wt-table th{position:sticky;top:0;padding:0 12px 7px;text-align:left;vertical-align:bottom;font-weight:var(--_k-weight-label);font-size:calc(var(--kw-t) - 2.5px);color:var(--_k-fg-3);white-space:nowrap;
16718
- background:var(--_k-pane-bg);box-shadow:inset 0 -1px 0 var(--_k-border)}
16719
- .knobs-wt-table td{padding:8px 12px;vertical-align:baseline;box-shadow:inset 0 -1px 0 var(--_k-border)}
16720
- .knobs-wt-table tr:last-child td{box-shadow:none}
16721
- .knobs-wt-table :is(th,td):first-child{padding-left:0}
16722
- .knobs-wt-table :is(th,td):last-child{padding-right:0}
16723
- .knobs-wt-table td:first-child{color:var(--_k-fg)}
16724
- .knobs-wt-table [data-align="right"]{text-align:right}
16725
- .knobs-wt-table [data-align="center"]{text-align:center}
16726
- .knobs-wt-table td[data-mono]{font-family:var(--_k-mono);font-size:calc(var(--kw-t) - 2.5px);color:var(--_k-fg);white-space:nowrap}
16727
- .knobs-wt-table td[data-long]{min-width:24ch}
16728
- .knobs-wt-table td>.knobs-wt-steplink{margin-left:4px}
16729
- .knobs-ui .knobs-wt-table a.knobs-wt-chip{text-decoration:none}
16730
- .knobs-wt-ctone{color:var(--kw-tone)}
16731
-
16732
- /* Chapters: timestamp pills under a video; one highlight springs to the one playing. */
16733
- .knobs-wt-chapters{position:relative;display:flex;flex-wrap:wrap;gap:4px;margin-top:8px}
16734
- .knobs-wt-chapter-mark{position:absolute;left:0;top:0;width:0;border-radius:calc(var(--_k-radius) + 2px);background:var(--_k-fill-selected);opacity:0;pointer-events:none;will-change:transform;
16735
- transition:opacity calc(var(--_k-motion-snappy-ms) * 1ms)}
16736
- .knobs-wt-chapter-mark[data-on]{opacity:1}
16737
- .knobs-ui .knobs-wt-chapter{position:relative;display:inline-flex;align-items:baseline;gap:6px;height:26px;padding:0 10px 0 8px;border-radius:calc(var(--_k-radius) + 2px);color:var(--_k-fg-2);
16738
- font-size:calc(var(--kw-t) - 2px);line-height:26px;white-space:nowrap;cursor:pointer;transition:background calc(var(--_k-hover-out) * 1ms),color calc(var(--_k-hover-out) * 1ms)}
16739
- .knobs-ui .knobs-wt-chapter:hover{background:var(--_ku-wash);color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
16740
- .knobs-ui .knobs-wt-chapter[aria-current]{background:transparent;color:var(--_k-fg)}
16741
- .knobs-ui .knobs-wt-chapter:focus-visible{box-shadow:inset 0 0 0 2px var(--_k-focus)}
16742
- .knobs-wt-chapter-time{font:var(--_k-weight) calc(var(--kw-t) - 3px)/1 var(--_k-mono);font-variant-numeric:tabular-nums;color:var(--_k-fg-3)}
16743
- .knobs-wt-chapter[aria-current] .knobs-wt-chapter-time{color:var(--_k-fg-2)}
16744
- @media(prefers-reduced-motion:reduce){:is(.knobs-walkthrough,.knobs-wt-slot) :where(*,::before,::after){transition-duration:.15s!important;transition-property:opacity,background,color!important}}
16745
- `;
16746
-
16747
- // src/ui/walkthrough-blocks.ts
16748
- init_motion();
16749
- init_look();
16971
+ init_icons();
16972
+ init_look();
16973
+ init_selection_pill();
16750
16974
 
16751
16975
  // src/ui/walkthrough-model.ts
16752
16976
  var DEFAULT_VERDICTS = ["ship", "change", "unsure"];
@@ -16953,7 +17177,15 @@ function fillTable(m) {
16953
17177
  out[k] = fillCell(v);
16954
17178
  return out;
16955
17179
  });
16956
- return { kind: "table", columns, rows, ...m.caption ? { caption: m.caption } : {} };
17180
+ const primary = columns.find((c) => c.key === m.primary)?.key ?? columns[0]?.key ?? "";
17181
+ return {
17182
+ kind: "table",
17183
+ columns,
17184
+ rows,
17185
+ ...m.caption ? { caption: m.caption } : {},
17186
+ stack: m.stack ?? "auto",
17187
+ primary
17188
+ };
16957
17189
  }
16958
17190
  function fillChapters(chapters) {
16959
17191
  const id = uniqueIds();
@@ -17026,6 +17258,8 @@ function normalizeWalkthrough(doc) {
17026
17258
  ...step.caption ? { caption: step.caption } : {},
17027
17259
  ...step.question ? { question: step.question } : {},
17028
17260
  media: list.map((m) => fillMedia(m, doc.base)),
17261
+ columns: Math.max(1, Math.min(3, Math.round(step.columns ?? 1))),
17262
+ ...step.more?.trim() && list.length > 1 ? { more: step.more.trim() } : {},
17029
17263
  options,
17030
17264
  ask
17031
17265
  };
@@ -17037,6 +17271,7 @@ function normalizeWalkthrough(doc) {
17037
17271
  id,
17038
17272
  title: s.title,
17039
17273
  ...link ? { link } : {},
17274
+ ...typeof s.kicker === "string" && s.kicker.trim() ? { kicker: s.kicker.trim() } : {},
17040
17275
  ...s.status ? { status: s.status } : {},
17041
17276
  ...s.summary ? { summary: s.summary } : {},
17042
17277
  steps: own
@@ -17047,14 +17282,25 @@ function normalizeWalkthrough(doc) {
17047
17282
  value
17048
17283
  }));
17049
17284
  const stepRef = (ref) => findStep(steps, ref)?.key ?? null;
17285
+ const extra = (x) => ({
17286
+ ...x.status?.trim() ? { status: x.status.trim() } : {},
17287
+ trail: (x.trail ?? []).filter((t) => typeof t === "string" && t.trim())
17288
+ });
17050
17289
  const feed = sortFeed([
17051
- ...(doc.updates ?? []).map((u) => ({ kind: "update", at: parseAt(u.at), text: u.text, step: stepRef(u.step) })),
17290
+ ...(doc.updates ?? []).map((u) => ({
17291
+ kind: "update",
17292
+ at: parseAt(u.at),
17293
+ text: u.text,
17294
+ step: stepRef(u.step),
17295
+ ...extra(u)
17296
+ })),
17052
17297
  ...(doc.qa ?? []).map((x) => ({
17053
17298
  kind: "qa",
17054
17299
  at: parseAt(x.at),
17055
17300
  text: x.a,
17056
17301
  q: x.q,
17057
- step: stepRef(x.step)
17302
+ step: stepRef(x.step),
17303
+ ...extra(x)
17058
17304
  }))
17059
17305
  ]);
17060
17306
  return {
@@ -17067,10 +17313,32 @@ function normalizeWalkthrough(doc) {
17067
17313
  id: slug(v),
17068
17314
  label: label(v)
17069
17315
  })),
17070
- sections,
17316
+ sections: withKickers(sections, doc.sections, doc.kickers),
17071
17317
  steps
17072
17318
  };
17073
17319
  }
17320
+ var isPullRequest = (href) => /\/(?:pull|merge_requests)\/\d+(?:[/?#]|$)/.test(href);
17321
+ function withKickers(sections, source, kickers = "pr") {
17322
+ if (kickers === false)
17323
+ return [...sections];
17324
+ const noun = typeof kickers === "object" ? kickers.noun.trim() : "PR";
17325
+ const counted = typeof kickers === "object" ? sections : sections.filter((s) => s.link && isPullRequest(s.link.href));
17326
+ return sections.map((s, i) => {
17327
+ const own = source[i]?.kicker;
17328
+ if (own !== undefined || s.kicker)
17329
+ return s;
17330
+ const at = counted.indexOf(s);
17331
+ return at < 0 ? s : { ...s, kicker: `${noun} ${at + 1} of ${counted.length}`.trim() };
17332
+ });
17333
+ }
17334
+ function tableStacks(stack, room, natural) {
17335
+ if (stack !== "auto")
17336
+ return stack === "always";
17337
+ return room > 0 && natural > room + 1;
17338
+ }
17339
+ function noteBarReserve(bar, inset, gap) {
17340
+ return Math.max(0, Math.ceil(bar)) + inset + gap;
17341
+ }
17074
17342
  function parseAt(at) {
17075
17343
  if (!at)
17076
17344
  return null;
@@ -17355,6 +17623,9 @@ function feedbackMarkdown(fb, verdicts) {
17355
17623
  `;
17356
17624
  }
17357
17625
  var escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
17626
+ function plainMarkdown(text) {
17627
+ return text.replace(/\[([^\]\n]+)\]\([^)\s]+\)/g, "$1").replace(/`([^`\n]+)`/g, "$1").replace(/\*\*([^*\n]+)\*\*/g, "$1").replace(/(^|[^*\w])\*([^*\n]+)\*(?!\w)/g, "$1$2").replace(/\s+/g, " ").trim();
17628
+ }
17358
17629
  function inlineMarkdown(text) {
17359
17630
  const codes = [];
17360
17631
  let s = text.replace(/`([^`\n]+)`/g, (_, c) => {
@@ -17375,6 +17646,10 @@ function inlineMarkdown(text) {
17375
17646
  }
17376
17647
 
17377
17648
  // src/ui/walkthrough-blocks.ts
17649
+ var GLYPH = {
17650
+ go: icon("caretRight", { small: true }),
17651
+ caret: icon("caretRight", { small: true })
17652
+ };
17378
17653
  var el = (doc, tag, cls) => {
17379
17654
  const n = doc.createElement(tag);
17380
17655
  if (cls)
@@ -17404,9 +17679,10 @@ function stepButton(doc, ref, steps) {
17404
17679
  const s = ref && steps?.find(ref);
17405
17680
  if (!s || !steps)
17406
17681
  return null;
17407
- const b = text(doc, "button", "knobs-wt-steplink", s.number);
17682
+ const b = el(doc, "button", "knobs-wt-steplink");
17683
+ b.innerHTML = GLYPH.go;
17408
17684
  b.type = "button";
17409
- b.title = `Go to ${s.number} ${s.title}`;
17685
+ b.title = `Go to “${s.title}”`;
17410
17686
  b.setAttribute("aria-label", b.title);
17411
17687
  b.addEventListener("click", (e) => {
17412
17688
  e.stopPropagation();
@@ -17531,7 +17807,7 @@ function mountStack(host, m, steps) {
17531
17807
  if (target && steps) {
17532
17808
  const title = text(doc, "button", "knobs-wt-stitle", it.title);
17533
17809
  title.type = "button";
17534
- title.title = `Go to ${target.number} ${target.title}`;
17810
+ title.title = `Go to “${target.title}”`;
17535
17811
  title.addEventListener("click", (e) => {
17536
17812
  e.stopPropagation();
17537
17813
  steps.go(target.key);
@@ -17586,175 +17862,775 @@ function delta(doc, d) {
17586
17862
  s.setAttribute("aria-label", `${d.add} added, ${d.del} removed`);
17587
17863
  return s;
17588
17864
  }
17589
- function mountTable(host, m, steps) {
17590
- const doc = host.ownerDocument;
17591
- const win = doc.defaultView;
17592
- const scroller = el(doc, "div", "knobs-wt-table");
17593
- const table = el(doc, "table");
17594
- if (m.caption)
17595
- table.append(text(doc, "caption", "", m.caption));
17596
- const head = el(doc, "thead");
17597
- const hr = el(doc, "tr");
17598
- for (const c of m.columns) {
17599
- const th = text(doc, "th", "", c.label);
17600
- th.scope = "col";
17601
- th.dataset.align = c.align;
17602
- hr.append(th);
17603
- }
17604
- head.append(hr);
17605
- const body = el(doc, "tbody");
17606
- for (const r of m.rows) {
17607
- const tr = el(doc, "tr");
17608
- for (const c of m.columns) {
17609
- const td = el(doc, "td");
17610
- td.dataset.align = c.align;
17611
- if (c.mono)
17612
- td.dataset.mono = "";
17613
- const cell = r[c.key];
17614
- if (cell) {
17615
- if (cell.text.length > 40 && !c.mono && !cell.code)
17616
- td.dataset.long = "";
17617
- let content;
17618
- if (cell.badge) {
17619
- content = link(doc, "knobs-wt-chip", cell.text, cell.href);
17620
- content.dataset.tone = cell.badge;
17621
- } else {
17622
- const inner = cell.code ? text(doc, "code", "", cell.text) : null;
17623
- if (cell.href) {
17624
- content = link(doc, "knobs-wt-clink", inner ? "" : cell.text, cell.href);
17625
- if (inner)
17626
- content.append(inner);
17627
- } else
17628
- content = inner ?? text(doc, "span", "", cell.text);
17629
- if (cell.tone) {
17630
- content.classList.add("knobs-wt-ctone");
17631
- content.dataset.tone = cell.tone;
17632
- }
17633
- }
17634
- td.append(content);
17635
- const go = stepButton(doc, cell.step, steps);
17636
- if (go)
17637
- td.append(go);
17638
- }
17639
- tr.append(td);
17640
- }
17641
- body.append(tr);
17642
- }
17643
- table.append(head, body);
17644
- scroller.append(table);
17645
- host.append(scroller);
17646
- const measure = () => {
17647
- scroller.style.setProperty("--kw-thead", `${head.offsetHeight}px`);
17648
- const scrolls = scroller.scrollWidth > scroller.clientWidth + 1 || scroller.scrollHeight > scroller.clientHeight + 1;
17649
- if (scrolls) {
17650
- scroller.tabIndex = 0;
17651
- scroller.setAttribute("role", "region");
17652
- scroller.setAttribute("aria-label", m.caption ?? "Table");
17653
- } else {
17654
- scroller.removeAttribute("tabindex");
17655
- scroller.removeAttribute("role");
17656
- scroller.removeAttribute("aria-label");
17657
- }
17658
- };
17659
- const ro = win && "ResizeObserver" in win ? new win.ResizeObserver(measure) : null;
17660
- ro?.observe(scroller);
17661
- measure();
17662
- return {
17663
- dispose: () => {
17664
- ro?.disconnect();
17665
- scroller.remove();
17666
- },
17667
- context: () => ({ kind: "table" })
17668
- };
17865
+ function cellContent(doc, into, cell, steps) {
17866
+ let content;
17867
+ if (cell.badge) {
17868
+ content = link(doc, "knobs-wt-chip", cell.text, cell.href);
17869
+ content.dataset.tone = cell.badge;
17870
+ } else {
17871
+ const inner = cell.code ? text(doc, "code", "", cell.text) : null;
17872
+ if (cell.href) {
17873
+ content = link(doc, "knobs-wt-clink", inner ? "" : cell.text, cell.href);
17874
+ if (inner)
17875
+ content.append(inner);
17876
+ } else
17877
+ content = inner ?? text(doc, "span", "", cell.text);
17878
+ if (cell.tone) {
17879
+ content.classList.add("knobs-wt-ctone");
17880
+ content.dataset.tone = cell.tone;
17881
+ }
17882
+ }
17883
+ into.append(content);
17884
+ const go = stepButton(doc, cell.step, steps);
17885
+ if (go)
17886
+ into.append(go);
17887
+ }
17888
+ var cellAttrs = (td, c) => {
17889
+ td.dataset.align = c.align;
17890
+ if (c.mono)
17891
+ td.dataset.mono = "";
17892
+ };
17893
+ function mountTable(host, m, steps) {
17894
+ const doc = host.ownerDocument;
17895
+ const win = doc.defaultView;
17896
+ const box = el(doc, "div", "knobs-wt-tablebox");
17897
+ const scroller = el(doc, "div", "knobs-wt-table");
17898
+ const table = el(doc, "table");
17899
+ if (m.caption)
17900
+ table.append(text(doc, "caption", "", m.caption));
17901
+ const head = el(doc, "thead");
17902
+ const hr = el(doc, "tr");
17903
+ for (const c of m.columns) {
17904
+ const th = text(doc, "th", "", c.label);
17905
+ th.scope = "col";
17906
+ th.dataset.align = c.align;
17907
+ hr.append(th);
17908
+ }
17909
+ head.append(hr);
17910
+ const body = el(doc, "tbody");
17911
+ for (const r of m.rows) {
17912
+ const tr = el(doc, "tr");
17913
+ for (const c of m.columns) {
17914
+ const td = el(doc, "td");
17915
+ cellAttrs(td, c);
17916
+ const cell = r[c.key];
17917
+ if (cell) {
17918
+ if (cell.text.length > 40 && !c.mono && !cell.code)
17919
+ td.dataset.long = "";
17920
+ cellContent(doc, td, cell, steps);
17921
+ }
17922
+ tr.append(td);
17923
+ }
17924
+ body.append(tr);
17925
+ }
17926
+ table.append(head, body);
17927
+ scroller.append(table);
17928
+ const stack = el(doc, "div", "knobs-wt-tstack");
17929
+ stack.setAttribute("role", "list");
17930
+ if (m.caption)
17931
+ stack.setAttribute("aria-label", m.caption);
17932
+ const primary = m.columns.find((c) => c.key === m.primary) ?? m.columns[0];
17933
+ for (const r of m.rows) {
17934
+ const item = el(doc, "div", "knobs-wt-trow");
17935
+ item.setAttribute("role", "listitem");
17936
+ const title = el(doc, "div", "knobs-wt-ttitle");
17937
+ if (primary) {
17938
+ cellAttrs(title, primary);
17939
+ const cell = r[primary.key];
17940
+ if (cell)
17941
+ cellContent(doc, title, cell, steps);
17942
+ }
17943
+ const line = el(doc, "div", "knobs-wt-tline");
17944
+ line.append(title);
17945
+ const badges = el(doc, "div", "knobs-wt-tbadges");
17946
+ for (const c of m.columns) {
17947
+ const cell = r[c.key];
17948
+ if (c !== primary && cell?.badge)
17949
+ cellContent(doc, badges, cell, steps);
17950
+ }
17951
+ if (badges.childNodes.length)
17952
+ line.append(badges);
17953
+ item.append(line);
17954
+ const terms = el(doc, "dl", "knobs-wt-tterms");
17955
+ for (const c of m.columns) {
17956
+ if (c === primary)
17957
+ continue;
17958
+ const cell = r[c.key];
17959
+ if (!cell || cell.text === "" || cell.badge)
17960
+ continue;
17961
+ const dd = el(doc, "dd");
17962
+ cellAttrs(dd, c);
17963
+ dd.removeAttribute("data-align");
17964
+ cellContent(doc, dd, cell, steps);
17965
+ terms.append(text(doc, "dt", "", c.label), dd);
17966
+ }
17967
+ if (terms.childNodes.length)
17968
+ item.append(terms);
17969
+ stack.append(item);
17970
+ }
17971
+ if (m.caption)
17972
+ stack.append(text(doc, "p", "knobs-wt-tcaption", m.caption));
17973
+ box.append(scroller, stack);
17974
+ host.append(box);
17975
+ let natural = 0;
17976
+ const measureNatural = () => {
17977
+ if (!box.isConnected)
17978
+ return 0;
17979
+ const was = box.dataset.stacked;
17980
+ delete box.dataset.stacked;
17981
+ box.dataset.measure = "";
17982
+ const w = table.getBoundingClientRect().width;
17983
+ delete box.dataset.measure;
17984
+ if (was !== undefined)
17985
+ box.dataset.stacked = was;
17986
+ return w;
17987
+ };
17988
+ const measure = () => {
17989
+ if (!natural)
17990
+ natural = measureNatural();
17991
+ const stacked = tableStacks(m.stack, box.clientWidth, natural);
17992
+ box.toggleAttribute("data-stacked", stacked);
17993
+ scroller.style.setProperty("--kw-thead", `${head.offsetHeight}px`);
17994
+ const scrolls = !stacked && (scroller.scrollWidth > scroller.clientWidth + 1 || scroller.scrollHeight > scroller.clientHeight + 1);
17995
+ if (scrolls) {
17996
+ scroller.tabIndex = 0;
17997
+ scroller.setAttribute("role", "region");
17998
+ scroller.setAttribute("aria-label", m.caption ?? "Table");
17999
+ } else {
18000
+ scroller.removeAttribute("tabindex");
18001
+ scroller.removeAttribute("role");
18002
+ scroller.removeAttribute("aria-label");
18003
+ }
18004
+ };
18005
+ if (m.stack === "always")
18006
+ box.dataset.stacked = "";
18007
+ const ro = win && "ResizeObserver" in win ? new win.ResizeObserver(measure) : null;
18008
+ ro?.observe(box);
18009
+ measure();
18010
+ doc.fonts?.ready.then(() => {
18011
+ if (!box.isConnected)
18012
+ return;
18013
+ natural = 0;
18014
+ measure();
18015
+ });
18016
+ return {
18017
+ dispose: () => {
18018
+ ro?.disconnect();
18019
+ box.remove();
18020
+ },
18021
+ context: () => ({ kind: "table" })
18022
+ };
18023
+ }
18024
+ function chapterPills(host, chapters, io) {
18025
+ const doc = host.ownerDocument;
18026
+ const list = el(doc, "div", "knobs-wt-chapters");
18027
+ list.setAttribute("role", "group");
18028
+ list.setAttribute("aria-label", "Chapters");
18029
+ let at = -2;
18030
+ const rows = chapters.map((c, i) => {
18031
+ const b = el(doc, "button", "knobs-wt-chapter");
18032
+ b.type = "button";
18033
+ b.append(text(doc, "span", "knobs-wt-chapter-n", String(i + 1)));
18034
+ b.append(text(doc, "span", "knobs-wt-chapter-label", c.label));
18035
+ b.append(text(doc, "span", "knobs-wt-chapter-time", clock(c.start)));
18036
+ b.addEventListener("click", () => {
18037
+ io.onEvent?.({ what: "seek", time: Math.round(c.start * 100) / 100, detail: c.label });
18038
+ io.seek(c.start);
18039
+ select(i);
18040
+ });
18041
+ list.append(b);
18042
+ return b;
18043
+ });
18044
+ host.append(list);
18045
+ const pill = selectionPill(list, {
18046
+ selected: () => rows[at] ?? null,
18047
+ rows: () => rows
18048
+ });
18049
+ const select = (i) => {
18050
+ if (i === at)
18051
+ return;
18052
+ at = i;
18053
+ rows.forEach((b, k) => k === at ? b.setAttribute("aria-current", "true") : b.removeAttribute("aria-current"));
18054
+ pill.update();
18055
+ };
18056
+ const sync = () => select(chapterAt(chapters, io.now()));
18057
+ sync();
18058
+ return {
18059
+ sync,
18060
+ get current() {
18061
+ return chapters[at] ?? null;
18062
+ },
18063
+ dispose() {
18064
+ pill.dispose();
18065
+ list.remove();
18066
+ }
18067
+ };
18068
+ }
18069
+ function disclosure(host, options) {
18070
+ const doc = host.ownerDocument;
18071
+ const win = doc.defaultView;
18072
+ let open = options.open === true;
18073
+ const root = el(doc, "div", "knobs-wt-disc");
18074
+ const toggle = el(doc, "button", "knobs-wt-disc-toggle");
18075
+ toggle.type = "button";
18076
+ toggle.innerHTML = GLYPH.caret;
18077
+ toggle.append(text(doc, "span", "", options.label));
18078
+ if (options.count !== undefined)
18079
+ toggle.append(text(doc, "span", "knobs-wt-disc-count", String(options.count)));
18080
+ const viewport = el(doc, "div", "knobs-wt-disc-body");
18081
+ const content = el(doc, "div", "knobs-wt-disc-content");
18082
+ content.append(options.content);
18083
+ viewport.append(content);
18084
+ root.append(toggle, viewport);
18085
+ host.append(root);
18086
+ const STATE = springMotion(SPRINGS.default);
18087
+ const h = new Spring(0, STATE.omega, STATE.zeta, 0.25);
18088
+ let full = 0;
18089
+ const paint = (moving) => {
18090
+ const k = full > 0 ? Math.max(0, Math.min(1, h.x / full)) : open ? 1 : 0;
18091
+ viewport.style.height = !moving && open ? "" : `${Math.max(0, h.x).toFixed(2)}px`;
18092
+ viewport.toggleAttribute("data-moving", moving);
18093
+ content.style.opacity = (open ? Math.min(1, k * 1.25) : Math.max(0, k * 1.5 - 0.5)).toFixed(3);
18094
+ };
18095
+ const loop = frameLoop(win, (dt) => {
18096
+ h.step(dt);
18097
+ const moving = !h.resting;
18098
+ paint(moving);
18099
+ return moving;
18100
+ });
18101
+ const sync = () => {
18102
+ root.toggleAttribute("data-open", open);
18103
+ toggle.setAttribute("aria-expanded", String(open));
18104
+ viewport.inert = !open;
18105
+ };
18106
+ sync();
18107
+ h.snap(0);
18108
+ paint(false);
18109
+ if (open) {
18110
+ viewport.style.height = "";
18111
+ content.style.opacity = "1";
18112
+ }
18113
+ const api = {
18114
+ element: root,
18115
+ get open() {
18116
+ return open;
18117
+ },
18118
+ toggle(next = !open) {
18119
+ if (next === open)
18120
+ return;
18121
+ open = next;
18122
+ sync();
18123
+ full = content.offsetHeight;
18124
+ if (reducedMotion(win)) {
18125
+ loop.stop();
18126
+ h.snap(open ? full : 0);
18127
+ viewport.style.height = open ? "" : "0px";
18128
+ content.style.opacity = open ? "1" : "0";
18129
+ return;
18130
+ }
18131
+ if (h.resting)
18132
+ h.snap(open ? 0 : full);
18133
+ h.set(open ? full : 0);
18134
+ paint(true);
18135
+ loop.wake();
18136
+ },
18137
+ dispose() {
18138
+ loop.stop();
18139
+ root.remove();
18140
+ }
18141
+ };
18142
+ toggle.addEventListener("click", () => api.toggle());
18143
+ return api;
18144
+ }
18145
+
18146
+ // src/ui/walkthrough-media.ts
18147
+ init_compare_model();
18148
+ init_glide();
18149
+ init_icons();
18150
+ init_look();
18151
+
18152
+ // src/ui/walkthrough-css.ts
18153
+ init_motion();
18154
+ var snappy3 = springCss(SPRINGS.snappy);
18155
+ var calm = springCss(SPRINGS.calm);
18156
+ var state = springCss(SPRINGS.default);
18157
+ var quick = springCss(FADE);
18158
+ var fade2 = "-webkit-mask-image:linear-gradient(to right,#000 calc(100% - 1.5em),transparent);mask-image:linear-gradient(to right,#000 calc(100% - 1.5em),transparent)";
18159
+ var walkthroughCss = `
18160
+ :where(.knobs-walkthrough,.knobs-walkthrough *,.knobs-wt-slot,.knobs-wt-slot *){box-sizing:border-box}
18161
+ .knobs-ui.knobs-walkthrough{--kw-snappy:${snappy3.easing};--kw-snappy-ms:${snappy3.duration}ms;--kw-calm:${calm.easing};--kw-calm-ms:${calm.duration}ms;--kw-state:${state.easing};--kw-state-ms:${state.duration}ms;--kw-quick:${quick.easing};--kw-quick-ms:${quick.duration}ms;
18162
+ --kw-measure:var(--k-walkthrough-measure,68ch);--kw-width:var(--k-walkthrough-width,680px);--kw-rail-w:220px;--kw-good:var(--k-walkthrough-good,var(--_k-ok));--kw-info:var(--k-walkthrough-info,var(--_k-info));
18163
+ --kw-card:var(--_ku-pane-bg);--kw-card-ring:var(--_ku-pane-ring);--kw-under:var(--_k-bg-page);--kw-ink:var(--k-walkthrough-ink,#ff375f);--kw-strong:var(--_k-weight-label);
18164
+ container:knobs-wt / inline-size;display:block;width:100%;color:var(--_k-fg);font:var(--_k-weight) var(--_k-type-body)/var(--_k-leading-body) var(--_k-font);font-variant-numeric:tabular-nums;text-align:left;
18165
+ letter-spacing:normal;-webkit-font-smoothing:antialiased}
18166
+ .knobs-walkthrough [hidden]{display:none!important}
18167
+ /* (0,0,1): a host's element rules stop here, and every class rule below wins over it. */
18168
+ :where(.knobs-walkthrough) :is(h1,h2,h3,p,ol,ul,dl,dd){margin:0;font:inherit;letter-spacing:inherit}
18169
+ .knobs-walkthrough a{color:inherit;text-decoration:underline;text-decoration-color:color-mix(in srgb,currentColor 35%,transparent);text-underline-offset:2px;transition:text-decoration-color .15s}
18170
+ .knobs-walkthrough a:hover{text-decoration-color:currentColor}
18171
+ .knobs-walkthrough code,.knobs-wt-slot code{font:var(--_k-type-label) var(--_k-mono);padding:1px 5px;border-radius:calc(var(--_k-radius) / 2);background:var(--_k-fill)}
18172
+ :where(.knobs-ui) :is(.knobs-wt-btn,.knobs-wt-pick,.knobs-wt-toc-row,button.knobs-wt-stitle,.knobs-wt-chapter,.knobs-wt-disc-toggle){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;
18173
+ background:transparent;border:0;border-radius:0;margin:0;padding:0;min-width:0;box-shadow:none;outline:none}
18174
+
18175
+ /* ——— layout: a readable column, centred, with a slim contents rail beside it (right by default);
18176
+ one column where it's narrow ——— */
18177
+ .knobs-wt-layout{display:grid;grid-template-columns:minmax(0,var(--kw-width)) var(--kw-rail-w);justify-content:center;gap:var(--_k-space-column);align-items:start}
18178
+ .knobs-walkthrough[data-rail="right"] .knobs-wt-main{grid-column:1;grid-row:1}
18179
+ .knobs-walkthrough[data-rail="right"] .knobs-wt-rail{grid-column:2;grid-row:1}
18180
+ .knobs-walkthrough[data-rail="left"] .knobs-wt-layout{grid-template-columns:var(--kw-rail-w) minmax(0,var(--kw-width))}
18181
+ .knobs-walkthrough[data-rail="none"] .knobs-wt-layout{grid-template-columns:minmax(0,var(--kw-width))}
18182
+ .knobs-wt-main{display:flex;flex-direction:column;gap:var(--_k-space-group);min-width:0;padding-bottom:12px}
18183
+ /* The intro, the updates and each section: a section's room apart, the hairline in the middle. */
18184
+ .knobs-wt-flow{display:flex;flex-direction:column;gap:calc(var(--_k-space-section) / 2);min-width:0}
18185
+ .knobs-wt-flow>*+*{padding-top:calc(var(--_k-space-section) / 2);box-shadow:inset 0 1px 0 var(--_k-border-subtle)}
18186
+ @container knobs-wt (max-width:940px){.knobs-walkthrough .knobs-wt-layout{grid-template-columns:minmax(0,1fr);gap:var(--_k-space-title)}
18187
+ .knobs-walkthrough[data-rail] .knobs-wt-layout>*{grid-column:auto;grid-row:auto}}
18188
+
18189
+ /* ——— one line of facts: small, faint, joined by ·, its trailing edge fading ——— */
18190
+ .knobs-wt-metaline{display:flex;align-items:baseline;gap:var(--_k-space-inside);min-width:0;overflow:hidden;white-space:nowrap;font-size:var(--_k-type-small);line-height:var(--_k-leading-small);color:var(--_k-fg-3);${fade2}}
18191
+ .knobs-wt-metaline>*{flex:none}
18192
+ .knobs-wt-metaline>*+*::before{content:"·";margin-right:var(--_k-space-inside);color:var(--_k-fg-3)}
18193
+ .knobs-walkthrough .knobs-wt-metaline a{display:inline-flex;align-items:baseline;color:inherit;text-decoration:none;transition:color calc(var(--_k-hover-out) * 1ms)}
18194
+ .knobs-walkthrough .knobs-wt-metaline a:hover{color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
18195
+ .knobs-wt-metaline svg{align-self:center;width:12px;height:12px;margin-left:2px}
18196
+ .knobs-wt-kicker-lead{color:var(--_k-fg);font-weight:var(--kw-strong)}
18197
+ .knobs-wt-metaline [data-status="on"]{color:var(--_k-fg)}
18198
+ .knobs-wt-metaline [data-status="warn"]{color:var(--_k-warn)}
18199
+
18200
+ /* ——— the page's head ——— */
18201
+ .knobs-wt-head{display:flex;flex-direction:column;gap:var(--_k-space-title)}
18202
+ .knobs-wt-title{font-size:var(--_k-type-title)!important;line-height:var(--_k-leading-title);font-weight:var(--kw-strong)!important;text-wrap:balance}
18203
+ .knobs-wt-summary{max-width:var(--kw-measure);color:var(--_k-fg-2);text-wrap:pretty}
18204
+ .knobs-wt-meta{display:flex;flex-wrap:wrap;gap:var(--_k-space-inside) var(--_k-space-title);color:var(--_k-fg-3);font-size:var(--_k-type-small);line-height:var(--_k-leading-small)}
18205
+ .knobs-wt-meta>span{display:inline-flex;gap:var(--_k-space-inside);white-space:nowrap}
18206
+ .knobs-wt-meta b{font-weight:var(--_k-weight);color:var(--_k-fg-2);font-family:var(--_k-mono)}
18207
+
18208
+ /* ——— a section: a PR or topic. Its kicker leads the meta line; its title outranks the page's ——— */
18209
+ .knobs-wt-section{display:flex;flex-direction:column;gap:var(--_k-space-group);min-width:0}
18210
+ .knobs-wt-sintro{display:flex;flex-direction:column;gap:var(--_k-space-title);min-width:0}
18211
+ .knobs-wt-shead{display:flex;flex-direction:column;gap:var(--_k-space-inside);min-width:0;scroll-margin-top:16px}
18212
+ .knobs-wt-heading{font-size:var(--_k-type-heading)!important;line-height:var(--_k-leading-heading);font-weight:var(--kw-strong)!important;letter-spacing:-.01em!important;text-wrap:balance}
18213
+ .knobs-wt-ssum{max-width:var(--kw-measure);color:var(--_k-fg-2);text-wrap:pretty}
18214
+ .knobs-wt-chip{display:inline-flex;align-items:center;gap:6px;height:20px;padding:0 8px;border-radius:var(--_k-round);font-size:var(--_k-type-small);font-weight:var(--kw-strong);white-space:nowrap;
18215
+ --kw-tone:var(--_k-fg-2);color:var(--kw-tone);background:color-mix(in srgb,var(--kw-tone) 14%,transparent)}
18216
+ .knobs-wt-chip::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor}
18217
+ :is(.knobs-wt-chip,.knobs-wt-toc-glyph)[data-tone="good"]{--kw-tone:var(--kw-good)}
18218
+ :is(.knobs-wt-chip,.knobs-wt-toc-glyph)[data-tone="warn"]{--kw-tone:var(--_k-warn)}
18219
+ :is(.knobs-wt-chip,.knobs-wt-toc-glyph)[data-tone="bad"]{--kw-tone:var(--_k-danger)}
18220
+ :is(.knobs-wt-chip,.knobs-wt-toc-glyph)[data-tone="info"]{--kw-tone:var(--kw-info)}
18221
+ :is(.knobs-wt-chip,.knobs-wt-toc-glyph)[data-tone="dim"]{--kw-tone:var(--_k-fg-2)}
18222
+
18223
+ /* ——— a step: on the page by default, a group's room between steps with a hairline in the middle;
18224
+ or (cards) one card each, the one surface its pieces sit on ——— */
18225
+ .knobs-walkthrough:not([data-cards]) .knobs-wt-step:not([data-full]){padding:0;border-radius:0;background:transparent;box-shadow:none}
18226
+ .knobs-walkthrough:not([data-cards]) .knobs-wt-step+.knobs-wt-step::before{content:"";position:absolute;left:0;right:0;top:calc(var(--_k-space-group) / -2);height:1px;background:var(--_k-border-subtle);pointer-events:none}
18227
+ .knobs-wt-step{position:relative;display:flex;flex-direction:column;gap:var(--_k-space-title);padding:var(--_k-pad-card);border-radius:var(--_k-radius-panel);background:var(--kw-card);box-shadow:var(--kw-card-ring);
18228
+ scroll-margin-top:16px;outline:none;container:knobs-wt-step / inline-size}
18229
+ .knobs-walkthrough[data-cards]{--kw-under:var(--kw-card)}
18230
+ .knobs-walkthrough[data-cards][data-frame="flat"] .knobs-wt-step{box-shadow:inset 0 0 0 1px var(--_k-border)}
18231
+ .knobs-wt-step-head{display:flex;flex-direction:column;gap:var(--_k-space-inside);min-width:0}
18232
+ .knobs-wt-stepTitle{min-width:0;font-weight:var(--kw-strong)!important;text-wrap:balance}
18233
+ .knobs-wt-caption{max-width:var(--kw-measure);color:var(--_k-fg-2);text-wrap:pretty}
18234
+ .knobs-wt-question{font-weight:var(--kw-strong)!important;max-width:var(--kw-measure)}
18235
+ .knobs-wt-media{display:grid;grid-template-columns:repeat(var(--kw-n,1),minmax(0,1fr));gap:var(--_k-space-group) var(--_k-space-column);min-width:0}
18236
+ @container knobs-wt-step (max-width:640px){.knobs-wt-media{grid-template-columns:minmax(0,1fr)}}
18237
+
18238
+ /* ——— the feed: updates and answers, newest first. Tight inside an item (4 / 8 / 4), a group's
18239
+ room between items with the hairline in the middle; no boxes ——— */
18240
+ .knobs-wt-feed{display:flex;flex-direction:column;gap:var(--_k-space-title);min-width:0;scroll-margin-top:16px}
18241
+ .knobs-wt-feed-head{display:flex;align-items:baseline;gap:var(--_k-space-sibling)}
18242
+ .knobs-wt-feed-title{font-weight:var(--kw-strong)!important}
18243
+ .knobs-wt-feed-count{font-size:var(--_k-type-small);line-height:var(--_k-leading-small);color:var(--_k-fg-3)}
18244
+ .knobs-wt-feed-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:var(--_k-space-group);min-width:0}
18245
+ .knobs-wt-fi{position:relative;display:flex;flex-direction:column;gap:var(--_k-space-sibling);min-width:0}
18246
+ .knobs-wt-fi+.knobs-wt-fi::before{content:"";position:absolute;left:0;right:0;top:calc(var(--_k-space-group) / -2);height:1px;background:var(--_k-border-subtle)}
18247
+ .knobs-wt-fi-q,.knobs-wt-fi-a{display:flex;flex-direction:column;gap:var(--_k-space-inside);min-width:0}
18248
+ .knobs-wt-fi-ask{max-width:var(--kw-measure);color:var(--_k-fg);font-weight:var(--kw-strong);text-wrap:pretty}
18249
+ .knobs-wt-fi-text{max-width:var(--kw-measure);color:var(--_k-fg);text-wrap:pretty}
18250
+ .knobs-wt-fi-answer{max-width:var(--kw-measure);color:var(--_k-fg-2);text-wrap:pretty}
18251
+ :where(.knobs-ui) .knobs-wt-fi-step{all:revert;box-sizing:border-box;font:inherit;color:inherit;background:transparent;border:0;margin:0;padding:0;cursor:pointer;outline:none;
18252
+ transition:color calc(var(--_k-hover-out) * 1ms)}
18253
+ .knobs-ui .knobs-wt-fi-step:is(:hover,:focus-visible){color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
18254
+ :where(.knobs-ui) .knobs-wt-steplink{all:revert;box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;margin:0 0 0 2px;padding:0;vertical-align:-5px;border:0;
18255
+ border-radius:calc(var(--_k-radius) / 2);background:transparent;color:var(--_k-fg-3);cursor:pointer;outline:none;transition:background calc(var(--_k-hover-out) * 1ms),color calc(var(--_k-hover-out) * 1ms)}
18256
+ .knobs-ui .knobs-wt-steplink:is(:hover,:focus-visible){background:var(--_k-fill);color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
18257
+ .knobs-wt-steplink svg{width:12px;height:12px}
18258
+
18259
+ /* ——— a disclosure: a small faint toggle with a caret, no fill. The body's height springs (in
18260
+ script) and its content fades in place, top-anchored, under a soft bottom mask ——— */
18261
+ .knobs-wt-disc{display:flex;flex-direction:column;min-width:0}
18262
+ .knobs-ui .knobs-wt-disc-toggle{display:inline-flex;align-items:center;gap:var(--_k-space-inside);width:max-content;height:20px;color:var(--_k-fg-3);font-size:var(--_k-type-small);line-height:var(--_k-leading-small);cursor:pointer;
18263
+ border-radius:calc(var(--_k-radius) / 2);transition:color calc(var(--_k-hover-out) * 1ms)}
18264
+ .knobs-ui .knobs-wt-disc-toggle:is(:hover,:focus-visible),.knobs-wt-disc[data-open]>.knobs-wt-disc-toggle{color:var(--_k-fg-2);transition-duration:calc(var(--_k-hover-in) * 1ms)}
18265
+ .knobs-ui .knobs-wt-disc-toggle:focus-visible{box-shadow:0 0 0 2px var(--_k-focus)}
18266
+ .knobs-wt-disc-toggle svg{flex:none;width:12px;height:12px;transition:rotate var(--kw-state-ms) var(--kw-state)}
18267
+ .knobs-wt-disc[data-open]>.knobs-wt-disc-toggle svg{rotate:90deg}
18268
+ .knobs-wt-disc-count{color:var(--_k-fg-3)}
18269
+ .knobs-wt-disc-body{overflow:hidden}
18270
+ .knobs-wt-disc[data-open]>.knobs-wt-disc-body:not([data-moving]){overflow:visible}
18271
+ .knobs-wt-disc-body[data-moving]{-webkit-mask-image:linear-gradient(#000 calc(100% - 16px),transparent);mask-image:linear-gradient(#000 calc(100% - 16px),transparent)}
18272
+ .knobs-wt-disc-content{padding-top:var(--_k-space-inside)}
18273
+ /* A trail sits on the toggle's label edge: faint lines, the time trailing. */
18274
+ .knobs-wt-trail{display:flex;flex-direction:column;list-style:none;padding:0 0 0 calc(12px + var(--_k-space-inside));font-size:var(--_k-type-small);line-height:var(--_k-leading-small);color:var(--_k-fg-3);min-width:0}
18275
+ .knobs-wt-trail li{display:flex;align-items:baseline;gap:var(--_k-space-sibling);min-width:0;white-space:nowrap}
18276
+ .knobs-wt-trail-text{flex:0 1 auto;min-width:0;overflow:hidden;${fade2}}
18277
+ .knobs-wt-trail-at{flex:none}
18278
+
18279
+ /* ——— a step's sent notes ——— */
18280
+ .knobs-wt-notes{display:flex;flex-direction:column}
18281
+ .knobs-wt-notes:empty{display:none}
18282
+ .knobs-wt-n{display:flex;align-items:flex-start;gap:var(--_k-space-sibling);padding:6px 0;overflow:hidden;
18283
+ transition:height var(--kw-snappy-ms) var(--kw-snappy),opacity var(--kw-snappy-ms) var(--kw-snappy),filter var(--kw-snappy-ms) var(--kw-snappy)}
18284
+ .knobs-wt-n[data-entering]{opacity:0;filter:blur(3px)}
18285
+ .knobs-wt-n[data-leaving]{opacity:0;filter:blur(3px);padding-block:0}
18286
+ .knobs-wt-n+.knobs-wt-n{box-shadow:inset 0 1px 0 var(--_k-border-subtle)}
18287
+ .knobs-wt-n-meta{flex:none;min-width:44px;color:var(--_k-fg-3);font:var(--_k-weight) var(--_k-type-small)/var(--_k-leading-body) var(--_k-mono)}
18288
+ .knobs-wt-n-meta[data-seek]{cursor:pointer}
18289
+ .knobs-wt-n-meta[data-seek]:hover{color:var(--_k-fg)}
18290
+ .knobs-wt-n-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:var(--_k-space-sibling)}
18291
+ .knobs-wt-n-voice{height:30px;max-width:320px}
18292
+ .knobs-wt-n-ink{display:block;width:120px;max-height:80px;border-radius:calc(var(--_k-radius) / 2);background:var(--_k-fill)}
18293
+ .knobs-wt-n-ink path{fill:none;stroke:var(--kw-ink);stroke-width:2px;vector-effect:non-scaling-stroke;stroke-linecap:round;stroke-linejoin:round}
18294
+
18295
+ /* ——— the note bar, docked at the bottom of the column. It holds its own place at the page's end
18296
+ (so the last step stays clear of it), and script keeps its height as the scroller's
18297
+ scroll-padding-bottom ——— */
18298
+ .knobs-wt-dock{position:sticky;bottom:12px;z-index:40;display:grid;pointer-events:none}
18299
+ .knobs-wt-dock>*{pointer-events:auto}
18300
+ .knobs-wt-dock>:is(.knobs-wt-notebar,.knobs-wt-launch){grid-area:1/1}
18301
+ .knobs-wt-dock[data-mode="docked"]>.knobs-wt-launch{display:none}
18302
+ /* Collapsed: the launcher and the bar swap in place, a quick fade with a little scale and blur
18303
+ from the launcher's corner; the exit is the shorter. */
18304
+ :where(.knobs-ui) .knobs-wt-launch{all:revert;box-sizing:border-box;justify-self:end;align-self:end;display:inline-flex;align-items:center;gap:6px;height:32px;margin:0;padding:0 10px 0 9px;border:0;border-radius:var(--_k-round);
18305
+ background:var(--_k-bg);color:var(--_k-fg-2);box-shadow:var(--_ku-pane-ring),var(--_k-shadow-overlay);font:var(--kw-strong) var(--_k-type-small)/1 var(--_k-font);cursor:pointer;outline:none;transform-origin:100% 100%;
18306
+ transition:color calc(var(--_k-hover-out) * 1ms),opacity var(--kw-quick-ms) var(--kw-quick),scale var(--kw-quick-ms) var(--kw-quick),filter var(--kw-quick-ms) var(--kw-quick)}
18307
+ .knobs-ui .knobs-wt-launch:is(:hover,:focus-visible){color:var(--_k-fg)}
18308
+ .knobs-ui .knobs-wt-launch:focus-visible{box-shadow:var(--_k-focus-ring)}
18309
+ .knobs-wt-launch svg{width:16px;height:16px}
18310
+ .knobs-wt-launch kbd{font:inherit;font-weight:var(--_k-weight);color:var(--_k-fg-3)}
18311
+ .knobs-wt-dock[data-mode="collapsed"] .knobs-wt-notebar{transform-origin:100% 100%;transition:opacity var(--kw-quick-ms) var(--kw-quick),scale var(--kw-quick-ms) var(--kw-quick),filter var(--kw-quick-ms) var(--kw-quick)}
18312
+ .knobs-wt-dock[data-mode="collapsed"]:not([data-open])>.knobs-wt-notebar,.knobs-wt-dock[data-mode="collapsed"][data-open]>.knobs-wt-launch{opacity:0;scale:.98;filter:blur(3px);pointer-events:none;
18313
+ transition-duration:calc(var(--kw-quick-ms) * .8)}
18314
+ .knobs-wt-notebar{display:flex;flex-direction:column;gap:2px;padding:6px 6px 6px 14px;border-radius:calc(var(--_k-radius-panel) + 2px);background:var(--_k-bg);
18315
+ box-shadow:var(--_ku-pane-ring),var(--_k-shadow-overlay)}
18316
+ .knobs-wt-on{padding-top:3px;color:var(--_k-fg-3);font-size:var(--_k-type-small);line-height:var(--_k-leading-small);white-space:nowrap;overflow:hidden;${fade2}}
18317
+ .knobs-wt-row{display:flex;align-items:flex-end;gap:4px}
18318
+ :where(.knobs-ui) :is(.knobs-wt-input,.knobs-wt-tool,.knobs-wt-x,.knobs-wt-back){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;background:transparent;border:0;
18319
+ border-radius:0;margin:0;padding:0;min-width:0;box-shadow:none;outline:none}
18320
+ .knobs-ui .knobs-wt-input{flex:1;min-width:0;height:32px;max-height:140px;padding:6px 0;resize:none;color:var(--_k-fg);caret-color:var(--_k-accent);line-height:var(--_k-leading-body);font-size:var(--_k-type-body)}
18321
+ .knobs-ui .knobs-wt-input::placeholder{color:var(--_k-fg-3)}
18322
+ .knobs-wt-attach{display:flex;gap:4px;align-items:center;align-self:center}
18323
+ .knobs-wt-attach:empty{display:none}
18324
+ .knobs-wt-chip-a{display:inline-flex;align-items:center;gap:2px;height:24px;padding:0 2px 0 9px;border-radius:var(--_k-round);background:var(--_k-fill-selected);font-size:var(--_k-type-small);white-space:nowrap}
18325
+ .knobs-ui .knobs-wt-x{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:50%;color:var(--_k-fg-3);cursor:pointer;transition:background .15s,color .15s}
18326
+ .knobs-ui .knobs-wt-x:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
18327
+ .knobs-wt-x svg{width:12px;height:12px}
18328
+ .knobs-ui .knobs-wt-tool{flex:none;display:inline-flex;align-items:center;justify-content:center;gap:5px;min-width:32px;height:32px;padding:0 8px;border-radius:calc(var(--_k-radius) + 2px);
18329
+ color:var(--_k-fg-2);cursor:pointer;transition:background .15s,color .15s}
18330
+ .knobs-ui .knobs-wt-tool:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
18331
+ .knobs-ui .knobs-wt-tool[aria-pressed="true"]{background:var(--_k-fill-selected);color:var(--_k-fg)}
18332
+ .knobs-wt-tool svg{width:16px;height:16px}
18333
+ .knobs-wt-rec{font-size:var(--_k-type-small)}
18334
+ .knobs-wt-rec:empty{display:none}
18335
+ .knobs-ui .knobs-wt-tool[data-send]{color:var(--_k-fg-3)}
18336
+ .knobs-ui .knobs-wt-tool[data-send][data-ready]{background:var(--_k-accent);color:var(--_k-bg)}
18337
+ /* Back: in place over the bar for a few seconds after a note moves the page on. */
18338
+ .knobs-ui .knobs-wt-back{position:absolute;left:8px;bottom:calc(100% + 8px);display:inline-flex;align-items:center;gap:var(--_k-space-inside);max-width:min(360px,calc(100% - 16px));height:28px;padding:0 12px 0 9px;
18339
+ border-radius:var(--_k-round);background:var(--_k-bg);color:var(--_k-fg);box-shadow:var(--_ku-pane-ring),var(--_k-shadow-overlay);font-size:var(--_k-type-small);font-weight:var(--kw-strong);cursor:pointer;
18340
+ opacity:0;scale:.96;filter:blur(3px);pointer-events:none;transition:opacity .11s cubic-bezier(.4,0,1,1),scale .11s cubic-bezier(.4,0,1,1),filter .11s cubic-bezier(.4,0,1,1)}
18341
+ .knobs-ui .knobs-wt-back[data-on]{opacity:1;scale:1;filter:none;pointer-events:auto;transition:opacity .15s cubic-bezier(.2,.8,.2,1),scale .15s cubic-bezier(.2,.8,.2,1),filter .15s cubic-bezier(.2,.8,.2,1)}
18342
+ .knobs-ui .knobs-wt-back:hover{background:var(--_k-fill-selected)}
18343
+ .knobs-wt-back svg{flex:none;width:12px;height:12px}
18344
+ .knobs-wt-back-label{min-width:0;overflow:hidden;white-space:nowrap;${fade2}}
18345
+
18346
+ /* ——— ink over a step's media ——— */
18347
+ .knobs-wt-media,.knobs-wt-options{position:relative}
18348
+ .knobs-wt-ink{position:absolute;inset:0;z-index:5;width:100%;height:100%;overflow:visible;pointer-events:none;touch-action:none}
18349
+ .knobs-wt-ink[data-on]{pointer-events:auto;cursor:crosshair}
18350
+ .knobs-wt-ink path{fill:none;stroke:var(--kw-ink);stroke-width:3px;vector-effect:non-scaling-stroke;stroke-linecap:round;stroke-linejoin:round}
18351
+
18352
+ /* ——— full-window: the step over the page above the note bar, its media filling it; its place held
18353
+ in the page. The one raised surface ——— */
18354
+ .knobs-wt-scrim{position:fixed;inset:0;z-index:30;background:color-mix(in srgb,var(--_k-bg-page) 82%,transparent);opacity:0;pointer-events:none}
18355
+ .knobs-wt-scrim[data-on]{pointer-events:auto}
18356
+ .knobs-wt-hold-card{flex:none}
18357
+ .knobs-wt-step[data-full]{position:fixed;z-index:35;margin:0;overflow:hidden;overscroll-behavior:contain;padding:var(--_k-pad-card);border-radius:var(--_k-radius-panel);box-shadow:var(--_ku-pane-ring),var(--_k-shadow-overlay);background:var(--_k-bg)}
18358
+ .knobs-wt-step[data-full]::before{content:none}
18359
+ .knobs-wt-step[data-full]>:not(.knobs-wt-media){flex:none}
18360
+ .knobs-wt-step[data-full] .knobs-wt-notes{display:none}
18361
+ .knobs-wt-step[data-full]>.knobs-wt-media{flex:1;min-height:0;grid-auto-rows:minmax(0,1fr)}
18362
+ .knobs-wt-step[data-full] .knobs-wt-slot{display:flex;flex-direction:column;min-height:0}
18363
+ .knobs-wt-step[data-full] .knobs-wt-body{flex:1;min-height:0}
18364
+ .knobs-wt-step[data-full] .knobs-wt-fit{height:100%;transition:none}
18365
+ .knobs-wt-step[data-full] .knobs-wt-frame{height:100%;aspect-ratio:auto}
18366
+
18367
+ /* ——— a taste test: the options side by side, Kit's pick checked and the rest stepping back. On
18368
+ the step's card, so no fill of their own ——— */
18369
+ .knobs-wt-options{display:grid;grid-template-columns:repeat(var(--kw-n,2),minmax(0,1fr));gap:var(--_k-space-group) var(--_k-space-column)}
18370
+ @container knobs-wt-step (max-width:640px){.knobs-wt-options{grid-template-columns:minmax(0,1fr)}}
18371
+ .knobs-wt-option{position:relative;display:flex;flex-direction:column;gap:var(--_k-space-sibling);min-width:0}
18372
+ .knobs-wt-option>:not(.knobs-wt-pick){transition:opacity var(--kw-snappy-ms) var(--kw-snappy)}
18373
+ .knobs-wt-options[data-has-pick] .knobs-wt-option:not([data-picked])>:not(.knobs-wt-pick){opacity:.55}
18374
+ .knobs-ui .knobs-wt-pick{display:flex;align-items:center;gap:8px;width:100%;min-height:28px;margin:0 -6px;width:calc(100% + 12px);padding:0 6px;border-radius:var(--_k-radius);cursor:pointer;color:var(--_k-fg-2);text-align:left;
18375
+ transition:background calc(var(--_k-hover-out) * 1ms)}
18376
+ .knobs-ui .knobs-wt-pick:is(:hover,:focus-visible){color:var(--_k-fg);background:var(--_k-fill);transition-duration:calc(var(--_k-hover-in) * 1ms)}
18377
+ .knobs-wt-letter{flex:none;font-weight:var(--kw-strong);color:var(--_k-fg)}
18378
+ .knobs-wt-olabel{flex:1;min-width:0;font-weight:var(--kw-strong);color:var(--_k-fg)}
18379
+ .knobs-wt-suggested{flex:none;font-size:var(--_k-type-small);color:var(--_k-fg-3)}
18380
+ .knobs-wt-check{flex:none;display:flex;width:18px;height:18px;align-items:center;justify-content:center;border-radius:50%;background:var(--_k-fill);color:transparent;
18381
+ transition:background var(--kw-snappy-ms) var(--kw-snappy),color var(--kw-snappy-ms) var(--kw-snappy)}
18382
+ .knobs-wt-check svg{width:12px;height:12px}
18383
+ .knobs-wt-option[data-picked] .knobs-wt-check{background:var(--_k-accent);color:var(--_k-bg)}
18384
+ .knobs-wt-key{display:inline-flex;align-items:center;justify-content:center;min-width:15px;height:15px;padding:0 3px;border-radius:4px;background:var(--_k-fill);color:var(--_k-fg-3);
18385
+ font:var(--kw-strong) var(--_k-type-small)/1 var(--_k-font);opacity:.5;transition:opacity .15s}
18386
+ .knobs-wt-step[data-current] .knobs-wt-key{opacity:1}
18387
+ .knobs-wt-ocap{color:var(--_k-fg-2);max-width:var(--kw-measure)}
18388
+
18389
+ /* ——— the rail: progress, contents, export ——— */
18390
+ .knobs-wt-rail{position:sticky;top:16px;display:flex;flex-direction:column;gap:var(--_k-space-title);max-height:calc(100vh - 32px);min-height:0;font-size:var(--_k-type-small);line-height:var(--_k-leading-small)}
18391
+ .knobs-wt-progress{display:flex;flex-direction:column;gap:var(--_k-space-sibling)}
18392
+ .knobs-wt-count{display:flex;align-items:baseline;justify-content:space-between;color:var(--_k-fg-2)}
18393
+ .knobs-wt-count b{color:var(--_k-fg);font-weight:var(--kw-strong)}
18394
+ .knobs-wt-bar{position:relative;height:4px;border-radius:2px;overflow:hidden;background:var(--_k-fill)}
18395
+ .knobs-wt-fill{position:absolute;inset:0;border-radius:inherit;background:var(--_k-accent);transform-origin:0 50%;transform:scaleX(var(--kw-done,0))}
18396
+ .knobs-walkthrough[data-settled] .knobs-wt-fill{transition:transform var(--kw-calm-ms) var(--kw-calm)}
18397
+ /* The contents: rows centred on one spine, title rows (32 high, the control radius) and the
18398
+ steps under them (20 high, half of it), bled out so their text sits on the rail's edge; the
18399
+ selection pill slides to the current step, and base rows never brighten for being current. */
18400
+ .knobs-wt-toc{position:relative;flex:1 1 auto;min-height:0;overflow:auto;scrollbar-width:none;display:flex;flex-direction:column;gap:var(--_k-space-sibling);margin:0 calc(var(--_k-space-sibling) * -1);padding:0 0 8px}
18401
+ .knobs-wt-toc::-webkit-scrollbar{display:none}
18402
+ .knobs-wt-toc-group{display:flex;flex-direction:column;min-width:0}
18403
+ .knobs-ui .knobs-wt-toc-row{position:relative;display:grid;grid-template-columns:var(--kw-toc-n,0px) minmax(0,1fr) auto;column-gap:var(--_k-space-sibling);align-items:center;flex:none;width:100%;height:32px;
18404
+ padding:0 var(--_k-space-sibling);border-radius:var(--_k-radius);color:var(--_k-fg-2);font-size:var(--_k-type-body);line-height:var(--_k-leading-body);font-weight:var(--kw-strong);text-align:left;white-space:nowrap;cursor:pointer;
18405
+ transition:color calc(var(--_k-hover-out) * 1ms)}
18406
+ .knobs-ui .knobs-wt-toc-sub{height:20px;border-radius:calc(var(--_k-radius) / 2);font-size:var(--_k-type-small);line-height:var(--_k-leading-small);font-weight:var(--_k-weight);color:var(--_k-fg-3)}
18407
+ .knobs-wt-toc[data-bare] .knobs-wt-toc-row{grid-template-columns:minmax(0,1fr) auto}
18408
+ .knobs-wt-toc[data-bare] .knobs-wt-toc-n{display:none}
18409
+ .knobs-ui .knobs-wt-toc-row:is(:hover,:focus-visible){color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
18410
+ .knobs-ui .knobs-wt-toc-sub:is(:hover,:focus-visible){color:var(--_k-fg-2)}
18411
+ .knobs-ui .knobs-wt-toc-row:focus-visible{box-shadow:inset 0 0 0 1px var(--_k-focus)}
18412
+ .knobs-wt-toc-n,.knobs-wt-toc-end{display:flex;align-items:center;font-size:var(--_k-type-small);line-height:var(--_k-leading-small);font-weight:var(--_k-weight);color:var(--_k-fg-3)}
18413
+ .knobs-wt-toc-row[data-active] .knobs-wt-toc-n{color:var(--_k-fg-2)}
18414
+ .knobs-wt-toc-label{min-width:0;overflow:hidden;${fade2}}
18415
+ .knobs-pick-copy .knobs-wt-toc-n,.knobs-pick-copy .knobs-wt-toc-end{color:var(--_k-fg-2)}
18416
+ /* A step's state: an open ring, a filled dot once answered, nothing to answer a faint ring. */
18417
+ .knobs-wt-toc-glyph{--kw-tone:var(--_k-fg-3);display:block;width:6px;height:6px;border-radius:50%;box-shadow:inset 0 0 0 1.5px var(--kw-tone);
18418
+ transition:background var(--kw-snappy-ms) var(--kw-snappy),box-shadow var(--kw-snappy-ms) var(--kw-snappy)}
18419
+ .knobs-wt-toc-glyph[data-tone="accent"]{--kw-tone:var(--_k-accent)}
18420
+ .knobs-wt-toc-glyph[data-done]{background:var(--kw-tone)}
18421
+ .knobs-wt-toc-glyph[data-tone="none"]{box-shadow:inset 0 0 0 1px var(--_k-border)}
18422
+ .knobs-wt-actions{display:flex;flex-wrap:wrap;gap:4px}
18423
+ .knobs-ui .knobs-wt-btn{display:inline-flex;align-items:center;gap:6px;height:26px;padding:0 9px;border-radius:var(--_k-radius);color:var(--_k-fg-2);cursor:pointer;white-space:nowrap;
18424
+ transition:background .15s,color .15s}
18425
+ .knobs-ui .knobs-wt-btn:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
18426
+ .knobs-ui .knobs-wt-btn[data-primary]{background:var(--_k-fill-selected);color:var(--_k-fg)}
18427
+ .knobs-ui .knobs-wt-btn[data-primary]:is(:hover,:focus-visible){background:color-mix(in srgb,var(--_k-fg) 16%,transparent)}
18428
+ .knobs-wt-keys{color:var(--_k-fg-3);font-size:var(--_k-type-small);line-height:var(--_k-leading-small)}
18429
+ .knobs-wt-keys kbd{font:inherit;color:var(--_k-fg-2)}
18430
+ /* Narrow: the rail becomes a bar along the top, with the progress and the export only. */
18431
+ @container knobs-wt (max-width:940px){
18432
+ .knobs-wt-rail{z-index:4;flex-direction:row;align-items:center;gap:var(--_k-space-title);max-height:none;margin:0 calc(var(--_k-pad-page) * -1);padding:8px var(--_k-pad-page);top:0;background:var(--_k-bg-page)}
18433
+ .knobs-wt-progress{flex:1}.knobs-wt-toc,.knobs-wt-keys{display:none}
18434
+ .knobs-wt-step,.knobs-wt-shead,.knobs-wt-feed{scroll-margin-top:60px}
18435
+ }
18436
+
18437
+ /* ——— media slots. Only a picture's frame (a quiet fill while it loads or letterboxes) and a code
18438
+ block (a well) are bounded; a metric, a callout, a table, a stack and a history sit on the page,
18439
+ set apart by space, hairlines and their own colours. On a card (.knobs-surface) code drops its
18440
+ well for hairlines, so nothing draws a fill and a ring of its own there ——— */
18441
+ .knobs-ui.knobs-wt-slot{position:relative;display:flex;flex-direction:column;gap:var(--_k-space-sibling);min-width:0;container:knobs-wt-slot / inline-size;color:var(--_k-fg);
18442
+ font:var(--_k-weight) var(--_k-type-body)/var(--_k-leading-body) var(--_k-font);font-variant-numeric:tabular-nums;--kw-measure:var(--k-walkthrough-measure,68ch);
18443
+ --kw-good:var(--k-walkthrough-good,var(--_k-ok));--kw-info:var(--k-walkthrough-info,var(--_k-info))}
18444
+ .knobs-wt-slot-label{display:block;font:var(--_k-weight-label) var(--_k-type-label)/var(--_k-leading-label) var(--_k-font);color:var(--_k-fg-2)}
18445
+ .knobs-wt-body{min-width:0}
18446
+ /* A compare is exactly as tall as its pictures at this width, so there's no letterbox: the two
18447
+ sides edge to edge (side) or one (overlaid), plus its mode bar, the timeline and their two gaps
18448
+ (92 px), and the frame strip with its gap (158 px). */
18449
+ .knobs-wt-fit{width:100%;height:calc(100cqw / 2 / var(--kw-aspect,1.7778) + var(--kw-chrome,92px))}
18450
+ .knobs-wt-fit[data-overlay]{height:calc(100cqw / var(--kw-aspect,1.7778) + var(--kw-chrome,92px))}
18451
+ .knobs-wt-fit[data-strip]{--kw-chrome:158px}
18452
+ .knobs-wt-fit[data-settled]{transition:height var(--k-wt-calm-ms,${calm.duration}ms) ${calm.easing}}
18453
+ .knobs-wt-frame{position:relative;width:100%;overflow:hidden;border-radius:calc(var(--_k-radius-panel) - 4px);background:var(--_k-fill);aspect-ratio:var(--kw-aspect,16/9)}
18454
+ .knobs-wt-frame>:is(video,img){position:absolute;inset:0;display:block;width:100%;height:100%;object-fit:contain;margin:0;border:0}
18455
+ .knobs-wt-frame[data-natural]{aspect-ratio:auto;background:transparent}
18456
+ .knobs-wt-frame[data-natural]>img{position:static;height:auto}
18457
+ .knobs-wt-code{margin:0;padding:var(--_k-pad-card);max-height:420px;overflow:auto;border-radius:calc(var(--_k-radius-panel) - 4px);background:var(--_k-fill);color:var(--_k-fg);
18458
+ font:var(--_k-type-label)/var(--_k-leading-body) var(--_k-mono);white-space:pre;tab-size:2;scrollbar-width:thin}
18459
+ .knobs-surface .knobs-wt-code{padding-inline:0;border-radius:0;background:transparent;box-shadow:inset 0 1px 0 var(--_k-border-subtle),inset 0 -1px 0 var(--_k-border-subtle)}
18460
+ .knobs-wt-code[data-terminal]{white-space:pre-wrap;overflow-wrap:anywhere}
18461
+ .knobs-wt-prompt{color:var(--_k-fg-3);user-select:none;-webkit-user-select:none}
18462
+ .knobs-wt-cmd{color:var(--_k-fg);font-weight:var(--_k-weight-label)}
18463
+ .knobs-wt-out{color:var(--_k-fg-2)}
18464
+ .knobs-wt-metric{display:flex;flex-direction:column;gap:var(--_k-space-sibling)}
18465
+ .knobs-wt-mlabel{color:var(--_k-fg-2)}
18466
+ .knobs-wt-mvalue{display:flex;align-items:baseline;flex-wrap:wrap;gap:var(--_k-space-sibling);font-variant-numeric:tabular-nums;font-feature-settings:"tnum"}
18467
+ .knobs-wt-mfrom{color:var(--_k-fg-3);font-size:var(--_k-type-title);line-height:var(--_k-leading-title)}
18468
+ .knobs-wt-mto{font-size:var(--_k-type-heading);line-height:var(--_k-leading-heading);font-weight:var(--_k-weight-label)}
18469
+ .knobs-wt-mof{color:var(--_k-fg-3)}
18470
+ .knobs-wt-mdelta{--kw-tone:var(--_k-fg-2);font-size:var(--_k-type-small);font-weight:var(--_k-weight-label);color:var(--kw-tone)}
18471
+ .knobs-wt-mdelta[data-tone="good"]{--kw-tone:var(--kw-good)}
18472
+ .knobs-wt-mdelta[data-tone="bad"]{--kw-tone:var(--_k-danger)}
18473
+ .knobs-wt-mtrack{position:relative;height:4px;border-radius:2px;background:color-mix(in srgb,var(--_k-fg) 8%,transparent)}
18474
+ .knobs-wt-mtrack>span{position:absolute;top:0;bottom:0;left:0;border-radius:inherit}
18475
+ .knobs-wt-mtrack>.knobs-wt-mafter{background:var(--_k-accent)}
18476
+ .knobs-wt-mtrack>.knobs-wt-mbefore{background:color-mix(in srgb,var(--_k-fg) 30%,transparent)}
18477
+ .knobs-wt-callout{--kw-tone:var(--kw-info);display:flex;gap:var(--_k-space-sibling);max-width:var(--kw-measure);color:var(--_k-fg)}
18478
+ .knobs-wt-callout::before{content:"";flex:none;width:6px;height:6px;margin-top:7px;border-radius:50%;background:var(--kw-tone)}
18479
+ .knobs-wt-callout[data-tone="good"]{--kw-tone:var(--kw-good)}
18480
+ .knobs-wt-callout[data-tone="warn"]{--kw-tone:var(--_k-warn)}
18481
+ .knobs-wt-callout[data-tone="bad"]{--kw-tone:var(--_k-danger)}
18482
+ .knobs-wt-text{max-width:var(--kw-measure);color:var(--_k-fg-2)}
18483
+ .knobs-wt-hold{border-radius:calc(var(--_k-radius-panel) - 4px);background:var(--_k-fill)}
18484
+ .knobs-wt-failed{display:flex;align-items:center;justify-content:center;min-height:80px;color:var(--_k-fg-3);font-size:var(--_k-type-small)}
18485
+
18486
+ /* ——— data blocks: history, stack, table, chapters. On the step's card, so no card of their own:
18487
+ hairlines and fills only. Tones read the status tokens; hover fades are the panel's own. ——— */
18488
+ :where(.knobs-wt-history,.knobs-wt-stack,.knobs-wt-tablebox,.knobs-wt-chapters){--kw-line:var(--_k-leading-body);--kw-rail:var(--_k-border-strong)}
18489
+ :where(.knobs-wt-slot) [data-tone="good"]{--kw-tone:var(--kw-good)}
18490
+ :where(.knobs-wt-slot) [data-tone="warn"]{--kw-tone:var(--_k-warn)}
18491
+ :where(.knobs-wt-slot) [data-tone="bad"]{--kw-tone:var(--_k-danger)}
18492
+ :where(.knobs-wt-slot) [data-tone="info"]{--kw-tone:var(--kw-info)}
18493
+ :where(.knobs-wt-slot) [data-tone="dim"]{--kw-tone:var(--_k-fg-3)}
18494
+ .knobs-wt-tags{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}
18495
+ .knobs-ui .knobs-wt-tag{display:inline-flex;align-items:center;height:20px;padding:0 6px;border-radius:5px;background:var(--_k-fill);color:var(--_k-fg-2);font:var(--_k-weight) var(--_k-type-small)/1 var(--_k-mono);
18496
+ text-decoration:none;white-space:nowrap;transition:background calc(var(--_k-hover-out) * 1ms),color calc(var(--_k-hover-out) * 1ms)}
18497
+ .knobs-ui a.knobs-wt-tag:is(:hover,:focus-visible){background:var(--_k-fill-selected);color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
18498
+
18499
+ /* A history: dates down the left, a spine of tone dots joined by a hairline (broken around each
18500
+ dot), the event beside it. Inferred events take a ring for a dot and a dashed tag. */
18501
+ .knobs-wt-history{--kw-hy:calc(5px + var(--kw-line) / 2);display:flex;flex-direction:column;gap:18px;font-size:var(--_k-type-body);line-height:var(--kw-line);color:var(--_k-fg)}
18502
+ .knobs-wt-hgroup{display:flex;flex-direction:column;gap:6px;min-width:0}
18503
+ .knobs-wt-hhead{display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 10px}
18504
+ .knobs-ui .knobs-wt-hheading{margin:0;font:var(--_k-weight-label) var(--_k-type-small)/var(--_k-leading-small) var(--_k-font);letter-spacing:.04em;text-transform:uppercase;color:var(--_k-fg-2)}
18505
+ .knobs-wt-hsum{color:var(--_k-fg-3);font-size:var(--_k-type-small);font-variant-numeric:tabular-nums}
18506
+ .knobs-wt-hlist{list-style:none;margin:0;padding:0;display:flex;flex-direction:column}
18507
+ .knobs-wt-hev{display:grid;grid-template-columns:var(--kw-hw,11ch) 14px minmax(0,1fr);grid-template-areas:"when spine main";column-gap:10px;align-items:baseline;padding:5px 0}
18508
+ .knobs-wt-hwhen{grid-area:when;display:flex;flex-direction:column;min-width:0;font:var(--_k-weight) var(--_k-type-label)/var(--kw-line) var(--_k-mono);font-variant-numeric:tabular-nums;color:var(--_k-fg-2)}
18509
+ .knobs-wt-hage{color:var(--_k-fg-3);font-size:var(--_k-type-small);line-height:var(--_k-leading-small)}
18510
+ .knobs-wt-hspine{grid-area:spine;position:relative;align-self:stretch;margin-block:-5px}
18511
+ .knobs-wt-hspine::before,.knobs-wt-hspine::after{content:"";position:absolute;left:6.5px;width:1px;background:var(--kw-rail)}
18512
+ .knobs-wt-hspine::before{top:0;height:calc(var(--kw-hy) - 6px)}
18513
+ .knobs-wt-hspine::after{top:calc(var(--kw-hy) + 6px);bottom:0}
18514
+ .knobs-wt-hev:first-child .knobs-wt-hspine::before,.knobs-wt-hev:last-child .knobs-wt-hspine::after{content:none}
18515
+ .knobs-wt-hdot{position:absolute;left:3.5px;top:calc(var(--kw-hy) - 3.5px);width:7px;height:7px;border-radius:50%;background:var(--kw-tone,var(--_k-fg-3))}
18516
+ .knobs-wt-hev[data-type="release"] .knobs-wt-hdot{border-radius:1.5px;rotate:45deg;scale:.9}
18517
+ .knobs-wt-hev[data-type="audit"] .knobs-wt-hdot{border-radius:1.5px}
18518
+ .knobs-wt-hev[data-provenance="inferred"] .knobs-wt-hdot{background:transparent;box-shadow:inset 0 0 0 1.5px var(--kw-tone,var(--_k-fg-3))}
18519
+ .knobs-wt-hmain{grid-area:main;display:flex;flex-direction:column;gap:2px;min-width:0}
18520
+ .knobs-wt-htitle{display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 8px;font-weight:var(--_k-weight-label);color:var(--_k-fg)}
18521
+ .knobs-wt-prov{padding:0 6px;border-radius:5px;background:var(--_k-fill);color:var(--_k-fg-3);font:var(--_k-weight) var(--_k-type-small)/18px var(--_k-mono);white-space:nowrap}
18522
+ .knobs-wt-hev[data-provenance="inferred"] .knobs-wt-prov{background:transparent;color:var(--_k-warn);outline:1px dashed color-mix(in oklch,var(--_k-warn) 50%,transparent);outline-offset:-1px}
18523
+ .knobs-ui .knobs-wt-hdetail{margin:0;max-width:var(--k-walkthrough-measure,68ch);color:var(--_k-fg-2)}
18524
+ @container knobs-wt-slot (max-width:520px){
18525
+ .knobs-wt-hev{grid-template-columns:14px minmax(0,1fr);grid-template-areas:"spine when" "spine main"}
18526
+ .knobs-wt-hwhen{flex-direction:row;flex-wrap:wrap;align-items:baseline;column-gap:8px}
18527
+ }
18528
+
18529
+ /* A stack: the trunk at the top, each PR hanging off the branch it merges into, rails drawn as a
18530
+ git graph (solid, so where two meet they don't darken). A PR with a step goes there on a click. */
18531
+ .knobs-wt-stack{--kw-spt:7px;--kw-spb:7px;--kw-sy:calc(var(--kw-spt) + var(--kw-line) / 2);display:flex;flex-direction:column;font-size:var(--_k-type-body);line-height:var(--kw-line);color:var(--_k-fg)}
18532
+ .knobs-wt-srow{display:grid;grid-template-columns:auto minmax(0,1fr) auto;column-gap:10px;align-items:start;margin:0 -8px;padding:var(--kw-spt) 8px var(--kw-spb);
18533
+ border-radius:max(0px,calc(var(--_k-radius-panel) - 8px));transition:background calc(var(--_k-hover-out) * 1ms)}
18534
+ .knobs-wt-srow[data-trunk]{--kw-spb:0px;grid-template-columns:auto minmax(0,1fr)}
18535
+ .knobs-wt-srow[data-go]{cursor:pointer}
18536
+ .knobs-wt-srow[data-go]:hover{background:var(--_ku-wash);transition-duration:calc(var(--_k-hover-in) * 1ms)}
18537
+ .knobs-wt-lanes{display:flex;align-self:stretch;margin-block:calc(var(--kw-spt) * -1) calc(var(--kw-spb) * -1)}
18538
+ .knobs-wt-lane{position:relative;flex:none;width:16px}
18539
+ .knobs-wt-lane:is([data-rail="pass"],[data-rail="tee"],[data-down])::before{content:"";position:absolute;left:7.5px;top:0;bottom:0;width:1px;background:var(--kw-rail)}
18540
+ .knobs-wt-lane[data-down]::before{top:calc(var(--kw-sy) + 3.5px)}
18541
+ .knobs-wt-lane:is([data-rail="tee"],[data-rail="end"])::after{content:"";position:absolute;left:7.5px;top:0;width:13px;height:calc(var(--kw-sy) + .5px);border:solid var(--kw-rail);border-width:0 0 1px 1px;border-bottom-left-radius:6px}
18542
+ .knobs-wt-lane[data-rail="node"]::after{content:"";position:absolute;left:4.5px;top:calc(var(--kw-sy) - 3.5px);width:7px;height:7px;border-radius:50%;background:var(--_k-fg-3)}
18543
+ .knobs-wt-srow[data-trunk] .knobs-wt-lane[data-rail="node"]::after{background:var(--_k-fg-2)}
18544
+ .knobs-wt-trunk{font:var(--_k-weight-label) var(--_k-type-label)/var(--kw-line) var(--_k-mono);color:var(--_k-fg-2)}
18545
+ .knobs-wt-smain{display:flex;flex-direction:column;gap:1px;min-width:0}
18546
+ .knobs-wt-sline{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 8px}
18547
+ .knobs-wt-sord{font:var(--_k-weight) var(--_k-type-label)/var(--kw-line) var(--_k-mono);font-variant-numeric:tabular-nums;color:var(--_k-fg-3);white-space:nowrap}
18548
+ .knobs-ui .knobs-wt-spr{font:var(--_k-weight-label) var(--_k-type-label)/var(--kw-line) var(--_k-mono);color:var(--_k-fg-2);text-decoration:none;white-space:nowrap}
18549
+ .knobs-ui a.knobs-wt-spr:is(:hover,:focus-visible){color:var(--_k-fg);text-decoration:underline}
18550
+ .knobs-ui .knobs-wt-stitle{font-weight:var(--_k-weight-label);line-height:var(--kw-line);color:var(--_k-fg);text-align:left}
18551
+ .knobs-ui button.knobs-wt-stitle{cursor:pointer;text-decoration:underline;text-decoration-color:transparent;text-underline-offset:3px;transition:text-decoration-color calc(var(--_k-hover-out) * 1ms)}
18552
+ .knobs-ui :is(.knobs-wt-srow[data-go]:hover button.knobs-wt-stitle,button.knobs-wt-stitle:focus-visible){text-decoration-color:color-mix(in oklch,currentColor 45%,transparent);transition-duration:calc(var(--_k-hover-in) * 1ms)}
18553
+ .knobs-ui .knobs-wt-ssummary{margin:0;max-width:var(--k-walkthrough-measure,68ch);color:var(--_k-fg-2)}
18554
+ .knobs-wt-smeta{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 12px;font:var(--_k-weight) var(--_k-type-small)/var(--kw-line) var(--_k-mono);font-variant-numeric:tabular-nums;color:var(--_k-fg-3)}
18555
+ .knobs-wt-delta{display:inline-flex;gap:6px}
18556
+ .knobs-wt-add{color:var(--_k-diff-add)}
18557
+ .knobs-wt-del{color:var(--_k-diff-del)}
18558
+ .knobs-wt-sstatus{margin-top:calc((var(--kw-line) - 20px) / 2)}
18559
+ .knobs-wt-stack[data-flat] .knobs-wt-srow{grid-template-columns:minmax(0,1fr) auto}
18560
+ .knobs-wt-stack[data-flat] .knobs-wt-srow+.knobs-wt-srow{box-shadow:inset 0 1px 0 var(--_k-border)}
18561
+ @container knobs-wt-slot (max-width:520px){
18562
+ .knobs-wt-srow:not([data-trunk]){grid-template-columns:auto minmax(0,1fr)}
18563
+ .knobs-wt-stack[data-flat] .knobs-wt-srow{grid-template-columns:minmax(0,1fr)}
18564
+ .knobs-wt-sstatus{grid-column:-2;justify-self:start;margin-top:4px}
17669
18565
  }
17670
- var grid = (v) => Math.round(v * 64) / 64;
17671
- function chapterPills(host, chapters, io) {
17672
- const doc = host.ownerDocument;
17673
- const win = doc.defaultView;
17674
- const bar = el(doc, "div", "knobs-wt-chapters");
17675
- bar.setAttribute("role", "group");
17676
- bar.setAttribute("aria-label", "Chapters");
17677
- const mark = el(doc, "span", "knobs-wt-chapter-mark");
17678
- mark.setAttribute("aria-hidden", "true");
17679
- bar.append(mark);
17680
- const pills = chapters.map((c) => {
17681
- const b = el(doc, "button", "knobs-wt-chapter");
17682
- b.type = "button";
17683
- b.append(text(doc, "span", "knobs-wt-chapter-time", clock(c.start)));
17684
- b.append(text(doc, "span", "knobs-wt-chapter-label", c.label));
17685
- b.addEventListener("click", () => {
17686
- io.onEvent?.({ what: "seek", time: Math.round(c.start * 100) / 100, detail: c.label });
17687
- io.seek(c.start);
17688
- sync();
17689
- });
17690
- bar.append(b);
17691
- return b;
17692
- });
17693
- host.append(bar);
17694
- const x = new Spring(0, MOTION.calm.omega, MOTION.calm.zeta, 0.25);
17695
- const y = new Spring(0, MOTION.calm.omega, MOTION.calm.zeta, 0.25);
17696
- const w = new Spring(0, MOTION.calm.omega, MOTION.calm.zeta, 0.25);
17697
- const draw = () => {
17698
- const left = grid(x.x);
17699
- mark.style.transform = `translate(${left}px,${grid(y.x)}px)`;
17700
- mark.style.width = `${grid(x.x + w.x) - left}px`;
17701
- };
17702
- const loop = frameLoop(win, (dt) => {
17703
- x.step(dt);
17704
- y.step(dt);
17705
- w.step(dt);
17706
- draw();
17707
- return !x.resting || !y.resting || !w.resting;
17708
- });
17709
- let at = -2;
17710
- let shown = false;
17711
- const place = (snap) => {
17712
- const p = pills[at];
17713
- if (!p) {
17714
- mark.removeAttribute("data-on");
17715
- pills.forEach((b) => b.removeAttribute("aria-current"));
17716
- shown = false;
17717
- return;
17718
- }
17719
- pills.forEach((b, i) => i === at ? b.setAttribute("aria-current", "true") : b.removeAttribute("aria-current"));
17720
- const to = [p.offsetLeft, p.offsetTop, p.offsetWidth];
17721
- if (snap || !shown || reducedMotion(win)) {
17722
- x.snap(to[0]);
17723
- y.snap(to[1]);
17724
- w.snap(to[2]);
17725
- draw();
17726
- } else {
17727
- x.set(to[0]);
17728
- y.set(to[1]);
17729
- w.set(to[2]);
17730
- loop.wake();
17731
- }
17732
- mark.style.height = `${p.offsetHeight}px`;
17733
- mark.dataset.on = "";
17734
- shown = true;
17735
- };
17736
- const sync = () => {
17737
- const next = chapterAt(chapters, io.now());
17738
- if (next === at)
17739
- return;
17740
- at = next;
17741
- place(false);
17742
- };
17743
- sync();
17744
- const ro = "ResizeObserver" in win ? new win.ResizeObserver(() => place(true)) : null;
17745
- ro?.observe(bar);
17746
- return {
17747
- sync,
17748
- get current() {
17749
- return chapters[at] ?? null;
17750
- },
17751
- dispose() {
17752
- loop.stop();
17753
- ro?.disconnect();
17754
- bar.remove();
17755
- }
17756
- };
18566
+
18567
+ /* A table: a real one, its head stuck while it scrolls, both ways; edges fade where there's more
18568
+ (the top one under the head), and thin bars show on hover. */
18569
+ /* Too narrow for its columns, each row becomes a block: its primary cell titling the rest as term
18570
+ and value, the blocks parted by hairlines. Scrolling sideways is the last resort (stack: never). */
18571
+ .knobs-wt-tablebox{min-width:0}
18572
+ .knobs-wt-tablebox[data-measure] table{width:min-content}
18573
+ .knobs-wt-tstack{display:none;flex-direction:column;min-width:0}
18574
+ .knobs-wt-tablebox[data-stacked]>.knobs-wt-table{display:none}
18575
+ .knobs-wt-tablebox[data-stacked]>.knobs-wt-tstack{display:flex}
18576
+ .knobs-wt-trow{display:flex;flex-direction:column;gap:var(--_k-space-sibling);min-width:0;padding-block:var(--_k-space-title);box-shadow:inset 0 -1px 0 var(--_k-border-subtle)}
18577
+ .knobs-wt-trow:first-child{padding-top:0}
18578
+ .knobs-wt-trow:last-of-type{padding-bottom:0;box-shadow:none}
18579
+ .knobs-wt-tline{display:flex;align-items:baseline;justify-content:space-between;gap:var(--_k-space-title);min-width:0}
18580
+ .knobs-wt-tbadges{flex:none;display:flex;align-items:center;gap:var(--_k-space-inside)}
18581
+ .knobs-wt-ttitle{flex:1 1 auto;min-width:0;color:var(--_k-fg);font-weight:var(--_k-weight-label);overflow-wrap:anywhere}
18582
+ .knobs-wt-ttitle[data-mono]{font-family:var(--_k-mono);font-size:var(--_k-type-label)}
18583
+ .knobs-wt-tterms{display:grid;grid-template-columns:minmax(6ch,max-content) minmax(0,1fr);gap:var(--_k-space-inside) var(--_k-space-title);min-width:0}
18584
+ .knobs-wt-tterms dt{color:var(--_k-fg-3);font-size:var(--_k-type-small)}
18585
+ .knobs-wt-tterms dd{min-width:0;color:var(--_k-fg-2);overflow-wrap:anywhere}
18586
+ .knobs-wt-tterms dd[data-mono]{font-family:var(--_k-mono);font-size:var(--_k-type-label);color:var(--_k-fg)}
18587
+ .knobs-wt-tcaption{padding-top:var(--_k-space-sibling);color:var(--_k-fg-3);font-size:var(--_k-type-small)}
18588
+ @property --kw-fade-l{syntax:"<number>";inherits:false;initial-value:1}
18589
+ @property --kw-fade-r{syntax:"<number>";inherits:false;initial-value:1}
18590
+ @keyframes kw-fade-l{from{--kw-fade-l:1}to{--kw-fade-l:0}}
18591
+ @keyframes kw-fade-r{from{--kw-fade-r:0}to{--kw-fade-r:1}}
18592
+ .knobs-wt-table{max-height:520px;overflow:auto;overscroll-behavior-x:contain;outline:none;scrollbar-width:thin;scrollbar-color:transparent transparent;
18593
+ transition:scrollbar-color calc(var(--_k-hover-out) * 1ms)}
18594
+ .knobs-wt-table:is(:hover,:focus-visible){scrollbar-color:color-mix(in oklch,var(--_k-fg) 28%,transparent) transparent;transition-duration:calc(var(--_k-hover-in) * 1ms)}
18595
+ .knobs-wt-table:focus-visible{box-shadow:inset 0 0 0 2px var(--_k-focus);border-radius:calc(var(--_k-radius-panel) - 4px)}
18596
+ @supports (animation-timeline:scroll()){
18597
+ .knobs-wt-table{animation:knobs-fade-a linear both,knobs-fade-b linear both,kw-fade-l linear both,kw-fade-r linear both;animation-duration:auto;
18598
+ animation-timeline:scroll(self block),scroll(self block),scroll(self inline),scroll(self inline);animation-range:0 24px,calc(100% - 24px) 100%,0 8px,calc(100% - 8px) 100%;
18599
+ --kw-table-mask-y:linear-gradient(to bottom,#000 var(--kw-thead,0px),rgb(0 0 0/var(--knobs-fade-a)) var(--kw-thead,0px),#000 calc(var(--kw-thead,0px) + 20px),#000 calc(100% - 20px),rgb(0 0 0/var(--knobs-fade-b)));
18600
+ --kw-table-mask-x:linear-gradient(to right,rgb(0 0 0/var(--kw-fade-l)),#000 24px,#000 calc(100% - 24px),rgb(0 0 0/var(--kw-fade-r)));
18601
+ -webkit-mask-image:var(--kw-table-mask-y),var(--kw-table-mask-x);mask-image:var(--kw-table-mask-y),var(--kw-table-mask-x);-webkit-mask-composite:source-in;mask-composite:intersect}
17757
18602
  }
18603
+ .knobs-wt-table table{width:100%;margin:0;border-collapse:separate;border-spacing:0;font-size:var(--_k-type-body);line-height:var(--_k-leading-body);font-variant-numeric:tabular-nums;color:var(--_k-fg-2)}
18604
+ .knobs-wt-table caption{caption-side:bottom;padding-top:8px;text-align:left;color:var(--_k-fg-3);font-size:var(--_k-type-small)}
18605
+ .knobs-wt-table th{position:sticky;top:0;padding:0 12px 7px;text-align:left;vertical-align:bottom;font-weight:var(--_k-weight-label);font-size:var(--_k-type-small);color:var(--_k-fg-3);white-space:nowrap;
18606
+ background:var(--kw-under,var(--_k-pane-bg));box-shadow:inset 0 -1px 0 var(--_k-border)}
18607
+ .knobs-wt-table td{padding:8px 12px;vertical-align:baseline;box-shadow:inset 0 -1px 0 var(--_k-border)}
18608
+ .knobs-wt-table tr:last-child td{box-shadow:none}
18609
+ .knobs-wt-table :is(th,td):first-child{padding-left:0}
18610
+ .knobs-wt-table :is(th,td):last-child{padding-right:0}
18611
+ .knobs-wt-table td:first-child{color:var(--_k-fg)}
18612
+ .knobs-wt-table [data-align="right"]{text-align:right}
18613
+ .knobs-wt-table [data-align="center"]{text-align:center}
18614
+ .knobs-wt-table td[data-mono]{font-family:var(--_k-mono);font-size:var(--_k-type-label);color:var(--_k-fg);white-space:nowrap}
18615
+ .knobs-wt-table td[data-long]{min-width:24ch}
18616
+ .knobs-ui :is(.knobs-wt-table,.knobs-wt-tstack) a.knobs-wt-chip{text-decoration:none}
18617
+ .knobs-wt-ctone{color:var(--kw-tone)}
18618
+
18619
+ /* Chapters: a readout list under a video or compare: number, label, start; the selection pill
18620
+ on the one playing, its rows bled out so their text sits on the media's edge. */
18621
+ .knobs-wt-body:has(>.knobs-wt-chapters){display:flex;flex-direction:column;gap:var(--_k-space-title)}
18622
+ .knobs-wt-chapters{position:relative;display:flex;flex-direction:column;margin:0 calc(var(--_k-space-sibling) * -1)}
18623
+ .knobs-ui .knobs-wt-chapter{position:relative;display:grid;grid-template-columns:2ch minmax(0,1fr) auto;column-gap:var(--_k-space-sibling);align-items:center;height:32px;padding:0 var(--_k-space-sibling);
18624
+ border-radius:var(--_k-radius);color:var(--_k-fg-2);text-align:left;white-space:nowrap;cursor:pointer;transition:color calc(var(--_k-hover-out) * 1ms)}
18625
+ .knobs-ui .knobs-wt-chapter:is(:hover,:focus-visible){color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
18626
+ .knobs-ui .knobs-wt-chapter:focus-visible{box-shadow:inset 0 0 0 1px var(--_k-focus)}
18627
+ .knobs-wt-chapter-n{font-size:var(--_k-type-small);color:var(--_k-fg-3)}
18628
+ .knobs-wt-chapter-label{min-width:0;overflow:hidden;${fade2}}
18629
+ .knobs-wt-chapter-time{font:var(--_k-weight) var(--_k-type-small)/1 var(--_k-mono);color:var(--_k-fg-3)}
18630
+ .knobs-pick-copy :is(.knobs-wt-chapter-n,.knobs-wt-chapter-time){color:var(--_k-fg-2)}
18631
+ @media(prefers-reduced-motion:reduce){:is(.knobs-walkthrough,.knobs-wt-slot) :where(*,::before,::after){transition-duration:.15s!important;transition-property:opacity,background,color!important}}
18632
+ @media(prefers-reduced-motion:reduce){.knobs-wt-disc-content{transition:opacity .15s}}
18633
+ `;
17758
18634
 
17759
18635
  // src/ui/walkthrough-media.ts
17760
18636
  function injectWalkthroughStyles(doc) {
@@ -18283,7 +19159,7 @@ function mediaSlot(container, options) {
18283
19159
  }
18284
19160
  };
18285
19161
  }
18286
- var CHECK3 = svg('<path d="m3.5 8.4 3 3 6-6.6"/>');
19162
+ var CHECK3 = icon("check", { small: true });
18287
19163
 
18288
19164
  // src/ui/walkthrough.ts
18289
19165
  var el2 = (doc, tag, cls) => {
@@ -18303,14 +19179,18 @@ var lookOf2 = (o) => ({
18303
19179
  });
18304
19180
  var grid2 = (v) => Math.round(v * 64) / 64;
18305
19181
  var ICON = {
18306
- full: svg('<path d="M3 6.5V3h3.5M13 6.5V3H9.5M3 9.5V13h3.5M13 9.5V13H9.5"/>'),
18307
- exit: svg('<path d="M6.5 3v3.5H3M9.5 3v3.5H13M6.5 13V9.5H3M9.5 13V9.5H13"/>'),
18308
- pen: svg('<path d="M10.5 3.2 12.8 5.5 5.8 12.5 3 13l.5-2.8z"/><path d="M9.3 4.4l2.3 2.3"/>'),
18309
- mic: svg('<rect x="6" y="2.5" width="4" height="7" rx="2"/><path d="M3.8 8a4.2 4.2 0 0 0 8.4 0M8 12.2v1.8"/>'),
19182
+ full: icon("cornersOut"),
19183
+ exit: icon("cornersIn"),
19184
+ pen: icon("pencilSimple"),
19185
+ mic: icon("microphone"),
18310
19186
  stop: svg('<rect x="4.5" y="4.5" width="7" height="7" rx="1.5" fill="currentColor" stroke="none"/>'),
18311
- send: svg('<path d="M8 13V3.5M4 7.5 8 3.5l4 4"/>'),
18312
- close: svg('<path d="M4.5 4.5l7 7M11.5 4.5l-7 7"/>')
19187
+ send: icon("paperPlaneRight"),
19188
+ close: icon("x", { small: true }),
19189
+ back: icon("arrowLeft", { small: true }),
19190
+ out: icon("arrowUpRight", { small: true })
18313
19191
  };
19192
+ var DOCK_INSET = 12;
19193
+ var DOCK_GAP = 12;
18314
19194
  var CALM2 = springMotion(SPRINGS.calm);
18315
19195
  var SMOOTH = springMotion(SPRINGS.smooth);
18316
19196
  var BACK_MS = 4500;
@@ -18443,8 +19323,12 @@ function walkthrough(container, options) {
18443
19323
  let count = null;
18444
19324
  let fill = null;
18445
19325
  let tocEl = null;
18446
- let mark = null;
19326
+ let pill = null;
19327
+ let tocRows = [];
19328
+ let discs = [];
18447
19329
  let full = null;
19330
+ const noteMode = options.noteBar ?? "collapsed";
19331
+ let reserve = noteBarReserve(52, DOCK_INSET, DOCK_GAP);
18448
19332
  const viewOf = (key) => views.find((v) => v.step.key === key);
18449
19333
  const feedback = () => collectFeedback(page, state);
18450
19334
  const markdown = () => feedbackMarkdown(feedback(), page.verdicts);
@@ -18468,9 +19352,6 @@ function walkthrough(container, options) {
18468
19352
  else
18469
19353
  run();
18470
19354
  };
18471
- const pillY = new Spring(0, CALM2.omega, CALM2.zeta, 0.25);
18472
- const pillH = new Spring(0, CALM2.omega, CALM2.zeta, 0.25);
18473
- let pillPlaced = false;
18474
19355
  const scroll = {
18475
19356
  spring: new Spring(0, CALM2.omega, CALM2.zeta, 0.5),
18476
19357
  to: null,
@@ -18478,13 +19359,6 @@ function walkthrough(container, options) {
18478
19359
  };
18479
19360
  const motion = frameLoop(win, (dt) => {
18480
19361
  let more = false;
18481
- if (mark) {
18482
- pillY.step(dt);
18483
- pillH.step(dt);
18484
- mark.style.transform = `translateY(${grid2(pillY.x)}px)`;
18485
- mark.style.height = `${grid2(pillY.x + pillH.x) - grid2(pillY.x)}px`;
18486
- more ||= !pillY.resting || !pillH.resting;
18487
- }
18488
19362
  if (scroll.to && scroll.el) {
18489
19363
  scroll.spring.set(scrollTarget(scroll.to, scroll.el));
18490
19364
  scroll.spring.step(dt);
@@ -18548,21 +19422,12 @@ function walkthrough(container, options) {
18548
19422
  v.glyph.dataset.tone = v.step.ask === "none" ? "none" : verdict ? verdictTone(verdict, page.verdicts.findIndex((x) => x.id === verdict)) : done ? "accent" : "dim";
18549
19423
  };
18550
19424
  const placeMark = () => {
18551
- if (!mark || !tocEl)
19425
+ if (!pill || !tocEl)
18552
19426
  return;
19427
+ pill.update();
18553
19428
  const row = viewOf(current)?.toc;
18554
19429
  if (!row)
18555
- return void mark.removeAttribute("data-on");
18556
- mark.dataset.on = "";
18557
- if (!pillPlaced || reduced()) {
18558
- pillY.snap(row.offsetTop);
18559
- pillH.snap(row.offsetHeight);
18560
- pillPlaced = true;
18561
- } else {
18562
- pillY.set(row.offsetTop);
18563
- pillH.set(row.offsetHeight);
18564
- }
18565
- motion.wake();
19430
+ return;
18566
19431
  const top = row.offsetTop;
18567
19432
  const bottom = top + row.offsetHeight;
18568
19433
  if (top < tocEl.scrollTop + 8)
@@ -18579,6 +19444,9 @@ function walkthrough(container, options) {
18579
19444
  const v = viewOf(key);
18580
19445
  v?.card.setAttribute("data-current", "");
18581
19446
  v?.toc?.setAttribute("aria-current", "step");
19447
+ for (const r of tocRows)
19448
+ if (r.dataset.section)
19449
+ r.toggleAttribute("data-active", r.dataset.section === v?.step.section);
18582
19450
  if (key)
18583
19451
  record({ what: "step", step: key });
18584
19452
  placeMark();
@@ -18595,6 +19463,16 @@ function walkthrough(container, options) {
18595
19463
  else
18596
19464
  break;
18597
19465
  }
19466
+ const s = scrollerOf(root);
19467
+ if (s.scrollTop > 0 && s.scrollTop >= s.scrollHeight - s.clientHeight - 2) {
19468
+ const bottom = (s === doc.scrollingElement ? win.innerHeight : s.getBoundingClientRect().bottom) - reserve;
19469
+ for (let i = views.length - 1;i >= 0; i--) {
19470
+ if (views[i].card.getBoundingClientRect().top < bottom) {
19471
+ pick = views[i].step.key;
19472
+ break;
19473
+ }
19474
+ }
19475
+ }
18598
19476
  setCurrent(pick);
18599
19477
  };
18600
19478
  let scrollRaf = 0;
@@ -18637,7 +19515,12 @@ function walkthrough(container, options) {
18637
19515
  };
18638
19516
  const scrim = el2(doc, "div", "knobs-wt-scrim");
18639
19517
  scrim.addEventListener("click", () => setFull(false));
18640
- const fullRect = () => ({ x: 12, y: 12, w: win.innerWidth - 24, h: win.innerHeight - 24 });
19518
+ const fullRect = () => ({
19519
+ x: DOCK_INSET,
19520
+ y: DOCK_INSET,
19521
+ w: win.innerWidth - DOCK_INSET * 2,
19522
+ h: win.innerHeight - DOCK_INSET - reserve
19523
+ });
18641
19524
  const placeFull = (f) => {
18642
19525
  const a = f.hold.getBoundingClientRect();
18643
19526
  const b = fullRect();
@@ -18782,6 +19665,7 @@ function walkthrough(container, options) {
18782
19665
  ink.on = true;
18783
19666
  ink.layer.dataset.on = "";
18784
19667
  root.dataset.inking = "";
19668
+ bar.setOpen(true);
18785
19669
  bar.sync();
18786
19670
  };
18787
19671
  const voice = {
@@ -18821,6 +19705,7 @@ function walkthrough(container, options) {
18821
19705
  });
18822
19706
  voice.recorder = rec;
18823
19707
  voice.started = performance.now();
19708
+ bar.setOpen(true);
18824
19709
  rec.start();
18825
19710
  voice.timer = win.setInterval(() => bar.sync(), 250);
18826
19711
  bar.sync();
@@ -18847,7 +19732,10 @@ function walkthrough(container, options) {
18847
19732
  if (!v)
18848
19733
  return;
18849
19734
  backTo = key;
18850
- backPill.textContent = `← Back to ${v.step.number}`;
19735
+ backPill.innerHTML = ICON.back;
19736
+ const words = el2(doc, "span", "knobs-wt-back-label");
19737
+ words.textContent = `Back to ${plainMarkdown(v.step.title)}`;
19738
+ backPill.append(words);
18851
19739
  backPill.dataset.on = "";
18852
19740
  win.clearTimeout(backTimer);
18853
19741
  backTimer = win.setTimeout(hideBack, BACK_MS);
@@ -18879,7 +19767,41 @@ function walkthrough(container, options) {
18879
19767
  sendBtn.dataset.send = "";
18880
19768
  row.append(input, attach, fullBtn, penBtn, micBtn, sendBtn);
18881
19769
  box.append(on, row);
18882
- dock.append(backPill, box);
19770
+ const launch = el2(doc, "button", "knobs-wt-launch");
19771
+ launch.type = "button";
19772
+ launch.innerHTML = ICON.pen;
19773
+ launch.append("Note");
19774
+ const kbd = el2(doc, "kbd");
19775
+ kbd.textContent = "n";
19776
+ launch.append(kbd);
19777
+ launch.setAttribute("aria-label", "Write a note (n)");
19778
+ launch.addEventListener("click", () => setOpen(true, true));
19779
+ dock.dataset.mode = noteMode;
19780
+ dock.append(backPill, box, launch);
19781
+ let open = noteMode !== "collapsed";
19782
+ const setOpen = (on, focus = false) => {
19783
+ if (noteMode === "collapsed" && on !== open) {
19784
+ open = on;
19785
+ dock.toggleAttribute("data-open", on);
19786
+ box.inert = !on;
19787
+ launch.inert = on;
19788
+ syncReserve();
19789
+ }
19790
+ if (on && focus)
19791
+ input.focus({ preventScroll: true });
19792
+ };
19793
+ dock.toggleAttribute("data-open", open);
19794
+ box.inert = !open;
19795
+ launch.inert = open;
19796
+ const empty = () => !input.value.trim() && !voice.pending && !voice.recorder && !ink.on && !ink.paths.length;
19797
+ const onOutside = (e) => {
19798
+ if (open && noteMode === "collapsed" && !dock.contains(e.target) && empty())
19799
+ setOpen(false);
19800
+ };
19801
+ doc.addEventListener("pointerdown", onOutside, true);
19802
+ const ro = "ResizeObserver" in win ? new win.ResizeObserver(() => syncReserve()) : null;
19803
+ ro?.observe(box);
19804
+ ro?.observe(launch);
18883
19805
  const fit = () => {
18884
19806
  input.style.height = "auto";
18885
19807
  input.style.height = `${Math.min(140, input.scrollHeight)}px`;
@@ -18895,6 +19817,8 @@ function walkthrough(container, options) {
18895
19817
  } else if (e.key === "Escape") {
18896
19818
  e.preventDefault();
18897
19819
  input.blur();
19820
+ if (empty())
19821
+ setOpen(false);
18898
19822
  viewOf(current)?.card.focus({ preventScroll: true });
18899
19823
  }
18900
19824
  });
@@ -18914,13 +19838,13 @@ function walkthrough(container, options) {
18914
19838
  const sync = () => {
18915
19839
  const v = viewOf(current);
18916
19840
  const ctx = describeMedia(mediaContext(v));
18917
- const where = v ? `${v.step.number} ${v.step.title}` : "General note";
19841
+ const where = v ? plainMarkdown(v.step.title) : "General note";
18918
19842
  const text = ctx ? `${where} · ${ctx}` : where;
18919
19843
  if (text !== lastOn) {
18920
19844
  on.textContent = text;
18921
19845
  lastOn = text;
18922
19846
  }
18923
- input.placeholder = v ? `Note on ${v.step.number}… (n)` : "A note on the whole page… (n)";
19847
+ input.placeholder = v ? "A note on this step… (n)" : "A note on the whole page… (n)";
18924
19848
  const attached = `${voice.pending?.url ?? ""}|${ink.paths.length}`;
18925
19849
  if (attached !== lastAttached) {
18926
19850
  lastAttached = attached;
@@ -18959,14 +19883,45 @@ function walkthrough(container, options) {
18959
19883
  };
18960
19884
  return {
18961
19885
  dock,
19886
+ box,
19887
+ launch,
18962
19888
  input,
18963
19889
  sync,
19890
+ get open() {
19891
+ return open;
19892
+ },
19893
+ setOpen,
19894
+ empty,
19895
+ unobserve: () => {
19896
+ ro?.disconnect();
19897
+ doc.removeEventListener("pointerdown", onOutside, true);
19898
+ },
18964
19899
  clear() {
18965
19900
  input.value = "";
18966
19901
  fit();
18967
19902
  }
18968
19903
  };
18969
19904
  })();
19905
+ let padded = null;
19906
+ function syncReserve() {
19907
+ if (disposed)
19908
+ return;
19909
+ const h = (bar.open ? bar.box : bar.launch).offsetHeight;
19910
+ if (noteMode === "hidden")
19911
+ reserve = 0;
19912
+ else if (h)
19913
+ reserve = noteBarReserve(h, DOCK_INSET, DOCK_GAP);
19914
+ root.style.setProperty("--kw-reserve", `${reserve}px`);
19915
+ const s = scrollerOf(root);
19916
+ if (padded?.el !== s) {
19917
+ if (padded)
19918
+ padded.el.style.scrollPaddingBottom = padded.was;
19919
+ padded = { el: s, was: s.style.scrollPaddingBottom };
19920
+ }
19921
+ s.style.scrollPaddingBottom = `${reserve}px`;
19922
+ if (full)
19923
+ placeFull(full);
19924
+ }
18970
19925
  const ticker = win.setInterval(() => !doc.hidden && bar.sync(), 500);
18971
19926
  const send = (text = bar.input.value) => {
18972
19927
  if (voice.recorder)
@@ -19002,6 +19957,10 @@ function walkthrough(container, options) {
19002
19957
  syncProgress();
19003
19958
  emit();
19004
19959
  bar.sync();
19960
+ if (noteMode === "collapsed") {
19961
+ bar.input.blur();
19962
+ bar.setOpen(false);
19963
+ }
19005
19964
  if (v && options.advance !== false) {
19006
19965
  const to = adjacentStep(views.map((x) => x.step.key), v.step.key, 1);
19007
19966
  if (to && to !== v.step.key) {
@@ -19009,7 +19968,8 @@ function walkthrough(container, options) {
19009
19968
  setFull(false);
19010
19969
  go(to);
19011
19970
  showBack(v.step.key);
19012
- bar.input.focus({ preventScroll: true });
19971
+ if (noteMode === "docked")
19972
+ bar.input.focus({ preventScroll: true });
19013
19973
  }
19014
19974
  }
19015
19975
  };
@@ -19096,22 +20056,105 @@ function walkthrough(container, options) {
19096
20056
  row.addEventListener("transitionend", done, { once: true });
19097
20057
  win.setTimeout(done, 600);
19098
20058
  };
20059
+ const metaLine = (cls) => el2(doc, "div", `knobs-wt-metaline ${cls}`);
20060
+ const metaText = (line, text, cls) => {
20061
+ const s = el2(doc, "span", cls);
20062
+ s.textContent = text;
20063
+ line.append(s);
20064
+ return s;
20065
+ };
20066
+ const metaLink = (line, text, href) => {
20067
+ const a = el2(doc, "a");
20068
+ a.href = href;
20069
+ a.target = "_blank";
20070
+ a.rel = "noreferrer";
20071
+ a.textContent = text;
20072
+ a.insertAdjacentHTML("beforeend", ICON.out);
20073
+ line.append(a);
20074
+ return a;
20075
+ };
20076
+ const feedItem = (f, now) => {
20077
+ const li = el2(doc, "li", "knobs-wt-fi");
20078
+ li.dataset.kind = f.kind;
20079
+ const q = el2(doc, "div", "knobs-wt-fi-q");
20080
+ const meta = metaLine("knobs-wt-fi-meta");
20081
+ if (f.at !== null) {
20082
+ const when = metaText(meta, ago(f.at, now));
20083
+ when.title = new Date(f.at).toLocaleString();
20084
+ } else
20085
+ metaText(meta, f.kind === "qa" ? "Answer" : "Update");
20086
+ const step = f.step ? page.steps.find((s) => s.key === f.step) : undefined;
20087
+ if (step) {
20088
+ const section = page.sections.find((s) => s.id === step.section);
20089
+ const b = el2(doc, "button", "knobs-wt-fi-step");
20090
+ b.type = "button";
20091
+ b.textContent = section?.link?.label ?? plainMarkdown(step.title);
20092
+ b.title = `Go to “${plainMarkdown(step.title)}”`;
20093
+ b.addEventListener("click", () => go(step.key));
20094
+ meta.append(b);
20095
+ }
20096
+ if (f.status) {
20097
+ const id = f.status.toLowerCase();
20098
+ const shipped = /^(ship|shipped|merged|done)$/.test(id);
20099
+ const changes = /^(changes?|changes queued)$/.test(id);
20100
+ const s = metaText(meta, shipped ? "Shipped" : changes ? "Changes queued" : f.status);
20101
+ s.dataset.status = shipped ? "on" : changes ? "warn" : "";
20102
+ }
20103
+ q.append(meta);
20104
+ if (f.q)
20105
+ q.append(prose2(doc, "p", "knobs-wt-fi-ask", f.q));
20106
+ else
20107
+ q.append(prose2(doc, "p", "knobs-wt-fi-text", f.text));
20108
+ li.append(q);
20109
+ if (f.q || f.trail.length) {
20110
+ const a = el2(doc, "div", "knobs-wt-fi-a");
20111
+ if (f.q)
20112
+ a.append(prose2(doc, "div", "knobs-wt-fi-answer", f.text));
20113
+ if (f.trail.length) {
20114
+ const list = el2(doc, "ol", "knobs-wt-trail");
20115
+ for (const line of f.trail) {
20116
+ const i = line.indexOf(" · ");
20117
+ const row = el2(doc, "li");
20118
+ const words = el2(doc, "span", "knobs-wt-trail-text");
20119
+ words.textContent = i < 0 ? line : line.slice(i + 3);
20120
+ row.append(words);
20121
+ if (i >= 0) {
20122
+ const at = el2(doc, "span", "knobs-wt-trail-at");
20123
+ at.textContent = line.slice(0, i);
20124
+ row.append(at);
20125
+ }
20126
+ list.append(row);
20127
+ }
20128
+ discs.push(disclosure(a, { label: "Context", count: f.trail.length, content: list }));
20129
+ }
20130
+ li.append(a);
20131
+ }
20132
+ return li;
20133
+ };
19099
20134
  const build = () => {
19100
20135
  for (const v of views) {
19101
20136
  v.slots.forEach((s) => s.dispose());
19102
20137
  v.taste?.dispose();
19103
20138
  }
20139
+ for (const d of discs)
20140
+ d.dispose();
20141
+ discs = [];
20142
+ pill?.dispose();
20143
+ pill = null;
20144
+ tocRows = [];
19104
20145
  if (full) {
19105
20146
  full.p.snap(0);
19106
20147
  closeFull();
19107
20148
  }
19108
20149
  clearInk();
19109
20150
  views = [];
19110
- pillPlaced = false;
19111
20151
  root.textContent = "";
19112
20152
  root.removeAttribute("data-settled");
20153
+ root.dataset.rail = options.rail === false ? "none" : options.rail === "left" ? "left" : "right";
20154
+ root.toggleAttribute("data-cards", options.cards === true);
19113
20155
  const layout = el2(doc, "div", "knobs-wt-layout");
19114
20156
  const main = el2(doc, "div", "knobs-wt-main");
20157
+ const flow = el2(doc, "div", "knobs-wt-flow");
19115
20158
  const head = el2(doc, "header", "knobs-wt-head");
19116
20159
  const h1 = el2(doc, "h1", "knobs-wt-title");
19117
20160
  h1.textContent = page.title;
@@ -19138,42 +20181,27 @@ function walkthrough(container, options) {
19138
20181
  }
19139
20182
  head.append(meta);
19140
20183
  }
19141
- main.append(head);
20184
+ flow.append(head);
20185
+ let feedEl = null;
19142
20186
  if (page.feed.length) {
19143
- const feed = el2(doc, "section", "knobs-wt-feed");
19144
- feed.setAttribute("aria-label", "Latest");
19145
- const list = el2(doc, "ol", "knobs-wt-feed-list knobs-scroll-fade");
20187
+ feedEl = el2(doc, "section", "knobs-wt-feed");
20188
+ feedEl.id = "wt-latest";
20189
+ feedEl.setAttribute("aria-label", "Updates and answers");
20190
+ const fh = el2(doc, "header", "knobs-wt-feed-head");
20191
+ const h = el2(doc, "h2", "knobs-wt-feed-title");
20192
+ h.textContent = page.feed.some((f) => f.kind === "qa") ? "Updates & Q&A" : "Updates";
20193
+ const n = el2(doc, "span", "knobs-wt-feed-count");
20194
+ n.textContent = String(page.feed.length);
20195
+ fh.append(h, n);
20196
+ const list = el2(doc, "ol", "knobs-wt-feed-list");
19146
20197
  const now = Date.now();
19147
- for (const f of page.feed) {
19148
- const li = el2(doc, "li", "knobs-wt-fi");
19149
- li.dataset.kind = f.kind;
19150
- const when = el2(doc, "span", "knobs-wt-fi-when");
19151
- when.textContent = f.at === null ? "" : ago(f.at, now);
19152
- if (f.at !== null)
19153
- when.title = new Date(f.at).toLocaleString();
19154
- const kind = el2(doc, "span", "knobs-wt-fi-kind");
19155
- kind.textContent = f.kind === "qa" ? "Answer" : "Update";
19156
- const body = el2(doc, "div", "knobs-wt-fi-body");
19157
- if (f.q)
19158
- body.append(prose2(doc, "p", "knobs-wt-fi-q", f.q));
19159
- body.append(prose2(doc, "p", "knobs-wt-fi-text", f.text));
19160
- li.append(when, kind, body);
19161
- if (f.step) {
19162
- const target = f.step;
19163
- const link = el2(doc, "button", "knobs-wt-fi-step");
19164
- link.type = "button";
19165
- link.textContent = page.steps.find((s) => s.key === target)?.number ?? "";
19166
- link.title = "Go to the step";
19167
- link.addEventListener("click", () => go(target));
19168
- li.append(link);
19169
- }
19170
- list.append(li);
19171
- }
19172
- feed.append(list);
19173
- main.append(feed);
20198
+ for (const f of page.feed)
20199
+ list.append(feedItem(f, now));
20200
+ feedEl.append(fh, list);
20201
+ flow.append(feedEl);
19174
20202
  }
19175
20203
  let rail = null;
19176
- count = fill = tocEl = mark = null;
20204
+ count = fill = tocEl = null;
19177
20205
  if (options.rail !== false) {
19178
20206
  rail = el2(doc, "aside", "knobs-wt-rail");
19179
20207
  rail.setAttribute("aria-label", "Progress");
@@ -19185,8 +20213,17 @@ function walkthrough(container, options) {
19185
20213
  progress.append(count, meter);
19186
20214
  tocEl = el2(doc, "nav", "knobs-wt-toc");
19187
20215
  tocEl.setAttribute("aria-label", "Contents");
19188
- mark = el2(doc, "div", "knobs-wt-toc-mark");
19189
- tocEl.append(mark);
20216
+ const ns = page.sections.map((s) => s.kicker && s.link ? s.link.label : "");
20217
+ const nw = Math.max(0, ...ns.map((n) => n.length));
20218
+ tocEl.style.setProperty("--kw-toc-n", nw ? `${nw + 0.5}ch` : "0px");
20219
+ if (!nw)
20220
+ tocEl.dataset.bare = "";
20221
+ if (feedEl) {
20222
+ const group = el2(doc, "div", "knobs-wt-toc-group");
20223
+ const target = feedEl;
20224
+ group.append(tocRow("Latest", "", String(page.feed.length), false, () => target.scrollIntoView({ block: "start", behavior: reduced() ? "auto" : "smooth" })));
20225
+ tocEl.append(group);
20226
+ }
19190
20227
  rail.append(progress, tocEl, actions(true));
19191
20228
  if (options.keys !== false) {
19192
20229
  const keys = el2(doc, "div", "knobs-wt-keys");
@@ -19198,45 +20235,44 @@ function walkthrough(container, options) {
19198
20235
  const sec = el2(doc, "section", "knobs-wt-section");
19199
20236
  sec.id = `wt-${section.id}`;
19200
20237
  const sh = el2(doc, "header", "knobs-wt-shead");
19201
- if (section.link || section.status) {
19202
- const kicker = el2(doc, "div", "knobs-wt-kicker");
19203
- if (section.link) {
19204
- const a = el2(doc, "a");
19205
- a.href = section.link.href;
19206
- a.target = "_blank";
19207
- a.rel = "noreferrer";
19208
- a.textContent = `${section.link.label} ↗`;
19209
- kicker.append(a);
19210
- }
20238
+ if (section.kicker || section.link || section.status) {
20239
+ const meta = metaLine("knobs-wt-kicker");
20240
+ if (section.kicker)
20241
+ metaText(meta, section.kicker, "knobs-wt-kicker-lead");
20242
+ if (section.link)
20243
+ metaLink(meta, section.link.label, section.link.href);
19211
20244
  if (section.status) {
19212
20245
  const s = statusLook(section.status);
19213
- const chip = el2(doc, "span", "knobs-wt-chip");
19214
- chip.dataset.tone = s.tone;
19215
- chip.textContent = s.label;
19216
- kicker.append(chip);
20246
+ metaText(meta, s.label, "knobs-wt-status").dataset.tone = s.tone;
19217
20247
  }
19218
- sh.append(kicker);
20248
+ sh.append(meta);
19219
20249
  }
19220
- const h2 = el2(doc, "h2", "knobs-wt-stitle");
19221
- h2.textContent = section.title;
20250
+ const h2 = el2(doc, "h2", "knobs-wt-heading");
20251
+ h2.innerHTML = inlineMarkdown(section.title);
19222
20252
  sh.append(h2);
20253
+ const intro = el2(doc, "div", "knobs-wt-sintro");
20254
+ intro.append(sh);
19223
20255
  if (section.summary)
19224
- sh.append(prose2(doc, "p", "knobs-wt-ssum", section.summary));
19225
- sec.append(sh);
20256
+ intro.append(prose2(doc, "p", "knobs-wt-ssum", section.summary));
20257
+ sec.append(intro);
20258
+ let group = null;
19226
20259
  if (tocEl) {
19227
- const t = el2(doc, "div", "knobs-wt-toc-section");
19228
- t.textContent = section.link ? `${section.link.label} ${section.title}` : section.title;
19229
- t.title = section.title;
19230
- tocEl.append(t);
20260
+ group = el2(doc, "div", "knobs-wt-toc-group");
20261
+ const first = section.steps[0]?.key;
20262
+ const n = section.kicker && section.link ? section.link.label : "";
20263
+ group.append(tocRow(plainMarkdown(section.title), n, "", false, () => first ? go(first) : undefined, section.id));
20264
+ tocEl.append(group);
19231
20265
  }
19232
20266
  for (const step of section.steps) {
19233
- const v = stepView(step);
20267
+ const v = stepView(step, group);
19234
20268
  sec.append(v.card);
19235
20269
  views.push(v);
19236
20270
  }
19237
- main.append(sec);
20271
+ flow.append(sec);
19238
20272
  }
19239
- main.append(bar.dock);
20273
+ main.append(flow);
20274
+ if (noteMode !== "hidden")
20275
+ main.append(bar.dock);
19240
20276
  if (rail)
19241
20277
  layout.append(rail);
19242
20278
  layout.append(main);
@@ -19246,16 +20282,40 @@ function walkthrough(container, options) {
19246
20282
  for (const n of notesOn(state, v.step.key))
19247
20283
  renderNote(v, n);
19248
20284
  }
20285
+ if (tocEl)
20286
+ pill = selectionPill(tocEl, {
20287
+ selected: () => viewOf(current)?.toc ?? null,
20288
+ rows: () => tocRows
20289
+ });
19249
20290
  syncProgress();
19250
20291
  current = null;
19251
20292
  fromScroll();
19252
20293
  placeMark();
19253
20294
  bar.sync();
20295
+ syncReserve();
19254
20296
  win.requestAnimationFrame(() => win.requestAnimationFrame(() => {
19255
20297
  if (!disposed && !reduced())
19256
20298
  root.dataset.settled = "";
19257
20299
  }));
19258
20300
  };
20301
+ const tocRow = (label, n, end, sub, run, section) => {
20302
+ const b = el2(doc, "button", sub ? "knobs-wt-toc-row knobs-wt-toc-sub" : "knobs-wt-toc-row");
20303
+ b.type = "button";
20304
+ if (section)
20305
+ b.dataset.section = section;
20306
+ const ncol = el2(doc, "span", "knobs-wt-toc-n");
20307
+ ncol.textContent = n;
20308
+ const t = el2(doc, "span", "knobs-wt-toc-label");
20309
+ t.textContent = label;
20310
+ const e = el2(doc, "span", "knobs-wt-toc-end");
20311
+ e.textContent = end;
20312
+ b.title = label;
20313
+ b.append(ncol, t, e);
20314
+ b.addEventListener("mousedown", (ev) => ev.preventDefault());
20315
+ b.addEventListener("click", run);
20316
+ tocRows.push(b);
20317
+ return b;
20318
+ };
19259
20319
  const actions = (rail) => {
19260
20320
  const row = el2(doc, "div", "knobs-wt-actions");
19261
20321
  const btn = (text, run, primary = false) => {
@@ -19296,30 +20356,38 @@ function walkthrough(container, options) {
19296
20356
  toast("Couldn't reach the clipboard", { tone: "warn", look, doc });
19297
20357
  }
19298
20358
  };
19299
- const stepView = (step) => {
19300
- const card = el2(doc, "article", "knobs-wt-step knobs-surface");
20359
+ const stepView = (step, group) => {
20360
+ const card = el2(doc, "article", options.cards ? "knobs-wt-step knobs-surface" : "knobs-wt-step");
19301
20361
  card.id = `wt-${step.key.replace("/", "--")}`;
19302
20362
  card.tabIndex = -1;
19303
20363
  card.dataset.key = step.key;
19304
- card.setAttribute("aria-label", `${step.number} ${step.title}`);
20364
+ card.setAttribute("aria-label", plainMarkdown(step.title));
19305
20365
  const head = el2(doc, "header", "knobs-wt-step-head");
19306
- const num = el2(doc, "span", "knobs-wt-num");
19307
- num.textContent = step.number;
19308
20366
  const h3 = el2(doc, "h3", "knobs-wt-stepTitle");
19309
- h3.textContent = step.title;
19310
- head.append(num, h3);
19311
- card.append(head);
20367
+ h3.innerHTML = inlineMarkdown(step.title);
20368
+ head.append(h3);
19312
20369
  if (step.caption)
19313
- card.append(prose2(doc, "p", "knobs-wt-caption", step.caption));
20370
+ head.append(prose2(doc, "p", "knobs-wt-caption", step.caption));
20371
+ card.append(head);
19314
20372
  const slots = [];
19315
20373
  const onEvent = (e) => record({ ...e, step: step.key });
19316
20374
  let media = null;
19317
20375
  if (step.media.length) {
19318
- media = el2(doc, "div", "knobs-wt-media");
19319
- media.style.setProperty("--kw-n", String(Math.min(step.media.length, 3)));
19320
- for (const m of step.media)
19321
- slots.push(mediaSlot(media, { ...look, media: m, onEvent, steps: stepLinks }));
20376
+ const grid = (list) => {
20377
+ const g = el2(doc, "div", "knobs-wt-media");
20378
+ g.style.setProperty("--kw-n", String(Math.min(list.length, step.columns)));
20379
+ for (const m of list)
20380
+ slots.push(mediaSlot(g, { ...look, media: m, onEvent, steps: stepLinks }));
20381
+ return g;
20382
+ };
20383
+ const [first, ...rest] = step.media;
20384
+ media = grid(step.more ? [first] : step.media);
19322
20385
  card.append(media);
20386
+ if (step.more && rest.length) {
20387
+ const holder = el2(doc, "div", "knobs-wt-more");
20388
+ discs.push(disclosure(holder, { label: step.more, count: rest.length, content: grid(rest) }));
20389
+ card.append(holder);
20390
+ }
19323
20391
  }
19324
20392
  let taste = null;
19325
20393
  if (step.question)
@@ -19344,16 +20412,11 @@ function walkthrough(container, options) {
19344
20412
  });
19345
20413
  let toc = null;
19346
20414
  let glyph = null;
19347
- if (tocEl) {
19348
- toc = el2(doc, "button", "knobs-wt-toc-step");
19349
- toc.type = "button";
20415
+ if (group) {
20416
+ toc = tocRow(plainMarkdown(step.title), "", "", true, () => go(step.key));
19350
20417
  glyph = el2(doc, "span", "knobs-wt-toc-glyph");
19351
- const label = el2(doc, "span", "knobs-wt-toc-label");
19352
- label.textContent = step.title;
19353
- toc.title = `${step.number} ${step.title}`;
19354
- toc.append(glyph, label);
19355
- toc.addEventListener("click", () => go(step.key));
19356
- tocEl.append(toc);
20418
+ toc.querySelector(".knobs-wt-toc-end").append(glyph);
20419
+ group.append(toc);
19357
20420
  }
19358
20421
  return { step, card, media, notes, toc, glyph, taste, slots };
19359
20422
  };
@@ -19412,15 +20475,21 @@ function walkthrough(container, options) {
19412
20475
  setFull(false);
19413
20476
  return go(next);
19414
20477
  }
20478
+ if ((key === "Home" || key === "End") && views.length) {
20479
+ claim();
20480
+ if (full)
20481
+ setFull(false);
20482
+ return go(views[key === "Home" ? 0 : views.length - 1].step.key);
20483
+ }
19415
20484
  if (key === "ArrowLeft" || key === "ArrowRight") {
19416
20485
  const d = (key === "ArrowLeft" ? -1 : 1) * (e.shiftKey ? 2 : 0.5);
19417
20486
  if (seekBy(d))
19418
20487
  claim();
19419
20488
  return;
19420
20489
  }
19421
- if (key === "n") {
20490
+ if (key === "n" && noteMode !== "hidden") {
19422
20491
  claim();
19423
- bar.input.focus({ preventScroll: true });
20492
+ bar.setOpen(true, true);
19424
20493
  return;
19425
20494
  }
19426
20495
  const v = viewOf(current);
@@ -19499,6 +20568,12 @@ function walkthrough(container, options) {
19499
20568
  win.removeEventListener(type, stopScroll);
19500
20569
  doc.removeEventListener("keydown", onKey);
19501
20570
  unregisterHints();
20571
+ bar.unobserve();
20572
+ pill?.dispose();
20573
+ for (const d of discs)
20574
+ d.dispose();
20575
+ if (padded)
20576
+ padded.el.style.scrollPaddingBottom = padded.was;
19502
20577
  for (const v of views) {
19503
20578
  v.slots.forEach((s) => s.dispose());
19504
20579
  v.taste?.dispose();