@kitlangton/knobs 0.4.0-next.3 → 0.4.0-next.5

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.js CHANGED
@@ -595,6 +595,8 @@ var compareCss = `
595
595
  .knobs-cmp-stage{position:relative;flex:1;min-height:96px;overflow:hidden;border-radius:var(--_k-radius-panel);background:var(--_ku-cmp-stage);touch-action:pan-y;outline:none}
596
596
  .knobs-compare[data-frame="flat"] .knobs-cmp-stage{border-radius:0}
597
597
  .knobs-compare[data-mode="wipe"] .knobs-cmp-stage{cursor:ew-resize}
598
+ /* Videos: a click on the stage plays and pauses. */
599
+ .knobs-compare[data-video]:not([data-mode="wipe"],[data-zoomed]) .knobs-cmp-stage{cursor:pointer}
598
600
  /* Unzoomed, a finger still scrolls the page up and down; zoomed, the stage takes every gesture. */
599
601
  .knobs-compare[data-zoomed] .knobs-cmp-stage{cursor:grab;touch-action:none}
600
602
  .knobs-compare[data-panning],.knobs-compare[data-panning] *{cursor:grabbing!important}
@@ -604,14 +606,28 @@ var compareCss = `
604
606
  .knobs-cmp-canvas{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none}
605
607
  .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}
606
608
  .knobs-compare[data-renderer="gpu"] .knobs-cmp-media{opacity:0}
609
+ /* The media comes in with its first frames: a fade and a 4px blur on the default spring. */
610
+ .knobs-cmp-canvas,.knobs-compare[data-renderer="dom"] .knobs-cmp-media{transition:opacity calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default),filter calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default)}
611
+ .knobs-compare:not([data-ready]) :is(.knobs-cmp-canvas,.knobs-cmp-media){opacity:0;filter:blur(4px)}
607
612
  /* Glass: what's behind shows through, blurred, on the knobs tokens. */
608
613
  .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);
609
614
  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)))}
610
- .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 calc(var(--_k-hover-out) * 1ms)}
611
- /* The wipe: a hairline down the line with a raised grip at the pointer's height; a wider strip
612
- takes the hand and the keys (a slider). It fades with the mode; wiping, line and grip brighten. */
613
- .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)}
614
- .knobs-cmp-handle[data-on]{opacity:1;pointer-events:auto;transition:opacity .22s cubic-bezier(.2,.8,.2,1)}
615
+ /* ✗ Before · ✓ After: Before quieter (fg-3), After in full text. Neutral: no colour of its own. */
616
+ .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);
617
+ white-space:nowrap;pointer-events:none;will-change:transform}
618
+ .knobs-cmp-label[data-side="before"]{color:var(--_k-fg-3)}
619
+ .knobs-cmp-label[data-side="after"]{color:var(--_k-fg)}
620
+ /* Fusing, the mark folds away and the label is its name. */
621
+ .knobs-cmp-mark{flex:none;display:flex;justify-content:flex-end;overflow:hidden}
622
+ .knobs-cmp-mark svg{display:block;flex:none;width:12px;height:12px}
623
+ /* The fuse's seam: a blurred strip over where the sides meet, soft at both sides. */
624
+ .knobs-cmp-seam{position:absolute;left:0;top:0;width:8px;height:0;opacity:0;pointer-events:none;will-change:transform,opacity;
625
+ -webkit-mask-image:linear-gradient(to right,transparent,#000 35%,#000 65%,transparent);mask-image:linear-gradient(to right,transparent,#000 35%,#000 65%,transparent)}
626
+ /* The wipe: a hairline down the line with a raised grip in its middle (at the pointer's height
627
+ while dragged); a wider strip takes the hand and the keys (a slider). It fades in with the
628
+ fuse; wiping, line and grip brighten. */
629
+ .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,opacity}
630
+ .knobs-cmp-handle[data-on]{pointer-events:auto}
615
631
  .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)}
616
632
  .knobs-compare[data-dragging] .knobs-cmp-handle::before{background:var(--_ku-cmp-line)}
617
633
  .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);
@@ -643,6 +659,27 @@ var compareCss = `
643
659
  @media(prefers-reduced-motion:reduce){:is(.knobs-compare,.knobs-cmp-strip) :where(*,::before,::after){transition-duration:.12s!important}}
644
660
  `;
645
661
 
662
+ // src/ui/player-css.ts
663
+ var playerCss = `
664
+ :where(.knobs-player,.knobs-player *){box-sizing:border-box}
665
+ .knobs-ui.knobs-player{--_kp-stage:var(--_k-compare-stage);display:flex;flex-direction:column;gap:6px;width:100%;outline:none;
666
+ font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg);letter-spacing:normal;text-align:left}
667
+ :where(.knobs-surface,.knobs-pane:not([data-nested])>.knobs-pane-clip) .knobs-ui.knobs-player,.knobs-ui.knobs-player[data-nested]{--_kp-stage:var(--k-compare-stage,transparent)}
668
+ .knobs-player-frame{position:relative;width:100%;aspect-ratio:var(--kp-aspect,16/9);overflow:hidden;border-radius:var(--_k-radius-panel);background:var(--_kp-stage);cursor:pointer}
669
+ .knobs-player[data-frame="flat"] .knobs-player-frame{border-radius:0}
670
+ .knobs-player-media{position:absolute;inset:0;display:block;width:100%;height:100%;margin:0;padding:0;border:0;object-fit:contain;
671
+ opacity:0;filter:blur(4px);transition:opacity calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default),filter calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default)}
672
+ .knobs-player[data-ready] .knobs-player-media{opacity:1;filter:none}
673
+ .knobs-player-glyph{position:absolute;left:50%;top:50%;display:grid;place-items:center;width:44px;height:44px;margin:-22px 0 0 -22px;border-radius:50%;color:var(--_k-fg);pointer-events:none;
674
+ opacity:0;filter:blur(4px);scale:.96;
675
+ 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);
676
+ box-shadow:inset 0 0 0 .5px var(--_k-border),inset 0 1px 0 var(--_k-rim);
677
+ transition:opacity calc(var(--_k-motion-snappy-ms) * 1ms) var(--_k-motion-snappy),filter calc(var(--_k-motion-snappy-ms) * 1ms) var(--_k-motion-snappy),scale calc(var(--_k-motion-snappy-ms) * 1ms) var(--_k-motion-snappy)}
678
+ .knobs-player-glyph svg{width:16px;height:16px}
679
+ .knobs-player-glyph[data-flash]{opacity:1;filter:none;scale:1}
680
+ .knobs-player-line{flex:none;min-height:0}
681
+ `;
682
+
646
683
  // src/ui/overlay-css.ts
647
684
  var overlayCss = `
648
685
  .knobs-layers{position:fixed;inset:0;z-index:2147480005;pointer-events:none}
@@ -1331,6 +1368,22 @@ var init_schema = __esm(() => {
1331
1368
  source: css("var(--k-warn)"),
1332
1369
  doc: "Modified lines."
1333
1370
  },
1371
+ {
1372
+ name: "before",
1373
+ tier: "semantic",
1374
+ type: "color",
1375
+ group: "Data",
1376
+ source: css("var(--k-fg-3)"),
1377
+ doc: "Before in a comparison, and only Before: its label and side of the seam. Neutral unless a theme gives it a colour."
1378
+ },
1379
+ {
1380
+ name: "after",
1381
+ tier: "semantic",
1382
+ type: "color",
1383
+ group: "Data",
1384
+ source: css("var(--k-fg)"),
1385
+ doc: "After in a comparison, and only After."
1386
+ },
1334
1387
  {
1335
1388
  name: "series-1",
1336
1389
  tier: "semantic",
@@ -1421,6 +1474,150 @@ var init_schema = __esm(() => {
1421
1474
  source: value("288px"),
1422
1475
  doc: "Panel width."
1423
1476
  },
1477
+ {
1478
+ name: "space-inside",
1479
+ tier: "semantic",
1480
+ type: "length",
1481
+ group: "Space",
1482
+ source: css("var(--k-unit)"),
1483
+ doc: "Parts of one thing: an icon and its label, a meta line and its title."
1484
+ },
1485
+ {
1486
+ name: "space-sibling",
1487
+ tier: "semantic",
1488
+ type: "length",
1489
+ group: "Space",
1490
+ source: css("calc(var(--k-unit) * 2)"),
1491
+ doc: "Siblings in a group: buttons in a row, a list item's lines."
1492
+ },
1493
+ {
1494
+ name: "space-title",
1495
+ tier: "semantic",
1496
+ type: "length",
1497
+ group: "Space",
1498
+ source: css("calc(var(--k-unit) * 3)"),
1499
+ doc: "A heading and what it heads."
1500
+ },
1501
+ {
1502
+ name: "space-group",
1503
+ tier: "semantic",
1504
+ type: "length",
1505
+ group: "Space",
1506
+ source: css("calc(var(--k-unit) * 12)"),
1507
+ doc: "Groups inside a section (prose, video, diff), stacked media, feed items (a hairline in the middle)."
1508
+ },
1509
+ {
1510
+ name: "space-section",
1511
+ tier: "semantic",
1512
+ type: "length",
1513
+ group: "Space",
1514
+ source: css("calc(var(--k-unit) * 24)"),
1515
+ doc: "Sections of a page (the intro, the updates, each PR), a hairline in the middle."
1516
+ },
1517
+ {
1518
+ name: "space-column",
1519
+ tier: "semantic",
1520
+ type: "length",
1521
+ group: "Space",
1522
+ source: css("calc(var(--k-unit) * 6)"),
1523
+ doc: "Columns: a contents rail and the page, media side by side."
1524
+ },
1525
+ {
1526
+ name: "pad-page",
1527
+ tier: "semantic",
1528
+ type: "length",
1529
+ group: "Space",
1530
+ source: css("calc(var(--k-unit) * 6)"),
1531
+ doc: "A page's edge; 16px under 640px wide."
1532
+ },
1533
+ {
1534
+ name: "pad-card",
1535
+ tier: "semantic",
1536
+ type: "length",
1537
+ group: "Space",
1538
+ source: css("calc(var(--k-unit) * 3)"),
1539
+ doc: "Inside a card or popover."
1540
+ },
1541
+ {
1542
+ name: "type-heading",
1543
+ tier: "semantic",
1544
+ type: "length",
1545
+ group: "Type",
1546
+ source: value("20px"),
1547
+ doc: "A page's title, weight 500."
1548
+ },
1549
+ {
1550
+ name: "leading-heading",
1551
+ tier: "semantic",
1552
+ type: "length",
1553
+ group: "Type",
1554
+ source: value("28px"),
1555
+ doc: "Line height for type-heading."
1556
+ },
1557
+ {
1558
+ name: "type-title",
1559
+ tier: "semantic",
1560
+ type: "length",
1561
+ group: "Type",
1562
+ source: value("16px"),
1563
+ doc: "Section titles (under their kicker), weight 500."
1564
+ },
1565
+ {
1566
+ name: "leading-title",
1567
+ tier: "semantic",
1568
+ type: "length",
1569
+ group: "Type",
1570
+ source: value("24px"),
1571
+ doc: "Line height for type-title."
1572
+ },
1573
+ {
1574
+ name: "type-body",
1575
+ tier: "semantic",
1576
+ type: "length",
1577
+ group: "Type",
1578
+ source: value("13px"),
1579
+ doc: "Prose, questions, list rows, step titles; 400, or 500 for emphasis."
1580
+ },
1581
+ {
1582
+ name: "leading-body",
1583
+ tier: "semantic",
1584
+ type: "length",
1585
+ group: "Type",
1586
+ source: value("20px"),
1587
+ doc: "Line height for type-body."
1588
+ },
1589
+ {
1590
+ name: "type-label",
1591
+ tier: "semantic",
1592
+ type: "length",
1593
+ group: "Type",
1594
+ source: value("12px"),
1595
+ doc: "Mono inside body text: code and paths."
1596
+ },
1597
+ {
1598
+ name: "leading-label",
1599
+ tier: "semantic",
1600
+ type: "length",
1601
+ group: "Type",
1602
+ source: value("16px"),
1603
+ doc: "Line height for type-label."
1604
+ },
1605
+ {
1606
+ name: "type-small",
1607
+ tier: "semantic",
1608
+ type: "length",
1609
+ group: "Type",
1610
+ source: value("11px"),
1611
+ doc: "Meta, controls, keys, timestamps; mono inside labels."
1612
+ },
1613
+ {
1614
+ name: "leading-small",
1615
+ tier: "semantic",
1616
+ type: "length",
1617
+ group: "Type",
1618
+ source: value("16px"),
1619
+ doc: "Line height for type-small."
1620
+ },
1424
1621
  {
1425
1622
  name: "font-size",
1426
1623
  tier: "semantic",
@@ -2385,16 +2582,18 @@ function injectTokens(doc) {
2385
2582
  style.textContent = rootCss;
2386
2583
  doc.head.append(style);
2387
2584
  }
2388
- var ROOT_CLASS = "knobs-root", extras, spring, rootCss;
2585
+ var ROOT_CLASS = "knobs-root", extras, narrow, spring, rootCss;
2389
2586
  var init_root = __esm(() => {
2390
2587
  init_knobs();
2391
2588
  init_css();
2392
2589
  init_glide();
2393
2590
  init_motion();
2394
2591
  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))}`;
2592
+ narrow = `@media(max-width:639.98px){:where(.${ROOT_CLASS}){--_k-pad-page:var(--k-pad-page,calc(var(--_k-unit) * 4))}}`;
2395
2593
  spring = `:where(:root,:host){--k-fill-ease:${DEFAULT_LINEAR.easing};--k-fill-ms:${DEFAULT_LINEAR.ms}}`;
2396
2594
  rootCss = `${tokensCss(knobs)}
2397
2595
  ${extras}
2596
+ ${narrow}
2398
2597
  ${spring}
2399
2598
  ${DRIVERS}
2400
2599
  ${motionCss}`;
@@ -2684,6 +2883,12 @@ var css2 = `
2684
2883
  --_ku-pane-bg and --_ku-pane-ring so this reaches them. */
2685
2884
  :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;
2686
2885
  --_ku-divider:var(--_k-split-gutter);--_kd-bg:transparent}
2886
+ /* What draws its box without those: a picker's card keeps its fill as the highlight but drops its
2887
+ ring; a timeline's track drops its well for a hairline under the lane; an inline panel sheds its
2888
+ card (--k-chrome 0). A full-window step is the one raised exception (it is the surface). */
2889
+ :is(.knobs-surface,.knobs-pane:not([data-nested])>.knobs-pane-clip) .knobs-picker-card,.knobs-ui[data-nested] .knobs-picker-card{box-shadow:none}
2890
+ :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)}
2891
+ :where(.knobs-surface,.knobs-pane:not([data-nested])>.knobs-pane-clip) .knobs-panel.knobs-inline:not(.knobs-page){--k-chrome:0}
2687
2892
  /* Only the pieces' own elements take their type and resets: what you put in a pane is yours. */
2688
2893
  .knobs-ui{box-sizing:border-box}
2689
2894
  :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}
@@ -2909,8 +3114,6 @@ var css2 = `
2909
3114
  .knobs-tl-hover{position:absolute;left:0;top:0;bottom:0;width:1px;margin-left:-.5px;background:var(--_k-border-strong);transform:translateX(-100px);pointer-events:none;opacity:0;transition:opacity .15s}
2910
3115
  .knobs-tl-track:hover .knobs-tl-hover{opacity:1}
2911
3116
  .knobs-timeline[data-scrubbing] .knobs-tl-hover{opacity:0}
2912
- @media(prefers-reduced-motion:reduce){.knobs-timeline :where(*,::before,::after){transition:none!important}}
2913
- @media(prefers-reduced-motion:reduce){:is(.knobs-tree,.knobs-split>.knobs-divider,.knobs-pane-head,.knobs-pane-rail) :where(*,::before,::after){transition:none!important}}
2914
3117
  .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}
2915
3118
  .knobs-tl-tag[data-on]{opacity:1}
2916
3119
  /* The lane (variant: "lane"): one slim track of ticks under a compare or a video; markers are
@@ -2926,7 +3129,26 @@ var css2 = `
2926
3129
  .knobs-timeline[data-variant="lane"] .knobs-tl-head::before{top:0}
2927
3130
  .knobs-timeline[data-variant="lane"] .knobs-tl-head::after{content:none}
2928
3131
  .knobs-timeline[data-variant="lane"] .knobs-tl-tag{top:2px;bottom:auto}
2929
- `, uiCss = () => aliased(css2 + readoutCss + overlayCss + bentoCss + activityCss + pickerCss + compareCss + selectionPillCss) + rolling_css_default, RESTYLED = ".knobs-ui,.knobs-panel,.knobs-chip,.knobs-notice", lookDefaults, tune = (s, m) => {
3132
+ /* The line (variant: "line"): a player's progress under its media. One 2px line, fg-3 with fg
3133
+ filling it, a segment per chapter; it thickens under the pointer and while scrubbing. The
3134
+ chapter and time under the pointer float over it; nothing else shows. */
3135
+ .knobs-ui.knobs-timeline[data-variant="line"]{height:auto;min-height:0;padding:0;gap:0}
3136
+ .knobs-timeline[data-variant="line"] .knobs-tl-transport{display:none}
3137
+ .knobs-ui.knobs-timeline[data-variant="line"] .knobs-tl-track{height:16px;overflow:visible;border-radius:var(--_k-radius);background:transparent;box-shadow:none;cursor:pointer}
3138
+ .knobs-timeline[data-variant="line"] :is(.knobs-tl-ruler,.knobs-tl-lane,.knobs-tl-hover,.knobs-tl-head,.knobs-tl-rlabel){display:none}
3139
+ .knobs-tl-segs{position:absolute;left:0;right:0;top:50%;height:2px;margin-top:-1px;pointer-events:none;transition:scale calc(var(--_k-hover-out) * 1ms) var(--_ku-ease)}
3140
+ .knobs-tl-seg{position:absolute;left:0;top:0;height:100%;overflow:hidden;border-radius:1px;background:var(--_k-fg-3);will-change:transform}
3141
+ .knobs-tl-seg-fill{position:absolute;inset:0;background:var(--_k-fg);transform-origin:0 0;will-change:transform}
3142
+ .knobs-timeline[data-variant="line"] .knobs-tl-track:hover .knobs-tl-segs,.knobs-timeline[data-variant="line"][data-scrubbing] .knobs-tl-segs{scale:1 2}
3143
+ .knobs-timeline[data-variant="line"] .knobs-tl-range{top:3px;bottom:3px;border-radius:3px}
3144
+ .knobs-timeline[data-variant="line"]:focus-visible .knobs-tl-track,.knobs-timeline[data-variant="line"][data-keyboard]:focus .knobs-tl-track{background:transparent;box-shadow:0 0 0 2px color-mix(in srgb,var(--_k-accent) 60%,transparent)}
3145
+ .knobs-timeline[data-variant="line"] .knobs-tl-tag{top:auto;bottom:calc(100% + 2px);display:flex;gap:6px;padding:0 6px;border-radius:calc(var(--_k-radius) * 999);background:color-mix(in srgb,var(--_k-bg) 72%,transparent);
3146
+ -webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);box-shadow:inset 0 0 0 .5px var(--_k-border)}
3147
+ .knobs-tl-tag-label{color:var(--_k-fg-2)}
3148
+ .knobs-tl-tag-label:empty{display:none}
3149
+ @media(prefers-reduced-motion:reduce){.knobs-timeline :where(*,::before,::after){transition:none!important}}
3150
+ @media(prefers-reduced-motion:reduce){:is(.knobs-tree,.knobs-split>.knobs-divider,.knobs-pane-head,.knobs-pane-rail) :where(*,::before,::after){transition:none!important}}
3151
+ `, uiCss = () => aliased(css2 + readoutCss + overlayCss + bentoCss + activityCss + pickerCss + compareCss + playerCss + selectionPillCss) + rolling_css_default, RESTYLED = ".knobs-ui,.knobs-panel,.knobs-chip,.knobs-notice", lookDefaults, tune = (s, m) => {
2930
3152
  s.omega = m.omega;
2931
3153
  s.zeta = m.zeta;
2932
3154
  return s;
@@ -5305,6 +5527,17 @@ function currentMarker(markers, t, fps) {
5305
5527
  best = m;
5306
5528
  return best;
5307
5529
  }
5530
+ function minutes(t) {
5531
+ const s = Math.max(0, Math.floor(t + 0.000001));
5532
+ const h = Math.floor(s / 3600);
5533
+ const m = Math.floor(s % 3600 / 60);
5534
+ const ss = String(s % 60).padStart(2, "0");
5535
+ return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${ss}` : `${m}:${ss}`;
5536
+ }
5537
+ function chapterBounds(times, duration) {
5538
+ const inside = times.filter((t) => t > 0.000001 && t < duration - 0.000001);
5539
+ return [0, ...[...new Set(inside)].sort((a, b) => a - b), duration];
5540
+ }
5308
5541
  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;
5309
5542
  var init_timeline_model = __esm(() => {
5310
5543
  SECOND_STEPS = [0.1, 0.2, 0.25, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 900, 1800, 3600];
@@ -5322,7 +5555,8 @@ function timeline(container, options) {
5322
5555
  let speed = options.speed ?? 1;
5323
5556
  let repeat = options.repeat ?? true;
5324
5557
  const arrows = options.arrows ?? "frames";
5325
- const lane = options.variant === "lane";
5558
+ const line = options.variant === "line";
5559
+ const lane = line || options.variant === "lane";
5326
5560
  let time = Math.min(duration, Math.max(0, options.time ?? 0));
5327
5561
  let playing = false;
5328
5562
  let markers = [...options.markers ?? []];
@@ -5357,7 +5591,7 @@ function timeline(container, options) {
5357
5591
  root.setAttribute("aria-keyshortcuts", "Space ArrowLeft ArrowRight , . Home End [ ] I O M");
5358
5592
  applyLook2(root, options);
5359
5593
  if (lane)
5360
- root.dataset.variant = "lane";
5594
+ root.dataset.variant = line ? "line" : "lane";
5361
5595
  const transport = el("div", "knobs-tl-transport");
5362
5596
  const playBtn = el("button", "knobs-tl-play");
5363
5597
  playBtn.type = "button";
@@ -5370,6 +5604,10 @@ function timeline(container, options) {
5370
5604
  const hover = el("div", "knobs-tl-hover");
5371
5605
  const tag = el("span", "knobs-tl-tag");
5372
5606
  tag.setAttribute("aria-hidden", "true");
5607
+ const tagLabel = el("span", "knobs-tl-tag-label");
5608
+ const tagTime = el("span", "knobs-tl-tag-time");
5609
+ tag.append(tagLabel, tagTime);
5610
+ const segs = el("div", "knobs-tl-segs");
5373
5611
  const head = el("div", "knobs-tl-head");
5374
5612
  head.id = `${id}-head`;
5375
5613
  head.setAttribute("role", "slider");
@@ -5377,11 +5615,13 @@ function timeline(container, options) {
5377
5615
  head.setAttribute("aria-valuemin", "0");
5378
5616
  head.setAttribute("aria-valuemax", String(duration));
5379
5617
  track.append(canvas, markerLane, hover, head, tag);
5618
+ if (line)
5619
+ track.insertBefore(segs, markerLane);
5380
5620
  root.append(transport, track);
5381
5621
  container.append(root);
5382
5622
  let width = 1;
5383
5623
  let height = 1;
5384
- const fitView = () => clampView({ start: 0, span: duration }, duration, { pad: 0.02 });
5624
+ const fitView = () => clampView({ start: 0, span: duration }, duration, { pad: line ? 0 : 0.02 });
5385
5625
  let target = fitView();
5386
5626
  const vStart = new Spring(target.start, INSTANT.omega, 1, 0.00001);
5387
5627
  const vSpan = new Spring(target.span, INSTANT.omega, 1, 0.00001);
@@ -5390,13 +5630,13 @@ function timeline(container, options) {
5390
5630
  const xOf = (t) => (t - vStart.x) * pxPerSec();
5391
5631
  const tOf = (x) => vStart.x + x / pxPerSec();
5392
5632
  const minSpan = () => Math.max(4 / fps, 0.05);
5393
- const retarget = (next, motion = INSTANT) => {
5394
- target = clampView(next, duration, { pad: 0.04, minSpan: minSpan() });
5633
+ const retarget = (next, motion = INSTANT, hand = false) => {
5634
+ target = clampView(next, duration, { pad: line ? 0 : 0.04, minSpan: minSpan() });
5395
5635
  for (const s of [vStart, vSpan]) {
5396
5636
  s.omega = motion.omega;
5397
5637
  s.zeta = motion.zeta;
5398
5638
  }
5399
- if (reducedMotion(win) && motion === CALM) {
5639
+ if (hand || reducedMotion(win) && motion === CALM) {
5400
5640
  vStart.snap(target.start);
5401
5641
  vSpan.snap(target.span);
5402
5642
  } else {
@@ -5422,6 +5662,8 @@ function timeline(container, options) {
5422
5662
  };
5423
5663
  let drawn = "";
5424
5664
  const drawRuler = () => {
5665
+ if (line)
5666
+ return;
5425
5667
  const v = view();
5426
5668
  const key = `${v.start.toFixed(6)}:${v.span.toFixed(6)}:${width}:${height}:${units}:${fps}`;
5427
5669
  if (key === drawn)
@@ -5509,6 +5751,8 @@ function timeline(container, options) {
5509
5751
  n.title = `${m.label ? `${m.label} · ` : ""}${clock(m.time)}`;
5510
5752
  }
5511
5753
  placeMarkers();
5754
+ if (line)
5755
+ syncSegments();
5512
5756
  };
5513
5757
  const placeMarkers = () => {
5514
5758
  for (const m of markers) {
@@ -5517,6 +5761,36 @@ function timeline(container, options) {
5517
5761
  n.style.transform = `translateX(${xOf(m.time).toFixed(2)}px)`;
5518
5762
  }
5519
5763
  };
5764
+ let bounds = [];
5765
+ const segEls = [];
5766
+ const syncSegments = () => {
5767
+ bounds = chapterBounds(markers.map((m) => m.time), duration);
5768
+ while (segEls.length > bounds.length - 1)
5769
+ segEls.pop().el.remove();
5770
+ while (segEls.length < bounds.length - 1) {
5771
+ const n = el("i", "knobs-tl-seg");
5772
+ const fill = el("b", "knobs-tl-seg-fill");
5773
+ n.append(fill);
5774
+ segs.append(n);
5775
+ segEls.push({ el: n, fill });
5776
+ }
5777
+ placeSegments();
5778
+ };
5779
+ const placeSegments = () => {
5780
+ if (!line)
5781
+ return;
5782
+ const last = segEls.length - 1;
5783
+ segEls.forEach((s, i) => {
5784
+ const a = bounds[i];
5785
+ const b = bounds[i + 1];
5786
+ const x0 = xOf(a) + (i > 0 ? SEG_GAP / 2 : 0);
5787
+ const x1 = xOf(b) - (i < last ? SEG_GAP / 2 : 0);
5788
+ s.el.style.transform = `translateX(${x0.toFixed(2)}px)`;
5789
+ s.el.style.width = `${Math.max(0, x1 - x0).toFixed(2)}px`;
5790
+ const k = Math.min(1, Math.max(0, (shownHead - a) / Math.max(0.000000001, b - a)));
5791
+ s.fill.style.transform = `scaleX(${k.toFixed(4)})`;
5792
+ });
5793
+ };
5520
5794
  const syncRanges = () => {
5521
5795
  const keep = new Set(ranges.map((r) => r.id));
5522
5796
  for (const [k, n] of rangeEls)
@@ -5533,7 +5807,7 @@ function timeline(container, options) {
5533
5807
  b.dataset.edge = "end";
5534
5808
  const label = el("span", "knobs-tl-rlabel");
5535
5809
  n.append(label, a, b);
5536
- track.insertBefore(n, markerLane);
5810
+ track.insertBefore(n, line ? segs : markerLane);
5537
5811
  rangeEls.set(r.id, n);
5538
5812
  if (built)
5539
5813
  enter(n);
@@ -5585,6 +5859,7 @@ function timeline(container, options) {
5585
5859
  let currentId = null;
5586
5860
  const renderHead = () => {
5587
5861
  head.style.transform = `translateX(${xOf(shownHead).toFixed(2)}px)`;
5862
+ placeSegments();
5588
5863
  head.setAttribute("aria-valuenow", time.toFixed(3));
5589
5864
  head.setAttribute("aria-valuetext", `${time.toFixed(2)} seconds, frame ${toFrame(time, fps)}`);
5590
5865
  const cur = currentMarker(markers, time, fps)?.id ?? null;
@@ -5848,14 +6123,25 @@ function timeline(container, options) {
5848
6123
  setTime(s.t, "scrub", s.shown, press);
5849
6124
  root.toggleAttribute("data-stuck", s.stop !== null);
5850
6125
  };
6126
+ let pointerX = null;
6127
+ const zoomAtHand = (factor) => zoom(factor, pointerX === null ? time : target.start + pointerX / width * target.span);
5851
6128
  const showTag = (x) => {
5852
6129
  const at = Math.min(duration, Math.max(0, tOf(x)));
5853
- const text = `${at.toFixed(2)} s`;
5854
- if (tag.textContent !== text)
5855
- tag.textContent = text;
6130
+ const text = line ? minutes(at) : `${at.toFixed(2)} s`;
6131
+ if (tagTime.textContent !== text)
6132
+ tagTime.textContent = text;
6133
+ const label = line ? currentMarker(markers, at, fps)?.label ?? "" : "";
6134
+ if (tagLabel.textContent !== label)
6135
+ tagLabel.textContent = label;
5856
6136
  const cx = Math.min(width, Math.max(0, x));
5857
- const flip = cx > width - tag.offsetWidth - 12;
5858
- tag.style.transform = `translateX(${cx.toFixed(1)}px) translateX(${flip ? "calc(-100% - 4px)" : "4px"})`;
6137
+ if (line) {
6138
+ const w = tag.offsetWidth;
6139
+ const left = Math.min(Math.max(0, width - w), Math.max(0, cx - w / 2));
6140
+ tag.style.transform = `translateX(${left.toFixed(1)}px)`;
6141
+ } else {
6142
+ const flip = cx > width - tag.offsetWidth - 12;
6143
+ tag.style.transform = `translateX(${cx.toFixed(1)}px) translateX(${flip ? "calc(-100% - 4px)" : "4px"})`;
6144
+ }
5859
6145
  tag.dataset.on = "";
5860
6146
  };
5861
6147
  const hideTag = () => delete tag.dataset.on;
@@ -5916,6 +6202,7 @@ function timeline(container, options) {
5916
6202
  });
5917
6203
  track.addEventListener("pointermove", (e) => {
5918
6204
  const x = localX(e);
6205
+ pointerX = x;
5919
6206
  hover.style.transform = `translateX(${x.toFixed(1)}px)`;
5920
6207
  if (!drag || drag.kind === "scrub")
5921
6208
  showTag(x);
@@ -5989,6 +6276,7 @@ function timeline(container, options) {
5989
6276
  track.addEventListener("pointercancel", endDrag);
5990
6277
  track.addEventListener("lostpointercapture", endDrag);
5991
6278
  track.addEventListener("pointerleave", () => {
6279
+ pointerX = null;
5992
6280
  if (drag)
5993
6281
  return;
5994
6282
  hover.style.transform = "translateX(-100px)";
@@ -5996,7 +6284,7 @@ function timeline(container, options) {
5996
6284
  });
5997
6285
  track.addEventListener("dblclick", (e) => {
5998
6286
  if (lane)
5999
- return;
6287
+ return fit();
6000
6288
  const markerEl = e.target.closest(".knobs-tl-marker");
6001
6289
  if (markerEl)
6002
6290
  return rename(markerEl.dataset.id);
@@ -6007,18 +6295,40 @@ function timeline(container, options) {
6007
6295
  rename(m.id);
6008
6296
  });
6009
6297
  track.addEventListener("wheel", (e) => {
6010
- if (lane)
6298
+ const zooming = e.ctrlKey || e.metaKey;
6299
+ if (zooming && pinching)
6300
+ return e.preventDefault();
6301
+ const sideways = Math.abs(e.deltaX) > Math.abs(e.deltaY);
6302
+ if (lane && !zooming && !sideways)
6011
6303
  return;
6012
6304
  e.preventDefault();
6013
6305
  const scale = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? height : 1;
6014
- if (e.ctrlKey || e.metaKey) {
6306
+ if (zooming) {
6015
6307
  const at = target.start + localX(e) / width * target.span;
6016
- retarget(zoomView(target, Math.exp(-e.deltaY * scale * (e.ctrlKey ? 0.01 : 0.002)), at));
6308
+ const k = Math.exp(-e.deltaY * scale * (e.ctrlKey ? 0.01 : 0.002));
6309
+ retarget(zoomView(target, k, at), INSTANT, lane);
6017
6310
  } else {
6018
- const d = (Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY) * scale;
6019
- retarget({ ...target, start: target.start + d / width * target.span });
6311
+ const d = (sideways ? e.deltaX : e.deltaY) * scale;
6312
+ retarget({ ...target, start: target.start + d / width * target.span }, INSTANT, lane);
6020
6313
  }
6021
6314
  }, { passive: false });
6315
+ let pinchScale = 1;
6316
+ let pinching = false;
6317
+ const onGesture = (e) => {
6318
+ const g = e;
6319
+ e.preventDefault();
6320
+ if (e.type === "gesturestart")
6321
+ pinchScale = 1;
6322
+ pinching = e.type !== "gestureend";
6323
+ if (e.type !== "gesturechange")
6324
+ return;
6325
+ const s = g.scale ?? 1;
6326
+ const x = g.clientX !== undefined ? localX({ clientX: g.clientX }) : width / 2;
6327
+ retarget(zoomView(target, s / pinchScale, target.start + x / width * target.span), INSTANT, true);
6328
+ pinchScale = s;
6329
+ };
6330
+ for (const type of ["gesturestart", "gesturechange", "gestureend"])
6331
+ track.addEventListener(type, onGesture);
6022
6332
  root.addEventListener("keydown", (e) => {
6023
6333
  if (e.target !== root || e.metaKey || e.ctrlKey) {
6024
6334
  if ((e.metaKey || e.ctrlKey) && (e.key === "=" || e.key === "-" || e.key === "0") && e.target === root) {
@@ -6026,7 +6336,7 @@ function timeline(container, options) {
6026
6336
  if (e.key === "0")
6027
6337
  fit();
6028
6338
  else
6029
- zoom(e.key === "=" ? 1.5 : 1 / 1.5);
6339
+ zoomAtHand(e.key === "=" ? 1.5 : 1 / 1.5);
6030
6340
  }
6031
6341
  return;
6032
6342
  }
@@ -6108,18 +6418,12 @@ function timeline(container, options) {
6108
6418
  return;
6109
6419
  case "=":
6110
6420
  case "+":
6111
- if (lane)
6112
- return;
6113
6421
  claim();
6114
- return zoom(1.5);
6422
+ return zoomAtHand(1.5);
6115
6423
  case "-":
6116
- if (lane)
6117
- return;
6118
6424
  claim();
6119
- return zoom(1 / 1.5);
6425
+ return zoomAtHand(1 / 1.5);
6120
6426
  case "0":
6121
- if (lane)
6122
- return;
6123
6427
  claim();
6124
6428
  return fit();
6125
6429
  }
@@ -6335,7 +6639,7 @@ function timeline(container, options) {
6335
6639
  };
6336
6640
  return api;
6337
6641
  }
6338
- var RULER = 22, INSTANT, CALM, GLIDE, uid2 = 0, PLAY = '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5 3.6v8.8c0 .5.5.8.9.5l6.6-4.4c.4-.3.4-.8 0-1L5.9 3.1c-.4-.3-.9 0-.9.5Z" fill="currentColor"/></svg>', PAUSE = '<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="4" y="3.2" width="2.8" height="9.6" rx="1" fill="currentColor"/><rect x="9.2" y="3.2" width="2.8" height="9.6" rx="1" fill="currentColor"/></svg>';
6642
+ var RULER = 22, SEG_GAP = 3, INSTANT, CALM, GLIDE, uid2 = 0, PLAY = '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5 3.6v8.8c0 .5.5.8.9.5l6.6-4.4c.4-.3.4-.8 0-1L5.9 3.1c-.4-.3-.9 0-.9.5Z" fill="currentColor"/></svg>', PAUSE = '<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="4" y="3.2" width="2.8" height="9.6" rx="1" fill="currentColor"/><rect x="9.2" y="3.2" width="2.8" height="9.6" rx="1" fill="currentColor"/></svg>';
6339
6643
  var init_timeline = __esm(() => {
6340
6644
  init_rgba();
6341
6645
  init_motion();
@@ -6407,15 +6711,28 @@ function compareLayout(box, before, after) {
6407
6711
  const shape = after.w > 0 && after.h > 0 ? after : before;
6408
6712
  const pair = letterbox({ w: shape.w * 2, h: shape.h }, box);
6409
6713
  const half = pair.w / 2;
6714
+ const fused = { x: pair.x + half / 2, y: pair.y, w: half, h: pair.h };
6410
6715
  return {
6411
6716
  side: {
6412
6717
  before: letterbox(before, { x: pair.x, y: pair.y, w: half, h: pair.h }),
6413
6718
  after: letterbox(after, { x: pair.x + half, y: pair.y, w: half, h: pair.h }),
6414
6719
  split: pair.x + half
6415
6720
  },
6416
- overlay: { before: letterbox(before, box), after: letterbox(after, box) }
6721
+ overlay: { before: letterbox(before, fused), after: letterbox(after, fused) }
6417
6722
  };
6418
6723
  }
6724
+ function fuseFrame(L, before, after, k, wipe) {
6725
+ const t = Math.min(1, Math.max(0, k));
6726
+ const o = L.overlay;
6727
+ const left = Math.min(o.before.x, o.after.x);
6728
+ const line = wipeX({ x: left, y: 0, w: Math.max(o.before.x + o.before.w, o.after.x + o.after.w) - left, h: 0 }, wipe);
6729
+ const cut = L.side.split + (line - L.side.split) * t;
6730
+ const contact = Math.max(0, Math.min(cut - after.x, before.x + before.w - cut));
6731
+ const feather = Math.min(contact, 1 + FUSE.feather * Math.sin(Math.PI * t));
6732
+ const overlap = before.x + before.w - after.x;
6733
+ const seam = t > 0.001 && overlap >= 0 ? Math.min(1, Math.max(0, overlap / FUSE.seamFade)) * Math.min(1, t * 4) : 0;
6734
+ return { cut, feather, seam, blur: t < 0.995 ? FUSE.blur * (1 - t) : 0 };
6735
+ }
6419
6736
  function clampZoom(r) {
6420
6737
  const w = Math.min(1, Math.max(1 / ZOOM_MAX, r.w));
6421
6738
  const u = Math.min(1 - w, Math.max(0, r.u));
@@ -6501,7 +6818,15 @@ function stripPeaks(diffs, o = {}) {
6501
6818
  });
6502
6819
  return out;
6503
6820
  }
6504
- 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), seekTime = (t, fps, duration) => frameMidpoint(frameOf(t, fps), fps, duration), 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({
6821
+ 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), seekTime = (t, fps, duration) => frameMidpoint(frameOf(t, fps), fps, duration), sameFrame = (a, b, fps) => frameOf(a, fps) === frameOf(b, fps), SEEK_IDLE, FUSE, labelFold = (fold, mark, blur = 3) => {
6822
+ const f = Math.min(1, Math.max(0, fold));
6823
+ const open = 1 - f;
6824
+ return {
6825
+ width: mark * open,
6826
+ opacity: Math.min(1, Math.max(0, (open - 0.2) / 0.8)),
6827
+ blur: blur * Math.min(1, Math.max(0, (f - 0.25) / 0.75))
6828
+ };
6829
+ }, 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({
6505
6830
  u: r.u - dx / Math.max(1, base.w) * r.w,
6506
6831
  v: r.v - dy / Math.max(1, base.h) * r.h,
6507
6832
  w: r.w
@@ -6524,6 +6849,7 @@ var init_compare_model = __esm(() => {
6524
6849
  MODE_KEYS = { side: "s", wipe: "w", difference: "d" };
6525
6850
  VIDEO = /\.(mp4|m4v|mov|webm|ogv|mkv)$/i;
6526
6851
  SEEK_IDLE = { seeking: false, pending: null };
6852
+ FUSE = { feather: 24, blur: 4, seamFade: 48 };
6527
6853
  ZOOM_FIT = { u: 0, v: 0, w: 1, h: 1 };
6528
6854
  });
6529
6855
 
@@ -7418,11 +7744,6 @@ fn pm(c: vec4f) -> vec4f { return vec4f(c.rgb * c.a, c.a); }
7418
7744
 
7419
7745
  fn composite(mode: u32, p: vec2f, a: vec4f, b: vec4f) -> vec4f {
7420
7746
  switch mode {
7421
- case 1u: {
7422
- let s = max(u.wipe.y, 0.5);
7423
- let m = smoothstep(u.wipe.x - s, u.wipe.x + s, p.x);
7424
- return pm(vec4f(mix(a.rgb, b.rgb, m), mix(a.a, b.a, m)));
7425
- }
7426
7747
  case 2u: {
7427
7748
  let d = abs(a.rgb - b.rgb);
7428
7749
  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);
@@ -7431,8 +7752,10 @@ fn composite(mode: u32, p: vec2f, a: vec4f, b: vec4f) -> vec4f {
7431
7752
  }
7432
7753
  default: {}
7433
7754
  }
7434
- // Side by side: each side in its own place, Before over After where they meet.
7435
- return vec4f(a.rgb * a.a + b.rgb * b.a * (1.0 - a.a), a.a + b.a * (1.0 - a.a));
7755
+ // Side by side, fusing and wiping: Before over After, cut at the seam with its feather.
7756
+ let s = max(u.wipe.y, 0.5);
7757
+ let ka = a.a * (1.0 - smoothstep(u.wipe.x - s, u.wipe.x + s, p.x));
7758
+ return vec4f(a.rgb * ka + b.rgb * b.a * (1.0 - ka), ka + b.a * (1.0 - ka));
7436
7759
  }
7437
7760
 
7438
7761
  @fragment fn fs(@builtin(position) q: vec4f) -> @location(0) vec4f {
@@ -7834,6 +8157,196 @@ var init_compare_strip = __esm(() => {
7834
8157
  DIFF = { w: 32, h: 18 };
7835
8158
  });
7836
8159
 
8160
+ // src/ui/player.ts
8161
+ function player(container, options) {
8162
+ const doc = container.ownerDocument;
8163
+ const el = (tag, cls) => {
8164
+ const n = doc.createElement(tag);
8165
+ n.className = cls;
8166
+ return n;
8167
+ };
8168
+ const look = {
8169
+ ...options.preset ? { preset: options.preset } : {},
8170
+ ...options.theme ? { theme: options.theme } : {},
8171
+ ...options.appearance ? { appearance: options.appearance } : {}
8172
+ };
8173
+ const root = el("div", "knobs-player");
8174
+ root.setAttribute("role", "group");
8175
+ root.setAttribute("aria-label", options.label ?? "Video");
8176
+ applyLook2(root, options);
8177
+ const frame = el("div", "knobs-player-frame");
8178
+ frame.style.setProperty("--kp-aspect", String(options.aspect ?? 16 / 9));
8179
+ const v = el("video", "knobs-player-media");
8180
+ v.muted = options.muted ?? true;
8181
+ v.defaultMuted = v.muted;
8182
+ v.playsInline = true;
8183
+ v.preload = "metadata";
8184
+ v.setAttribute("playsinline", "");
8185
+ if (options.poster)
8186
+ v.poster = options.poster;
8187
+ if (options.captions) {
8188
+ const track = doc.createElement("track");
8189
+ track.kind = "captions";
8190
+ track.label = "Captions";
8191
+ track.srclang = doc.documentElement.lang || "en";
8192
+ track.default = true;
8193
+ track.src = options.captions;
8194
+ v.append(track);
8195
+ }
8196
+ const glyph = playGlyph(doc);
8197
+ frame.append(v, glyph.element);
8198
+ const host = el("div", "knobs-player-line");
8199
+ root.append(frame, host);
8200
+ container.append(root);
8201
+ const markers = (options.chapters ?? []).map((c, i) => ({
8202
+ id: c.id ?? `c${i}`,
8203
+ time: c.start,
8204
+ label: c.label
8205
+ }));
8206
+ const tl = timeline(host, {
8207
+ ...look,
8208
+ variant: "line",
8209
+ duration: 1,
8210
+ fps: options.fps ?? 30,
8211
+ markers,
8212
+ repeat: options.loop ?? true,
8213
+ arrows: { step: 5, shift: 10 },
8214
+ label: options.label ? `${options.label} time` : "Video time",
8215
+ ...options.hints === false ? { hints: false } : {}
8216
+ });
8217
+ const onFrame = () => root.setAttribute("data-ready", "");
8218
+ let pending = null;
8219
+ const seekVideo = (t) => {
8220
+ if (v.readyState < 1)
8221
+ return;
8222
+ if (v.seeking)
8223
+ pending = t;
8224
+ else
8225
+ v.currentTime = t;
8226
+ };
8227
+ let reported = 0;
8228
+ const onSeeked = () => {
8229
+ if (pending !== null) {
8230
+ const t = pending;
8231
+ pending = null;
8232
+ v.currentTime = t;
8233
+ return;
8234
+ }
8235
+ if (!v.paused || tl.playing || Math.abs(v.currentTime - reported) < 0.1)
8236
+ return;
8237
+ reported = v.currentTime;
8238
+ options.onSeek?.(reported);
8239
+ };
8240
+ const onMeta = () => {
8241
+ onFrame();
8242
+ if (v.videoWidth)
8243
+ frame.style.setProperty("--kp-aspect", String(v.videoWidth / v.videoHeight));
8244
+ if (Number.isFinite(v.duration))
8245
+ tl.update({ duration: v.duration });
8246
+ };
8247
+ v.addEventListener("loadedmetadata", onMeta);
8248
+ v.addEventListener("loadeddata", onFrame);
8249
+ v.addEventListener("seeked", onSeeked);
8250
+ const unlisten = tl.listen({
8251
+ onSeek(t, source) {
8252
+ if (source === "play") {
8253
+ if (Math.abs(v.currentTime - t) > 0.25)
8254
+ v.currentTime = t;
8255
+ return;
8256
+ }
8257
+ if (Math.abs(v.currentTime - t) < 1 / (options.fps ?? 30))
8258
+ return;
8259
+ seekVideo(t);
8260
+ },
8261
+ onPlay(on) {
8262
+ if (on)
8263
+ v.play().catch(() => tl.pause());
8264
+ else {
8265
+ v.pause();
8266
+ reported = tl.time;
8267
+ }
8268
+ root.toggleAttribute("data-playing", on);
8269
+ options.onPlay?.(on, tl.time);
8270
+ },
8271
+ onScrub(on) {
8272
+ if (on)
8273
+ v.pause();
8274
+ }
8275
+ });
8276
+ tl.drive(() => {
8277
+ if (v.readyState < 1)
8278
+ return Number.NaN;
8279
+ if (v.seeking)
8280
+ return tl.time;
8281
+ return v.ended ? Number.POSITIVE_INFINITY : v.currentTime;
8282
+ });
8283
+ const onClick = (e) => {
8284
+ if (e.button !== 0)
8285
+ return;
8286
+ tl.toggle();
8287
+ glyph.flash(tl.playing);
8288
+ tl.focus();
8289
+ };
8290
+ frame.addEventListener("click", onClick);
8291
+ v.src = options.src;
8292
+ const api = {
8293
+ element: root,
8294
+ video: v,
8295
+ timeline: tl,
8296
+ get time() {
8297
+ return tl.time;
8298
+ },
8299
+ get playing() {
8300
+ return tl.playing;
8301
+ },
8302
+ get duration() {
8303
+ return Number.isFinite(v.duration) ? v.duration : 0;
8304
+ },
8305
+ play: () => tl.play(),
8306
+ pause: () => tl.pause(),
8307
+ toggle: () => tl.toggle(),
8308
+ seek: (t, o) => tl.seek(t, o?.glide ? { glide: true } : undefined),
8309
+ seekBy: (d) => tl.seek(Math.min(tl.duration, Math.max(0, tl.time + d)), { glide: true }),
8310
+ focus: () => tl.focus(),
8311
+ dispose() {
8312
+ unlisten();
8313
+ glyph.dispose();
8314
+ tl.drive(null);
8315
+ tl.dispose();
8316
+ v.pause();
8317
+ v.removeEventListener("loadedmetadata", onMeta);
8318
+ v.removeEventListener("loadeddata", onFrame);
8319
+ v.removeEventListener("seeked", onSeeked);
8320
+ v.removeAttribute("src");
8321
+ v.load();
8322
+ root.remove();
8323
+ }
8324
+ };
8325
+ return api;
8326
+ }
8327
+ function playGlyph(doc) {
8328
+ const element = doc.createElement("span");
8329
+ element.className = "knobs-player-glyph";
8330
+ element.setAttribute("aria-hidden", "true");
8331
+ let timer = 0;
8332
+ const win = doc.defaultView;
8333
+ return {
8334
+ element,
8335
+ flash(playing) {
8336
+ element.innerHTML = playing ? PLAY : PAUSE;
8337
+ element.setAttribute("data-flash", "");
8338
+ win.clearTimeout(timer);
8339
+ timer = win.setTimeout(() => element.removeAttribute("data-flash"), FLASH_MS);
8340
+ },
8341
+ dispose: () => win.clearTimeout(timer)
8342
+ };
8343
+ }
8344
+ var FLASH_MS = 450;
8345
+ var init_player = __esm(() => {
8346
+ init_look();
8347
+ init_timeline();
8348
+ });
8349
+
7837
8350
  // src/ui/compare.ts
7838
8351
  function compare(container, options) {
7839
8352
  const doc = container.ownerDocument;
@@ -7930,6 +8443,19 @@ function compare(container, options) {
7930
8443
  const labels = [el("div", "knobs-cmp-label"), el("div", "knobs-cmp-label")];
7931
8444
  labels[0].dataset.side = "before";
7932
8445
  labels[1].dataset.side = "after";
8446
+ const setLabel = (index, text) => {
8447
+ const l = labels[index];
8448
+ if (!l.firstElementChild) {
8449
+ const mark = el("span", "knobs-cmp-mark");
8450
+ mark.innerHTML = icon(index === 0 ? "x" : "check", { small: true });
8451
+ l.append(mark, el("span", "knobs-cmp-name"));
8452
+ }
8453
+ const name = l.lastElementChild;
8454
+ if (name.textContent !== text)
8455
+ name.textContent = text;
8456
+ };
8457
+ const seam = el("div", "knobs-cmp-seam");
8458
+ seam.setAttribute("aria-hidden", "true");
7933
8459
  const handle = el("div", "knobs-cmp-handle");
7934
8460
  handle.setAttribute("role", "slider");
7935
8461
  handle.setAttribute("aria-label", "Wipe");
@@ -7939,7 +8465,8 @@ function compare(container, options) {
7939
8465
  grip.setAttribute("aria-hidden", "true");
7940
8466
  handle.append(grip);
7941
8467
  const note = el("div", "knobs-cmp-note");
7942
- stage.append(canvas, split, labels[0], labels[1], handle, zoomPill, note);
8468
+ const glyph = playGlyph(doc);
8469
+ stage.append(canvas, split, seam, labels[0], labels[1], handle, zoomPill, note, glyph.element);
7943
8470
  root.append(bar, stage);
7944
8471
  const wantsStrip = options.strip === true;
7945
8472
  let tl = bound;
@@ -7954,7 +8481,7 @@ function compare(container, options) {
7954
8481
  root.append(host);
7955
8482
  tl = timeline(host, {
7956
8483
  ...look,
7957
- variant: "lane",
8484
+ variant: "line",
7958
8485
  duration: 1,
7959
8486
  fps: fps(),
7960
8487
  arrows: { step: 0.5, shift: 2 },
@@ -7975,12 +8502,13 @@ function compare(container, options) {
7975
8502
  let toMode = mode;
7976
8503
  const wipeLine = new Spring(wipe, WIPE_CATCH.omega, WIPE_CATCH.zeta, 0.0001);
7977
8504
  let wipeCatching = false;
7978
- let gripY = null;
8505
+ const gripAt = new Spring(0.5, LAYOUT.omega, LAYOUT.zeta, 0.0001);
7979
8506
  let loupeHeld = false;
7980
8507
  let pointer = null;
7981
8508
  let drag = null;
7982
8509
  const touches = new Map;
7983
8510
  let pinch = null;
8511
+ let press = null;
7984
8512
  const zu = new Spring(0, ZOOM.omega, ZOOM.zeta, 0.00001);
7985
8513
  const zv = new Spring(0, ZOOM.omega, ZOOM.zeta, 0.00001);
7986
8514
  const zw = new Spring(1, ZOOM.omega, ZOOM.zeta, 0.00001);
@@ -8267,6 +8795,8 @@ function compare(container, options) {
8267
8795
  for (const s of sides) {
8268
8796
  const st = s.el.style;
8269
8797
  st.clipPath = "";
8798
+ st.maskImage = "";
8799
+ st.setProperty("-webkit-mask-image", "");
8270
8800
  st.mixBlendMode = "";
8271
8801
  st.opacity = "";
8272
8802
  s.shown = null;
@@ -8493,43 +9023,65 @@ function compare(container, options) {
8493
9023
  viewSent = key;
8494
9024
  options.onView({ zoom: clampZoom(z), loop: l });
8495
9025
  };
8496
- const labelsIn = (m, r, wr, wx) => {
8497
- if (m === "wipe") {
8498
- const at = seamLabels(wx, wr.x, wr.x + wr.w, labelW[0], labelW[1], INSET);
8499
- return [at.before, at.after];
8500
- }
8501
- const ax = r.after.x + INSET + (r.after.w - labelW[1] - INSET * 2) * lay.x;
8502
- return [
8503
- { x: r.before.x + INSET, opacity: 1 },
8504
- { x: ax, opacity: 1 }
8505
- ];
9026
+ const differenceWeight = () => {
9027
+ const f = Math.min(1, Math.max(0, fade.x));
9028
+ return (fromMode === "difference" ? 1 - f : 0) + (toMode === "difference" ? f : 0);
8506
9029
  };
8507
9030
  const placeGrip = (wr) => {
8508
- const gy = gripTop(gripY ?? wr.y + wr.h / 2, wr, GRIP.h) - wr.y;
9031
+ const gy = gripTop(wr.y + gripAt.x * wr.h, wr, GRIP.h) - wr.y;
8509
9032
  grip.style.transform = `translate3d(0,${gy.toFixed(2)}px,0)`;
8510
9033
  };
9034
+ let folded = 0;
8511
9035
  const draw = () => {
8512
9036
  dirty = false;
9037
+ if (!root.hasAttribute("data-ready") && sides.every((x) => x.shown || x.failed))
9038
+ root.setAttribute("data-ready", "");
8513
9039
  const r = layoutNow();
8514
9040
  const wr = wipeRect(r.L.overlay);
8515
- const wx = wipeX(wr, wipeLine.x);
8516
- const k = lay.x;
8517
- const a = labelsIn(fromMode, r, wr, wx);
8518
- const b = labelsIn(toMode, r, wr, wx);
8519
- const f = Math.min(1, Math.max(0, fade.x));
9041
+ const k = Math.min(1, Math.max(0, lay.x));
9042
+ const fu = fuseFrame(r.L, r.before, r.after, k, wipeLine.x);
9043
+ const dw = differenceWeight();
9044
+ const fusing = 1 - dw;
9045
+ folded = k * fusing;
9046
+ const fold = labelFold(folded, MARK.w);
9047
+ const w = labelW.map((x) => x - (MARK.w - fold.width) - MARK.gap * folded);
9048
+ const ride = seamLabels(fu.cut, r.before.x, r.after.x + r.after.w, w[0], w[1], INSET);
9049
+ const apart = [r.before.x + INSET, r.after.x + INSET];
9050
+ const corner = [r.before.x + INSET, r.after.x + r.after.w - labelW[1] - INSET];
9051
+ const rides = [ride.before, ride.after];
8520
9052
  const ys = [r.before.y + INSET, r.after.y + INSET];
8521
9053
  for (const i of [0, 1]) {
8522
- const x = a[i].x + (b[i].x - a[i].x) * f;
8523
- labels[i].style.transform = `translate(${x.toFixed(2)}px,${ys[i].toFixed(2)}px)`;
8524
- labels[i].style.opacity = (a[i].opacity + (b[i].opacity - a[i].opacity) * f).toFixed(3);
9054
+ const fused = apart[i] + (rides[i].x - apart[i]) * k;
9055
+ const x = fused + (corner[i] - fused) * dw;
9056
+ const opacity = (1 - k + k * rides[i].opacity) * fusing + dw;
9057
+ const l = labels[i];
9058
+ l.style.transform = `translate(${x.toFixed(2)}px,${ys[i].toFixed(2)}px)`;
9059
+ l.style.opacity = opacity.toFixed(3);
9060
+ const mark = l.firstElementChild;
9061
+ if (mark) {
9062
+ mark.style.width = `${fold.width.toFixed(2)}px`;
9063
+ mark.style.marginRight = `${(-MARK.gap * folded).toFixed(2)}px`;
9064
+ mark.style.opacity = fold.opacity.toFixed(3);
9065
+ mark.style.filter = fold.blur > 0.05 ? `blur(${fold.blur.toFixed(2)}px)` : "";
9066
+ }
8525
9067
  }
8526
9068
  const top = Math.min(r.L.side.before.y, r.L.side.after.y);
8527
9069
  split.style.transform = `translate(${r.L.side.split.toFixed(2)}px,${top.toFixed(2)}px)`;
8528
9070
  split.style.height = `${Math.max(0, r.L.side.after.h).toFixed(2)}px`;
8529
9071
  split.style.opacity = String(Math.max(0, 1 - k * 2));
9072
+ const seamOn = fu.seam * fusing;
9073
+ const blur = fu.blur > 0.01 ? `blur(${fu.blur.toFixed(2)}px)` : "none";
9074
+ seam.style.transform = `translate(${fu.cut.toFixed(2)}px,${wr.y.toFixed(2)}px) translateX(-50%)`;
9075
+ seam.style.width = `${(8 + 2 * fu.feather).toFixed(2)}px`;
9076
+ seam.style.height = `${Math.max(0, wr.h).toFixed(2)}px`;
9077
+ seam.style.opacity = (blur === "none" ? 0 : seamOn).toFixed(3);
9078
+ seam.style.backdropFilter = blur;
9079
+ seam.style.setProperty("-webkit-backdrop-filter", blur);
8530
9080
  handle.toggleAttribute("data-on", mode === "wipe");
8531
- handle.style.transform = `translate(${wx.toFixed(2)}px,${wr.y.toFixed(2)}px)`;
9081
+ handle.style.transform = `translate(${fu.cut.toFixed(2)}px,${wr.y.toFixed(2)}px)`;
8532
9082
  handle.style.height = `${Math.max(0, wr.h).toFixed(2)}px`;
9083
+ handle.style.opacity = seamOn.toFixed(3);
9084
+ handle.style.filter = blur;
8533
9085
  placeGrip(wr);
8534
9086
  handle.setAttribute("aria-valuenow", String(Math.round(wipe * 100)));
8535
9087
  handle.setAttribute("aria-valuetext", `${Math.round(wipe * 100)}% Before`);
@@ -8542,24 +9094,27 @@ function compare(container, options) {
8542
9094
  if (pending)
8543
9095
  return;
8544
9096
  if (renderer === "dom") {
8545
- const place = (n, rect, clip, cutAt) => {
9097
+ const cut = mode === "difference" ? null : fu;
9098
+ const place = (n, rect, clip, before) => {
8546
9099
  const st = n.style;
8547
9100
  st.transform = `translate(${rect.x.toFixed(2)}px,${rect.y.toFixed(2)}px)`;
8548
9101
  st.width = `${rect.w.toFixed(2)}px`;
8549
9102
  st.height = `${rect.h.toFixed(2)}px`;
9103
+ const edge = before && cut ? cut.cut + cut.feather : Number.POSITIVE_INFINITY;
8550
9104
  const t = Math.max(0, clip.y - rect.y);
8551
9105
  const l = Math.max(0, clip.x - rect.x);
8552
9106
  const b = Math.max(0, rect.y + rect.h - (clip.y + clip.h));
8553
- const right = Math.max(0, rect.x + rect.w - Math.min(clip.x + clip.w, cutAt));
9107
+ const right = Math.max(0, rect.x + rect.w - Math.min(clip.x + clip.w, edge));
8554
9108
  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)` : "";
9109
+ const mask = before && cut && cut.feather > 0.5 ? `linear-gradient(to right,#000 ${(cut.cut - cut.feather - rect.x).toFixed(2)}px,transparent ${(edge - rect.x).toFixed(2)}px)` : "";
9110
+ st.maskImage = mask;
9111
+ st.setProperty("-webkit-mask-image", mask);
8555
9112
  };
8556
- const cut = mode === "wipe" ? wx : Number.POSITIVE_INFINITY;
8557
9113
  for (const s of sides) {
8558
9114
  const rect = s.index === 0 ? r.zBefore : r.zAfter;
8559
9115
  const clip = s.index === 0 ? r.before : r.after;
8560
- const at = s.index === 0 ? cut : Number.POSITIVE_INFINITY;
8561
- place(s.el, rect, clip, at);
8562
- place(s.preview, rect, clip, at);
9116
+ place(s.el, rect, clip, s.index === 0);
9117
+ place(s.preview, rect, clip, s.index === 0);
8563
9118
  if (s.index !== 0)
8564
9119
  continue;
8565
9120
  for (const n of [s.el, s.preview])
@@ -8579,8 +9134,8 @@ function compare(container, options) {
8579
9134
  width: Math.max(1, Math.round(stageW * dpr)),
8580
9135
  height: Math.max(1, Math.round(stageH * dpr)),
8581
9136
  dpr,
8582
- wipeX: wx * dpr,
8583
- soft: 0.75 * dpr,
9137
+ wipeX: fu.cut * dpr,
9138
+ soft: Math.max(0.75, fu.feather) * dpr,
8584
9139
  from: MODE_INDEX[fromMode],
8585
9140
  to: MODE_INDEX[toMode],
8586
9141
  mix: Math.min(1, Math.max(0, fade.x)),
@@ -8607,6 +9162,7 @@ function compare(container, options) {
8607
9162
  zu.step(dt);
8608
9163
  zv.step(dt);
8609
9164
  zw.step(dt);
9165
+ gripAt.step(dt);
8610
9166
  const playing = isPlaying();
8611
9167
  if (playing) {
8612
9168
  const m = master();
@@ -8637,11 +9193,9 @@ function compare(container, options) {
8637
9193
  pushFrame(s);
8638
9194
  }
8639
9195
  }
8640
- const moving = !lay.resting || !fade.resting || !zoomResting() || wipeCatching;
9196
+ const moving = !lay.resting || !fade.resting || !zoomResting() || wipeCatching || !gripAt.resting;
8641
9197
  if (moving)
8642
9198
  dirty = true;
8643
- if (auto && !lay.resting)
8644
- fitHeight();
8645
9199
  if (dirty)
8646
9200
  draw();
8647
9201
  return playing || moving;
@@ -8667,13 +9221,7 @@ function compare(container, options) {
8667
9221
  zw.set(c.w);
8668
9222
  requestDraw();
8669
9223
  };
8670
- const autoHeight = (w) => {
8671
- const shape = sizeOf(sides[1]);
8672
- const side = splitHeight(w, shape);
8673
- const over = w * shape.h / shape.w;
8674
- const h = side + (over - side) * lay.x;
8675
- return Math.max(60, Math.min(win.innerHeight * 0.8, h));
8676
- };
9224
+ const autoHeight = (w) => Math.max(60, Math.min(win.innerHeight * 0.8, splitHeight(w, sizeOf(sides[1]))));
8677
9225
  const fitHeight = () => {
8678
9226
  const h = autoHeight(stageW);
8679
9227
  if (Math.abs(h - stageH) < 0.25)
@@ -8685,7 +9233,8 @@ function compare(container, options) {
8685
9233
  const readColors = () => {
8686
9234
  const c = tokenColor(win.getComputedStyle(root), "--_ku-cmp-diff", [0.49, 0.81, 0.61, 1]);
8687
9235
  accent = [c[0], c[1], c[2]];
8688
- labelW = [labels[0].offsetWidth, labels[1].offsetWidth];
9236
+ const unfold = MARK.w - labelFold(folded, MARK.w).width + MARK.gap * folded;
9237
+ labelW = [labels[0].offsetWidth + unfold, labels[1].offsetWidth + unfold];
8689
9238
  requestDraw();
8690
9239
  };
8691
9240
  const measure = () => {
@@ -8776,8 +9325,10 @@ function compare(container, options) {
8776
9325
  root.focus({ preventScroll: true });
8777
9326
  touches.set(e.pointerId, p);
8778
9327
  stage.setPointerCapture(e.pointerId);
9328
+ press = touches.size === 1 && mode !== "wipe" && master() ? p : null;
8779
9329
  if (touches.size >= 2) {
8780
9330
  e.preventDefault();
9331
+ press = null;
8781
9332
  return startPinch();
8782
9333
  }
8783
9334
  if (mode === "wipe") {
@@ -8787,7 +9338,7 @@ function compare(container, options) {
8787
9338
  if (inside && (grab.onLine || !zoomed())) {
8788
9339
  drag = { kind: "wipe", offset: grab.offset };
8789
9340
  root.dataset.dragging = "";
8790
- gripY = p.y;
9341
+ gripAt.snap(wr.h > 0 ? (p.y - wr.y) / wr.h : 0.5);
8791
9342
  e.preventDefault();
8792
9343
  if (!grab.onLine)
8793
9344
  setWipe(wipeAt(p.x, wr), true, true);
@@ -8805,6 +9356,8 @@ function compare(container, options) {
8805
9356
  const onMove = (e) => {
8806
9357
  const p = local(e);
8807
9358
  pointer = p;
9359
+ if (press && Math.hypot(p.x - press.x, p.y - press.y) > 4)
9360
+ press = null;
8808
9361
  if (touches.has(e.pointerId))
8809
9362
  touches.set(e.pointerId, p);
8810
9363
  if (pinch && touches.size >= 2) {
@@ -8818,14 +9371,12 @@ function compare(container, options) {
8818
9371
  pinch = { d, x: mid.x, y: mid.y };
8819
9372
  return handZoom(zoomAbout(panned, k, q.pu, q.pv));
8820
9373
  }
8821
- if (mode === "wipe" && drag?.kind !== "pan") {
8822
- gripY = p.y;
8823
- if (!drag)
8824
- placeGrip(wipeRect(layoutNow().L.overlay));
8825
- }
8826
- if (drag?.kind === "wipe")
8827
- setWipe(wipeAt(p.x, wipeRect(layoutNow().L.overlay), drag.offset));
8828
- else if (drag?.kind === "pan") {
9374
+ if (drag?.kind === "wipe") {
9375
+ const wr = wipeRect(layoutNow().L.overlay);
9376
+ gripAt.snap(wr.h > 0 ? (p.y - wr.y) / wr.h : 0.5);
9377
+ setWipe(wipeAt(p.x, wr, drag.offset));
9378
+ placeGrip(wr);
9379
+ } else if (drag?.kind === "pan") {
8829
9380
  const dx = p.x - drag.x;
8830
9381
  const dy = p.y - drag.y;
8831
9382
  drag.x = p.x;
@@ -8836,11 +9387,23 @@ function compare(container, options) {
8836
9387
  drawNow();
8837
9388
  };
8838
9389
  const endDrag = (e) => {
9390
+ if (press && e.type === "pointerup") {
9391
+ api.toggle();
9392
+ glyph.flash(isPlaying());
9393
+ }
9394
+ press = null;
8839
9395
  touches.delete(e.pointerId);
8840
9396
  if (touches.size < 2)
8841
9397
  pinch = null;
8842
9398
  if (!drag)
8843
9399
  return;
9400
+ if (drag.kind === "wipe") {
9401
+ if (reducedMotion(win))
9402
+ gripAt.snap(0.5);
9403
+ else
9404
+ gripAt.set(0.5);
9405
+ requestDraw();
9406
+ }
8844
9407
  drag = null;
8845
9408
  delete root.dataset.dragging;
8846
9409
  delete root.dataset.panning;
@@ -8990,8 +9553,9 @@ function compare(container, options) {
8990
9553
  old.el.replaceWith(s.el);
8991
9554
  old.preview.replaceWith(s.preview);
8992
9555
  sides[index] = s;
9556
+ root.toggleAttribute("data-video", sides.some((x) => x.kind === "video"));
8993
9557
  note.textContent = "";
8994
- labels[index].textContent = labelOf(index);
9558
+ setLabel(index, labelOf(index));
8995
9559
  if (near)
8996
9560
  loadSide(s);
8997
9561
  readColors();
@@ -9002,8 +9566,9 @@ function compare(container, options) {
9002
9566
  stage.insertBefore(sides[1].preview, labels[0]);
9003
9567
  stage.insertBefore(sides[0].el, labels[0]);
9004
9568
  stage.insertBefore(sides[0].preview, labels[0]);
9005
- labels[0].textContent = labelOf(0);
9006
- labels[1].textContent = labelOf(1);
9569
+ setLabel(0, labelOf(0));
9570
+ setLabel(1, labelOf(1));
9571
+ root.toggleAttribute("data-video", sides.some((s) => s.kind === "video"));
9007
9572
  loupeBtn.setAttribute("aria-pressed", String(loupeOn));
9008
9573
  loopBtn.setAttribute("aria-pressed", String(loopNow() !== null));
9009
9574
  const modePill = modeList.length > 1 ? selectionPill(modes, {
@@ -9143,11 +9708,11 @@ function compare(container, options) {
9143
9708
  if (before)
9144
9709
  replace(0, before);
9145
9710
  else if (next.before)
9146
- labels[0].textContent = next.before.label ?? "Before";
9711
+ setLabel(0, next.before.label ?? "Before");
9147
9712
  if (after)
9148
9713
  replace(1, after);
9149
9714
  else if (next.after)
9150
- labels[1].textContent = next.after.label ?? "After";
9715
+ setLabel(1, next.after.label ?? "After");
9151
9716
  if (before || after)
9152
9717
  strip?.update({ ...before ? { before } : {}, ...after ? { after } : {} });
9153
9718
  if (next.before || next.after)
@@ -9204,6 +9769,7 @@ function compare(container, options) {
9204
9769
  disposed = true;
9205
9770
  loop.stop();
9206
9771
  modePill?.dispose();
9772
+ glyph.dispose();
9207
9773
  win.clearTimeout(releaseTimer);
9208
9774
  ro.disconnect();
9209
9775
  nearIo?.disconnect();
@@ -9233,7 +9799,7 @@ function compare(container, options) {
9233
9799
  wake();
9234
9800
  return api;
9235
9801
  }
9236
- var COMPARE_FFMPEG = "ffmpeg -i in.mov -vf fps=30 -fps_mode cfr -c:v libx264 -preset slow -crf 18 -g 15 -keyint_min 15 -pix_fmt yuv420p -movflags +faststart -an out.mp4", LAYOUT, FADE2, FADE_REDUCED, ZOOM, WIPE_CATCH, SLOP = 12, INSET = 8, GRIP, LOUPE, FAST_SEEK_FRAMES = 15, RELEASE_MS = 3000, MODE_LABEL;
9802
+ var COMPARE_FFMPEG = "ffmpeg -i in.mov -vf fps=30 -fps_mode cfr -c:v libx264 -preset slow -crf 18 -g 15 -keyint_min 15 -pix_fmt yuv420p -movflags +faststart -an out.mp4", LAYOUT, FADE2, FADE_REDUCED, ZOOM, WIPE_CATCH, SLOP = 12, INSET = 8, GRIP, MARK, LOUPE, FAST_SEEK_FRAMES = 15, RELEASE_MS = 3000, MODE_LABEL;
9237
9803
  var init_compare = __esm(() => {
9238
9804
  init_motion();
9239
9805
  init_page_hints();
@@ -9244,15 +9810,17 @@ var init_compare = __esm(() => {
9244
9810
  init_icons();
9245
9811
  init_look();
9246
9812
  init_rgba();
9813
+ init_player();
9247
9814
  init_selection_pill();
9248
9815
  init_timeline();
9249
9816
  init_timeline_model();
9250
- LAYOUT = springMotion({ duration: 0.35, bounce: 0 });
9817
+ LAYOUT = springMotion(SPRINGS.default);
9251
9818
  FADE2 = springMotion(SPRINGS.snappy);
9252
9819
  FADE_REDUCED = springMotion(FADE);
9253
9820
  ZOOM = springMotion({ duration: 0.35, bounce: 0 });
9254
9821
  WIPE_CATCH = springMotion(CATCH_UP);
9255
9822
  GRIP = { w: 16, h: 28 };
9823
+ MARK = { w: 12, gap: 4 };
9256
9824
  LOUPE = { r: 88, zoom: 4 };
9257
9825
  MODE_LABEL = {
9258
9826
  side: { short: "Side", long: "Side by side" },
@@ -10298,19 +10866,19 @@ var init_diff_css = __esm(() => {
10298
10866
  @property --kd-fb{syntax:"<length>";inherits:false;initial-value:0px}
10299
10867
  :where(.knobs-ui){--kd-add:var(--_k-diff-add);--kd-del:var(--_k-diff-del);--kd-mod:var(--_k-diff-mod);--kd-move:var(--k-diff-move,var(--_k-info))}
10300
10868
  :where(.knobs-diff,.knobs-diff *,.knobs-diff-files,.knobs-diff-files *){box-sizing:border-box}
10301
- .knobs-ui.knobs-diff{--kd-bg:var(--k-diff-bg,var(--_kd-bg,var(--_k-bg)));--kd-solid:var(--kd-fill,var(--kd-bg));--kd-line:calc(var(--_k-font-size) * 1.5);
10869
+ .knobs-ui.knobs-diff{--kd-bg:var(--k-diff-bg,var(--_kd-bg,transparent));--kd-solid:var(--kd-fill,var(--kd-bg));--kd-line:calc(var(--_k-font-size) * 1.5);
10302
10870
  --kd-i:var(--k-diff-inset,8px);--kd-x:var(--kd-i);--kd-inner:max(3px,calc(var(--_ku-pane-radius) - var(--kd-i)));--kd-ctl:var(--kd-inner);
10303
10871
  --kd-head:calc(var(--_k-control-h) + 2 * var(--kd-i));--kd-fade:var(--k-diff-fade,20px);--kd-stick:0px;position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-width:0;min-height:0;overflow:hidden;
10304
10872
  background:var(--kd-bg);border-radius:var(--_ku-pane-radius);font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg);text-align:left;
10305
10873
  letter-spacing:normal;word-spacing:normal;text-transform:none;-webkit-font-smoothing:antialiased}
10306
10874
  .knobs-ui.knobs-diff[data-height="auto"]{height:auto}
10307
- .knobs-diff::after{content:"";position:absolute;inset:0;z-index:3;border-radius:inherit;box-shadow:var(--_ku-pane-ring);pointer-events:none}
10875
+ .knobs-ui.knobs-diff:not([data-on="pane"])::after{content:"";position:absolute;inset:0;z-index:3;box-shadow:inset 0 -1px 0 var(--_k-border-subtle);pointer-events:none}
10308
10876
  .knobs-diff [hidden],.knobs-diff-files [hidden]{display:none!important}
10309
10877
  .knobs-diff svg{display:block;flex:none}
10310
- .knobs-ui.knobs-diff[data-on="surface"]{--kd-x:0px;--kd-head:var(--_k-control-h);--kd-ctl:var(--_k-radius)}
10311
- /* The header's hairline shows only once the code has scrolled under it. */
10878
+ .knobs-ui.knobs-diff:not([data-on="pane"]){--kd-x:0px;--kd-head:calc(var(--_k-control-h) + var(--kd-i));--kd-ctl:var(--_k-radius)}
10879
+ /* A hairline under the header, firmer once the code has scrolled under it. */
10312
10880
  .knobs-diff-head{position:relative;z-index:1;flex:none;display:flex;align-items:center;gap:8px;height:var(--kd-head);padding:0 var(--kd-x);
10313
- box-shadow:inset 0 -1px transparent;transition:box-shadow .15s;user-select:none;-webkit-user-select:none}
10881
+ box-shadow:inset 0 -1px var(--_k-border-subtle);transition:box-shadow .15s;user-select:none;-webkit-user-select:none}
10314
10882
  .knobs-diff[data-scrolled]>.knobs-diff-head{box-shadow:inset 0 -1px var(--_k-border)}
10315
10883
  .knobs-diff-path{flex:0 1 auto;min-width:0;overflow:hidden;white-space:nowrap}
10316
10884
  .knobs-diff-dir,.knobs-diff-from{color:var(--_k-fg-3)}
@@ -10348,6 +10916,15 @@ var init_diff_css = __esm(() => {
10348
10916
  .knobs-ui .knobs-diff-viewed[aria-pressed="true"]{color:var(--_k-fg);background:var(--_ku-wash)}
10349
10917
  .knobs-diff-viewed svg{width:16px;height:16px}
10350
10918
  .knobs-diff-viewed[aria-pressed="true"] svg{color:var(--kd-add)}
10919
+ /* Narrow, the header gives up words before anything is cut off at its end: Viewed keeps its
10920
+ check, then the counts' bar goes, then the counts; the path fades at its end throughout. */
10921
+ .knobs-diff-head,.knobs-diff-part-head{container-type:inline-size}
10922
+ @container (max-width:520px){
10923
+ .knobs-diff-viewed-label{display:none}
10924
+ .knobs-ui .knobs-diff-viewed{width:var(--_k-control-h);padding:0;justify-content:center}
10925
+ .knobs-diff-counts .knobs-diff-bar{display:none}
10926
+ }
10927
+ @container (max-width:400px){.knobs-diff-counts{display:none}.knobs-diff-nav>span{min-width:2.6em}}
10351
10928
  .knobs-diff-path{transition:opacity .2s}
10352
10929
  .knobs-diff[data-viewed]>.knobs-diff-head .knobs-diff-path,.knobs-diff-part[data-viewed] .knobs-diff-path{opacity:.6}
10353
10930
  /* Split | Unified: the selection pill on the one picked, its bright copy the picked one's text; a
@@ -10412,24 +10989,26 @@ var init_diff_css = __esm(() => {
10412
10989
  --diffs-bg-deletion-number-override:color-mix(in srgb,var(--kd-del) 55%,var(--kd-solid));
10413
10990
  --diffs-bg-addition-emphasis-override:color-mix(in srgb,var(--kd-add) 22%,transparent);
10414
10991
  --diffs-bg-deletion-emphasis-override:color-mix(in srgb,var(--kd-del) 22%,transparent);
10415
- --diffs-bg-separator-override:color-mix(in srgb,var(--_k-fg) 4.5%,var(--kd-solid));
10416
- --diffs-bg-context-override:color-mix(in srgb,var(--_k-fg) 3%,var(--kd-solid));
10417
- --diffs-bg-buffer-override:color-mix(in srgb,var(--_k-fg) 3.5%,var(--kd-solid));
10992
+ /* Unchanged lines and the folds between hunks sit on the void; only a split's missing lines
10993
+ are faintly filled. */
10994
+ --diffs-bg-separator-override:var(--kd-solid);
10995
+ --diffs-bg-context-override:var(--kd-solid);
10996
+ --diffs-bg-buffer-override:color-mix(in srgb,var(--_k-fg) 2.5%,var(--kd-solid));
10418
10997
  --diffs-bg-selection-override:var(--_k-accent);
10419
10998
  --diffs-fg-number-override:var(--_k-fg-3);
10420
10999
  }
10421
- /* diffReview: one card, the file list a drawer at its left (its width springs; the list inside
10422
- is pinned to its right edge, so it slides out from under the diff), parted from the diff by a
10423
- hairline. The diff in it draws no card of its own. */
10424
- .knobs-ui.knobs-diff-review{position:relative;display:flex;width:100%;height:100%;min-width:0;min-height:0;overflow:hidden;border-radius:var(--_ku-pane-radius);background:var(--_ku-pane-bg);
11000
+ /* diffReview, on the void as the diff is: the file list a drawer at its left (its width springs;
11001
+ the list inside is pinned to its right edge, so it slides out from under the diff), parted
11002
+ from the diff by a hairline; one hairline runs under both headers and one under it all. */
11003
+ .knobs-ui.knobs-diff-review{position:relative;display:flex;width:100%;height:100%;min-width:0;min-height:0;overflow:hidden;
10425
11004
  font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg);text-align:left;letter-spacing:normal;-webkit-font-smoothing:antialiased}
10426
- .knobs-diff-review::after{content:"";position:absolute;inset:0;z-index:5;border-radius:inherit;box-shadow:var(--_ku-pane-ring);pointer-events:none}
11005
+ .knobs-diff-review::after{content:"";position:absolute;inset:0;z-index:5;box-shadow:inset 0 -1px 0 var(--_k-border-subtle);pointer-events:none}
10427
11006
  :where(.knobs-diff-review,.knobs-diff-review *){box-sizing:border-box}
10428
- .knobs-diff-drawer{position:relative;flex:none;width:0;height:100%;overflow:hidden;box-shadow:inset -1px 0 var(--_k-border)}
11007
+ .knobs-diff-drawer{position:relative;flex:none;width:0;height:100%;overflow:hidden;box-shadow:inset -1px 0 var(--_k-border-subtle)}
10429
11008
  .knobs-diff-drawer[data-shut]{visibility:hidden}
10430
11009
  .knobs-diff-drawer-in{position:absolute;top:0;bottom:0;right:0;display:flex;flex-direction:column}
10431
11010
  .knobs-diff-drawer-head{flex:none;display:flex;align-items:center;gap:6px;height:calc(var(--_k-control-h) + 2 * var(--k-diff-inset,8px));padding:0 var(--k-diff-inset,8px) 0 calc(var(--_ku-row-x) + 8px);
10432
- user-select:none;-webkit-user-select:none}
11011
+ box-shadow:inset 0 -1px var(--_k-border-subtle);user-select:none;-webkit-user-select:none}
10433
11012
  .knobs-diff-drawer-title{font-weight:var(--_k-weight-title)}
10434
11013
  .knobs-diff-drawer-count{color:var(--_k-fg-3);font-size:var(--_k-text-meta);font-variant-numeric:tabular-nums}
10435
11014
  .knobs-diff-drawer-in>.knobs-diff-files{flex:1;min-height:0;height:auto}
@@ -10441,10 +11020,11 @@ var init_diff_css = __esm(() => {
10441
11020
  .knobs-ui .knobs-diff-toggle[aria-pressed]:not(:hover,:focus-visible){background:transparent}
10442
11021
  /* diffReview's file list: a filter over a compact tree, a status letter, the counts and their
10443
11022
  bar on each file; one highlight glides to the file on show. Viewed files dim, with a tick. */
10444
- .knobs-diff-files{--k-row:var(--k-diff-row,28px);display:flex;flex-direction:column;height:100%;min-height:0;padding-top:2px}
11023
+ .knobs-diff-files{--k-row:var(--k-diff-row,28px);display:flex;flex-direction:column;height:100%;min-height:0;padding-top:4px}
10445
11024
  .knobs-diff-filter{position:relative;flex:none;display:flex;align-items:center;gap:6px;height:26px;margin:0 var(--_ku-row-x) 4px;padding:0 4px 0 8px;
10446
- border-radius:max(6px,var(--_ku-row-radius));background:var(--_ku-wash);color:var(--_k-fg-3);box-shadow:inset 0 0 0 1px transparent;transition:background .15s,box-shadow .15s}
10447
- .knobs-diff-filter:focus-within{background:var(--_ku-wash-hi);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--_k-accent) 45%,transparent)}
11025
+ border-radius:max(6px,var(--_ku-row-radius));color:var(--_k-fg-3);box-shadow:inset 0 0 0 1px transparent;transition:box-shadow .15s}
11026
+ /* The filter is a line of text, no box; focused, a hairline ring. */
11027
+ .knobs-diff-filter:focus-within{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--_k-accent) 45%,transparent)}
10448
11028
  .knobs-diff-filter svg{width:12px;height:12px;flex:none}
10449
11029
  .knobs-ui .knobs-diff-filter>input{flex:1;min-width:0;height:100%;font:var(--_k-weight) var(--_k-font-size)/1 var(--_k-font);color:var(--_k-fg);caret-color:var(--_k-accent);background:transparent}
10450
11030
  .knobs-ui .knobs-diff-filter>input::placeholder{color:var(--_k-fg-3)}
@@ -11263,7 +11843,10 @@ function codeDiff(container, options) {
11263
11843
  const viewedToggle = () => {
11264
11844
  const b = make("button", "knobs-diff-viewed");
11265
11845
  b.type = "button";
11266
- b.append(make("span", ""), "Viewed");
11846
+ const label = make("span", "knobs-diff-viewed-label");
11847
+ label.textContent = "Viewed";
11848
+ b.append(make("span", ""), label);
11849
+ b.setAttribute("aria-label", "Viewed");
11267
11850
  return b;
11268
11851
  };
11269
11852
  const paintViewed = (b, on) => {
@@ -15962,6 +16545,7 @@ init_timeline();
15962
16545
  init_timeline_model();
15963
16546
  init_compare();
15964
16547
  init_compare_strip();
16548
+ init_player();
15965
16549
  init_compare_model();
15966
16550
  init_tree();
15967
16551
  init_tree_model();
@@ -19378,401 +19962,15 @@ function optionPicker(container, options) {
19378
19962
  // src/ui/walkthrough.ts
19379
19963
  init_motion();
19380
19964
  init_page_hints();
19965
+ init_icons();
19381
19966
  init_look();
19967
+ init_selection_pill();
19382
19968
 
19383
- // src/ui/walkthrough-media.ts
19384
- init_compare_model();
19385
- init_glide();
19386
- init_look();
19387
-
19388
- // src/ui/walkthrough-css.ts
19969
+ // src/ui/walkthrough-blocks.ts
19389
19970
  init_motion();
19390
- var snappy3 = springCss(SPRINGS.snappy);
19391
- var calm = springCss(SPRINGS.calm);
19392
- var walkthroughCss = `
19393
- :where(.knobs-walkthrough,.knobs-walkthrough *,.knobs-wt-slot,.knobs-wt-slot *){box-sizing:border-box}
19394
- .knobs-ui.knobs-walkthrough{--kw-snappy:${snappy3.easing};--kw-snappy-ms:${snappy3.duration}ms;--kw-calm:${calm.easing};--kw-calm-ms:${calm.duration}ms;
19395
- --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));
19396
- --kw-card:var(--_ku-pane-bg);--kw-card-ring:var(--_ku-pane-ring);--kw-pad:16px;--kw-ink:var(--k-walkthrough-ink,#ff375f);
19397
- 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;
19398
- letter-spacing:normal;-webkit-font-smoothing:antialiased}
19399
- .knobs-walkthrough [hidden]{display:none!important}
19400
- .knobs-walkthrough :is(h1,h2,h3,p){margin:0;font:inherit;letter-spacing:inherit}
19401
- .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}
19402
- .knobs-walkthrough a:hover{text-decoration-color:currentColor}
19403
- .knobs-walkthrough code,.knobs-wt-slot code{font:calc(1em - 1px) var(--_k-mono);padding:1px 5px;border-radius:5px;background:var(--_k-fill)}
19404
- :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;
19405
- background:transparent;border:0;border-radius:0;margin:0;padding:0;min-width:0;box-shadow:none;outline:none}
19406
-
19407
- /* ——— layout: the rail beside the column; one column where it's narrow ——— */
19408
- .knobs-wt-layout{display:grid;grid-template-columns:200px minmax(0,1fr);gap:40px;align-items:start}
19409
- .knobs-wt-main{display:flex;flex-direction:column;gap:56px;min-width:0;max-width:1120px;padding-bottom:40vh}
19410
- @container knobs-wt (max-width:880px){.knobs-wt-layout{grid-template-columns:minmax(0,1fr);gap:0}.knobs-wt-main{gap:44px}}
19411
-
19412
- /* ——— the page's head ——— */
19413
- .knobs-wt-head{display:flex;flex-direction:column;gap:10px}
19414
- .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}
19415
- .knobs-wt-summary{max-width:var(--kw-measure);color:var(--_k-fg-2)}
19416
- .knobs-wt-meta{display:flex;flex-wrap:wrap;gap:6px 16px;color:var(--_k-fg-3);font-size:calc(var(--kw-text) - 2px)}
19417
- .knobs-wt-meta>span{display:inline-flex;gap:6px;white-space:nowrap}
19418
- .knobs-wt-meta b{font-weight:var(--_k-weight);color:var(--_k-fg-2);font-family:var(--_k-mono);font-size:calc(1em - .5px)}
19419
-
19420
- /* ——— a section: a PR or topic ——— */
19421
- .knobs-wt-section{display:flex;flex-direction:column;gap:14px;min-width:0}
19422
- .knobs-wt-shead{display:flex;flex-direction:column;gap:6px;scroll-margin-top:16px}
19423
- .knobs-wt-kicker{display:flex;align-items:center;gap:10px;font-size:calc(var(--kw-text) - 2px);color:var(--_k-fg-3)}
19424
- .knobs-wt-kicker a{font-family:var(--_k-mono);color:var(--_k-fg-2);text-decoration:none}
19425
- .knobs-wt-kicker a:hover{color:var(--_k-fg)}
19426
- .knobs-wt-stitle{font-size:calc(var(--kw-text) + 4px)!important;font-weight:var(--_k-weight-title)!important;line-height:1.3}
19427
- .knobs-wt-ssum{max-width:var(--kw-measure);color:var(--_k-fg-2)}
19428
- .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;
19429
- --kw-tone:var(--_k-fg-2);color:var(--kw-tone);background:color-mix(in srgb,var(--kw-tone) 14%,transparent)}
19430
- .knobs-wt-chip::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor}
19431
- :is(.knobs-wt-chip,.knobs-wt-toc-glyph)[data-tone="good"]{--kw-tone:var(--kw-good)}
19432
- :is(.knobs-wt-chip,.knobs-wt-toc-glyph)[data-tone="warn"]{--kw-tone:var(--_k-warn)}
19433
- :is(.knobs-wt-chip,.knobs-wt-toc-glyph)[data-tone="bad"]{--kw-tone:var(--_k-danger)}
19434
- :is(.knobs-wt-chip,.knobs-wt-toc-glyph)[data-tone="info"]{--kw-tone:var(--kw-info)}
19435
- :is(.knobs-wt-chip,.knobs-wt-toc-glyph)[data-tone="dim"]{--kw-tone:var(--_k-fg-2)}
19436
-
19437
- /* ——— a step: one card ——— */
19438
- .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);
19439
- scroll-margin-top:16px;outline:none;container:knobs-wt-step / inline-size}
19440
- .knobs-walkthrough[data-frame="flat"] .knobs-wt-step{box-shadow:inset 0 0 0 1px var(--_k-border)}
19441
- .knobs-wt-step-head{display:flex;align-items:baseline;gap:10px;min-width:0}
19442
- .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}
19443
- .knobs-wt-stepTitle{flex:1;min-width:0;font-weight:var(--_k-weight-title)!important}
19444
- .knobs-wt-caption{max-width:var(--kw-measure);color:var(--_k-fg-2);margin-top:-6px!important}
19445
- .knobs-wt-question{font-weight:var(--_k-weight-label)!important}
19446
- /* The step Kit is on: its number lights, and its choices show their keys. */
19447
- .knobs-wt-step[data-current] .knobs-wt-num{color:var(--_k-fg)}
19448
- .knobs-wt-media{display:grid;grid-template-columns:repeat(var(--kw-n,1),minmax(0,1fr));gap:12px;min-width:0}
19449
- @container knobs-wt-step (max-width:640px){.knobs-wt-media{grid-template-columns:minmax(0,1fr)}}
19450
-
19451
- /* ——— the feed: the agent's updates and answers, newest first, compact ——— */
19452
- .knobs-wt-feed{margin-top:-24px}
19453
- .knobs-wt-feed-list{list-style:none;margin:0;padding:0;max-height:260px;overflow:auto;scrollbar-width:none;display:flex;flex-direction:column}
19454
- .knobs-wt-feed-list::-webkit-scrollbar{display:none}
19455
- .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)}
19456
- .knobs-wt-fi+.knobs-wt-fi{box-shadow:inset 0 1px 0 var(--_k-border)}
19457
- .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}
19458
- .knobs-wt-fi-kind{color:var(--_k-fg-2);font-size:calc(var(--kw-text) - 2.5px);font-weight:var(--_k-weight-label)}
19459
- .knobs-wt-fi[data-kind="qa"] .knobs-wt-fi-kind{color:var(--kw-info)}
19460
- .knobs-wt-fi-body{max-width:var(--kw-measure);color:var(--_k-fg)}
19461
- .knobs-wt-fi-q{color:var(--_k-fg-2)}
19462
- .knobs-wt-fi-q::before{content:"You asked: ";color:var(--_k-fg-3)}
19463
- :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;
19464
- padding:3px 6px;margin:0;border-radius:6px;cursor:pointer;outline:none;transition:background .15s,color .15s}
19465
- .knobs-ui :is(.knobs-wt-fi-step,.knobs-wt-steplink):is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
19466
-
19467
- /* ——— a step's sent notes ——— */
19468
- .knobs-wt-notes{display:flex;flex-direction:column}
19469
- .knobs-wt-notes:empty{display:none}
19470
- .knobs-wt-n{display:flex;align-items:flex-start;gap:10px;padding:6px 0;overflow:hidden;
19471
- 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)}
19472
- .knobs-wt-n[data-entering]{opacity:0;filter:blur(3px)}
19473
- .knobs-wt-n[data-leaving]{opacity:0;filter:blur(3px);padding-block:0}
19474
- .knobs-wt-n+.knobs-wt-n{box-shadow:inset 0 1px 0 var(--_k-border)}
19475
- .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}
19476
- .knobs-wt-n-meta[data-seek]{cursor:pointer}
19477
- .knobs-wt-n-meta[data-seek]:hover{color:var(--_k-fg)}
19478
- .knobs-wt-n-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:6px;font-size:calc(var(--kw-text) - 1px)}
19479
- .knobs-wt-n-voice{height:30px;max-width:320px}
19480
- .knobs-wt-n-ink{display:block;width:120px;max-height:80px;border-radius:6px;background:var(--_k-fill)}
19481
- .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}
19482
-
19483
- /* ——— the note bar, docked at the bottom of the column ——— */
19484
- .knobs-wt-dock{position:sticky;bottom:12px;z-index:40;margin-top:-24px}
19485
- .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);
19486
- box-shadow:var(--_ku-pane-ring),var(--_k-shadow-overlay)}
19487
- .knobs-wt-on{padding-top:3px;color:var(--_k-fg-3);font-size:calc(var(--kw-text) - 2.5px);white-space:nowrap;overflow:hidden;
19488
- -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)}
19489
- .knobs-wt-row{display:flex;align-items:flex-end;gap:4px}
19490
- :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;
19491
- border-radius:0;margin:0;padding:0;min-width:0;box-shadow:none;outline:none}
19492
- .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)}
19493
- .knobs-ui .knobs-wt-input::placeholder{color:var(--_k-fg-3)}
19494
- .knobs-wt-attach{display:flex;gap:4px;align-items:center;align-self:center}
19495
- .knobs-wt-attach:empty{display:none}
19496
- .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}
19497
- .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}
19498
- .knobs-ui .knobs-wt-x:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
19499
- .knobs-wt-x svg{width:10px;height:10px}
19500
- .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);
19501
- color:var(--_k-fg-2);cursor:pointer;transition:background .15s,color .15s}
19502
- .knobs-ui .knobs-wt-tool:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
19503
- .knobs-ui .knobs-wt-tool[aria-pressed="true"]{background:var(--_k-fill-selected);color:var(--_k-fg)}
19504
- .knobs-wt-tool svg{width:15px;height:15px}
19505
- .knobs-wt-rec{font-size:calc(var(--kw-text) - 2.5px);font-variant-numeric:tabular-nums}
19506
- .knobs-wt-rec:empty{display:none}
19507
- .knobs-ui .knobs-wt-tool[data-send]{color:var(--_k-fg-3)}
19508
- .knobs-ui .knobs-wt-tool[data-send][data-ready]{background:var(--_k-accent);color:var(--_k-bg)}
19509
- /* Back: in place over the bar for a few seconds after a note moves the page on. */
19510
- .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);
19511
- 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;
19512
- 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)}
19513
- .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)}
19514
- .knobs-ui .knobs-wt-back:hover{background:var(--_k-fill-selected)}
19515
-
19516
- /* ——— ink over a step's media ——— */
19517
- .knobs-wt-media,.knobs-wt-options{position:relative}
19518
- .knobs-wt-ink{position:absolute;inset:0;z-index:5;width:100%;height:100%;overflow:visible;pointer-events:none;touch-action:none}
19519
- .knobs-wt-ink[data-on]{pointer-events:auto;cursor:crosshair}
19520
- .knobs-wt-ink path{fill:none;stroke:var(--kw-ink);stroke-width:3px;vector-effect:non-scaling-stroke;stroke-linecap:round;stroke-linejoin:round}
19521
-
19522
- /* ——— full-window: the step over the page, its media filling it; its place held in the page ——— */
19523
- .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}
19524
- .knobs-wt-scrim[data-on]{pointer-events:auto}
19525
- .knobs-wt-hold-card{flex:none}
19526
- .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)}
19527
- .knobs-wt-step[data-full]>:not(.knobs-wt-media){flex:none}
19528
- .knobs-wt-step[data-full] .knobs-wt-notes{display:none}
19529
- .knobs-wt-step[data-full]>.knobs-wt-media{flex:1;min-height:0;grid-auto-rows:minmax(0,1fr)}
19530
- .knobs-wt-step[data-full] .knobs-wt-slot{display:flex;flex-direction:column;min-height:0}
19531
- .knobs-wt-step[data-full] .knobs-wt-body{flex:1;min-height:0}
19532
- .knobs-wt-step[data-full] .knobs-wt-fit{height:100%;transition:none}
19533
- .knobs-wt-step[data-full] .knobs-wt-frame{height:100%;aspect-ratio:auto}
19534
-
19535
- /* ——— a taste test: the options side by side, Kit's pick lit and the rest stepping back ——— */
19536
- .knobs-wt-options{display:grid;grid-template-columns:repeat(var(--kw-n,2),minmax(0,1fr));gap:10px}
19537
- @container knobs-wt-step (max-width:640px){.knobs-wt-options{grid-template-columns:minmax(0,1fr)}}
19538
- .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);
19539
- transition:background var(--kw-snappy-ms) var(--kw-snappy)}
19540
- .knobs-wt-option[data-picked]{background:color-mix(in srgb,var(--_k-accent) 12%,var(--_ku-wash))}
19541
- .knobs-wt-option>:not(.knobs-wt-pick){transition:opacity var(--kw-snappy-ms) var(--kw-snappy)}
19542
- .knobs-wt-options[data-has-pick] .knobs-wt-option:not([data-picked])>:not(.knobs-wt-pick){opacity:.55}
19543
- .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}
19544
- .knobs-ui .knobs-wt-pick:is(:hover,:focus-visible){color:var(--_k-fg)}
19545
- .knobs-wt-letter{flex:none;font:var(--_k-weight-title) calc(var(--kw-text) - 1px)/1 var(--_k-font);color:var(--_k-fg)}
19546
- .knobs-wt-olabel{flex:1;min-width:0;font-weight:var(--_k-weight-label);color:var(--_k-fg)}
19547
- .knobs-wt-suggested{flex:none;font-size:calc(var(--kw-text) - 3px);color:var(--_k-fg-3)}
19548
- .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;
19549
- transition:background var(--kw-snappy-ms) var(--kw-snappy),color var(--kw-snappy-ms) var(--kw-snappy)}
19550
- .knobs-wt-check svg{width:12px;height:12px}
19551
- .knobs-wt-option[data-picked] .knobs-wt-check{background:var(--_k-accent);color:var(--_k-bg)}
19552
- .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);
19553
- font:var(--_k-weight-label) calc(var(--kw-text) - 4px)/1 var(--_k-font);font-variant-numeric:tabular-nums;opacity:.5;transition:opacity .15s}
19554
- .knobs-wt-step[data-current] .knobs-wt-key{opacity:1}
19555
- .knobs-wt-ocap{color:var(--_k-fg-2);font-size:calc(var(--kw-text) - 1px);padding:0 4px}
19556
-
19557
- /* ——— the rail: progress, contents, export ——— */
19558
- .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)}
19559
- .knobs-wt-progress{display:flex;flex-direction:column;gap:7px}
19560
- .knobs-wt-count{display:flex;align-items:baseline;justify-content:space-between;color:var(--_k-fg-2);font-variant-numeric:tabular-nums}
19561
- .knobs-wt-count b{color:var(--_k-fg);font-weight:var(--_k-weight-label)}
19562
- .knobs-wt-bar{position:relative;height:4px;border-radius:2px;overflow:hidden;background:var(--_k-fill)}
19563
- .knobs-wt-fill{position:absolute;inset:0;border-radius:inherit;background:var(--_k-accent);transform-origin:0 50%;transform:scaleX(var(--kw-done,0))}
19564
- .knobs-walkthrough[data-settled] .knobs-wt-fill{transition:transform var(--kw-calm-ms) var(--kw-calm)}
19565
- /* The contents: no box, one text spine for sections and steps (the steps' dots hang in the
19566
- gutter left of it), and one pill behind the current step that springs between rows. */
19567
- .knobs-wt-toc{position:relative;flex:1 1 auto;min-height:0;overflow:auto;scrollbar-width:none;margin:0 -8px;padding:0 0 8px}
19568
- .knobs-wt-toc::-webkit-scrollbar{display:none}
19569
- .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;
19570
- -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)}
19571
- .knobs-wt-toc-section:first-of-type{padding-top:2px}
19572
- .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}
19573
- .knobs-wt-toc-mark[data-on]{opacity:1}
19574
- .knobs-walkthrough[data-settled] .knobs-wt-toc-mark{transition:opacity .15s}
19575
- .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}
19576
- .knobs-ui .knobs-wt-toc-step:is(:hover,:focus-visible){color:var(--_k-fg)}
19577
- .knobs-wt-toc-step[aria-current="step"]{color:var(--_k-fg)}
19578
- .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)}
19579
- /* A step's state: an open ring, a filled dot once answered, nothing to answer a faint ring. */
19580
- .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);
19581
- transition:background var(--kw-snappy-ms) var(--kw-snappy),box-shadow var(--kw-snappy-ms) var(--kw-snappy)}
19582
- .knobs-wt-toc-glyph[data-tone="accent"]{--kw-tone:var(--_k-accent)}
19583
- .knobs-wt-toc-glyph[data-done]{background:var(--kw-tone)}
19584
- .knobs-wt-toc-glyph[data-tone="none"]{box-shadow:inset 0 0 0 1px var(--_k-border)}
19585
- .knobs-wt-actions{display:flex;flex-wrap:wrap;gap:4px}
19586
- .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;
19587
- transition:background .15s,color .15s}
19588
- .knobs-ui .knobs-wt-btn:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
19589
- .knobs-ui .knobs-wt-btn[data-primary]{background:var(--_k-fill-selected);color:var(--_k-fg)}
19590
- .knobs-ui .knobs-wt-btn[data-primary]:is(:hover,:focus-visible){background:color-mix(in srgb,var(--_k-fg) 16%,transparent)}
19591
- .knobs-wt-keys{color:var(--_k-fg-3);font-size:calc(var(--kw-text) - 3px);line-height:1.6}
19592
- .knobs-wt-keys kbd{font:inherit;color:var(--_k-fg-2)}
19593
- /* Narrow: the rail becomes a bar along the top, with the progress and the export only. */
19594
- @container knobs-wt (max-width:880px){
19595
- .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)}
19596
- .knobs-wt-progress{flex:1}.knobs-wt-toc,.knobs-wt-keys{display:none}
19597
- .knobs-wt-step,.knobs-wt-shead{scroll-margin-top:60px}
19598
- }
19599
-
19600
- /* ——— media slots ——— */
19601
- .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);
19602
- --kw-good:var(--k-walkthrough-good,var(--_k-ok));--kw-info:var(--k-walkthrough-info,var(--_k-info))}
19603
- .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}
19604
- /* A compare is exactly as tall as its pictures at this width, so there's no letterbox: the two
19605
- sides edge to edge (side) or one (overlaid), plus its mode bar, the timeline and their two gaps
19606
- (92 px), and the frame strip with its gap (158 px). */
19607
- .knobs-wt-fit{width:100%;height:calc(100cqw / 2 / var(--kw-aspect,1.7778) + var(--kw-chrome,92px))}
19608
- .knobs-wt-fit[data-overlay]{height:calc(100cqw / var(--kw-aspect,1.7778) + var(--kw-chrome,92px))}
19609
- .knobs-wt-fit[data-strip]{--kw-chrome:158px}
19610
- .knobs-wt-fit[data-settled]{transition:height var(--k-wt-calm-ms,${calm.duration}ms) ${calm.easing}}
19611
- .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)}
19612
- .knobs-wt-frame>:is(video,img){position:absolute;inset:0;display:block;width:100%;height:100%;object-fit:contain;margin:0;border:0}
19613
- .knobs-wt-frame[data-natural]{aspect-ratio:auto}
19614
- .knobs-wt-frame[data-natural]>img{position:static;height:auto}
19615
- .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);
19616
- font:calc(var(--k-walkthrough-text,14px) - 2px)/1.55 var(--_k-mono);white-space:pre;tab-size:2;scrollbar-width:thin}
19617
- .knobs-wt-code[data-terminal]{white-space:pre-wrap;overflow-wrap:anywhere}
19618
- .knobs-wt-prompt{color:var(--_k-fg-3);user-select:none;-webkit-user-select:none}
19619
- .knobs-wt-cmd{color:var(--_k-fg);font-weight:var(--_k-weight-label)}
19620
- .knobs-wt-out{color:var(--_k-fg-2)}
19621
- .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)}
19622
- .knobs-wt-mlabel{color:var(--_k-fg-2);font-size:calc(var(--k-walkthrough-text,14px) - 2px)}
19623
- .knobs-wt-mvalue{display:flex;align-items:baseline;flex-wrap:wrap;gap:8px;font-variant-numeric:tabular-nums;font-feature-settings:"tnum"}
19624
- .knobs-wt-mfrom{color:var(--_k-fg-3);font-size:calc(var(--k-walkthrough-text,14px) + 2px)}
19625
- .knobs-wt-mto{font-size:calc(var(--k-walkthrough-text,14px) + 12px);font-weight:var(--_k-weight-title);line-height:1}
19626
- .knobs-wt-mof{color:var(--_k-fg-3)}
19627
- .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)}
19628
- .knobs-wt-mdelta[data-tone="good"]{--kw-tone:var(--kw-good)}
19629
- .knobs-wt-mdelta[data-tone="bad"]{--kw-tone:var(--_k-danger)}
19630
- .knobs-wt-mtrack{position:relative;height:4px;border-radius:2px;background:color-mix(in srgb,var(--_k-fg) 8%,transparent)}
19631
- .knobs-wt-mtrack>span{position:absolute;top:0;bottom:0;left:0;border-radius:inherit}
19632
- .knobs-wt-mtrack>.knobs-wt-mafter{background:var(--_k-accent)}
19633
- .knobs-wt-mtrack>.knobs-wt-mbefore{background:color-mix(in srgb,var(--_k-fg) 30%,transparent)}
19634
- .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)}
19635
- .knobs-wt-callout::before{content:"";flex:none;width:6px;height:6px;margin-top:.62em;border-radius:50%;background:var(--kw-tone)}
19636
- .knobs-wt-callout[data-tone="good"]{--kw-tone:var(--kw-good)}
19637
- .knobs-wt-callout[data-tone="warn"]{--kw-tone:var(--_k-warn)}
19638
- .knobs-wt-callout[data-tone="bad"]{--kw-tone:var(--_k-danger)}
19639
- .knobs-wt-text{max-width:var(--k-walkthrough-measure,68ch);color:var(--_k-fg-2)}
19640
- .knobs-wt-hold{border-radius:calc(var(--_k-radius-panel) - 4px);background:var(--_k-fill)}
19641
- .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)}
19642
-
19643
- /* ——— data blocks: history, stack, table, chapters. On the step's card, so no card of their own:
19644
- hairlines and fills only. Tones read the status tokens; hover fades are the panel's own. ——— */
19645
- :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)}
19646
- :where(.knobs-wt-slot) [data-tone="good"]{--kw-tone:var(--kw-good)}
19647
- :where(.knobs-wt-slot) [data-tone="warn"]{--kw-tone:var(--_k-warn)}
19648
- :where(.knobs-wt-slot) [data-tone="bad"]{--kw-tone:var(--_k-danger)}
19649
- :where(.knobs-wt-slot) [data-tone="info"]{--kw-tone:var(--kw-info)}
19650
- :where(.knobs-wt-slot) [data-tone="dim"]{--kw-tone:var(--_k-fg-3)}
19651
- .knobs-wt-tags{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}
19652
- .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);
19653
- text-decoration:none;white-space:nowrap;transition:background calc(var(--_k-hover-out) * 1ms),color calc(var(--_k-hover-out) * 1ms)}
19654
- .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)}
19655
-
19656
- /* A history: dates down the left, a spine of tone dots joined by a hairline (broken around each
19657
- dot), the event beside it. Inferred events take a ring for a dot and a dashed tag. */
19658
- .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)}
19659
- .knobs-wt-hgroup{display:flex;flex-direction:column;gap:6px;min-width:0}
19660
- .knobs-wt-hhead{display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 10px}
19661
- .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)}
19662
- .knobs-wt-hsum{color:var(--_k-fg-3);font-size:calc(var(--kw-t) - 2px);font-variant-numeric:tabular-nums}
19663
- .knobs-wt-hlist{list-style:none;margin:0;padding:0;display:flex;flex-direction:column}
19664
- .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}
19665
- .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)}
19666
- .knobs-wt-hage{color:var(--_k-fg-3);font-size:calc(var(--kw-t) - 3px);line-height:1.35}
19667
- .knobs-wt-hspine{grid-area:spine;position:relative;align-self:stretch;margin-block:-5px}
19668
- .knobs-wt-hspine::before,.knobs-wt-hspine::after{content:"";position:absolute;left:6.5px;width:1px;background:var(--kw-rail)}
19669
- .knobs-wt-hspine::before{top:0;height:calc(var(--kw-hy) - 6px)}
19670
- .knobs-wt-hspine::after{top:calc(var(--kw-hy) + 6px);bottom:0}
19671
- .knobs-wt-hev:first-child .knobs-wt-hspine::before,.knobs-wt-hev:last-child .knobs-wt-hspine::after{content:none}
19672
- .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))}
19673
- .knobs-wt-hev[data-type="release"] .knobs-wt-hdot{border-radius:1.5px;rotate:45deg;scale:.9}
19674
- .knobs-wt-hev[data-type="audit"] .knobs-wt-hdot{border-radius:1.5px}
19675
- .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))}
19676
- .knobs-wt-hmain{grid-area:main;display:flex;flex-direction:column;gap:2px;min-width:0}
19677
- .knobs-wt-htitle{display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 8px;font-weight:var(--_k-weight-label);color:var(--_k-fg)}
19678
- .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}
19679
- .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}
19680
- .knobs-ui .knobs-wt-hdetail{margin:0;max-width:var(--k-walkthrough-measure,68ch);color:var(--_k-fg-2)}
19681
- @container knobs-wt-slot (max-width:520px){
19682
- .knobs-wt-hev{grid-template-columns:14px minmax(0,1fr);grid-template-areas:"spine when" "spine main"}
19683
- .knobs-wt-hwhen{flex-direction:row;flex-wrap:wrap;align-items:baseline;column-gap:8px}
19684
- }
19685
-
19686
- /* A stack: the trunk at the top, each PR hanging off the branch it merges into, rails drawn as a
19687
- git graph (solid, so where two meet they don't darken). A PR with a step goes there on a click. */
19688
- .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)}
19689
- .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);
19690
- border-radius:max(0px,calc(var(--_k-radius-panel) - 8px));transition:background calc(var(--_k-hover-out) * 1ms)}
19691
- .knobs-wt-srow[data-trunk]{--kw-spb:0px;grid-template-columns:auto minmax(0,1fr)}
19692
- .knobs-wt-srow[data-go]{cursor:pointer}
19693
- .knobs-wt-srow[data-go]:hover{background:var(--_ku-wash);transition-duration:calc(var(--_k-hover-in) * 1ms)}
19694
- .knobs-wt-lanes{display:flex;align-self:stretch;margin-block:calc(var(--kw-spt) * -1) calc(var(--kw-spb) * -1)}
19695
- .knobs-wt-lane{position:relative;flex:none;width:16px}
19696
- .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)}
19697
- .knobs-wt-lane[data-down]::before{top:calc(var(--kw-sy) + 3.5px)}
19698
- .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}
19699
- .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)}
19700
- .knobs-wt-srow[data-trunk] .knobs-wt-lane[data-rail="node"]::after{background:var(--_k-fg-2)}
19701
- .knobs-wt-trunk{font:var(--_k-weight-label) calc(var(--kw-t) - 2px)/var(--kw-line) var(--_k-mono);color:var(--_k-fg-2)}
19702
- .knobs-wt-smain{display:flex;flex-direction:column;gap:1px;min-width:0}
19703
- .knobs-wt-sline{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 8px}
19704
- .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}
19705
- .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}
19706
- .knobs-ui a.knobs-wt-spr:is(:hover,:focus-visible){color:var(--_k-fg);text-decoration:underline}
19707
- .knobs-ui .knobs-wt-stitle{font-weight:var(--_k-weight-label);line-height:var(--kw-line);color:var(--_k-fg);text-align:left}
19708
- .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)}
19709
- .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)}
19710
- .knobs-ui .knobs-wt-ssummary{margin:0;max-width:var(--k-walkthrough-measure,68ch);color:var(--_k-fg-2)}
19711
- .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)}
19712
- .knobs-wt-delta{display:inline-flex;gap:6px}
19713
- .knobs-wt-add{color:var(--_k-diff-add)}
19714
- .knobs-wt-del{color:var(--_k-diff-del)}
19715
- .knobs-wt-sstatus{margin-top:calc((var(--kw-line) - 20px) / 2)}
19716
- .knobs-wt-stack[data-flat] .knobs-wt-srow{grid-template-columns:minmax(0,1fr) auto}
19717
- .knobs-wt-stack[data-flat] .knobs-wt-srow+.knobs-wt-srow{box-shadow:inset 0 1px 0 var(--_k-border)}
19718
- @container knobs-wt-slot (max-width:520px){
19719
- .knobs-wt-srow:not([data-trunk]){grid-template-columns:auto minmax(0,1fr)}
19720
- .knobs-wt-stack[data-flat] .knobs-wt-srow{grid-template-columns:minmax(0,1fr)}
19721
- .knobs-wt-sstatus{grid-column:-2;justify-self:start;margin-top:4px}
19722
- }
19723
-
19724
- /* A table: a real one, its head stuck while it scrolls, both ways; edges fade where there's more
19725
- (the top one under the head), and thin bars show on hover. */
19726
- @property --kw-fade-l{syntax:"<number>";inherits:false;initial-value:1}
19727
- @property --kw-fade-r{syntax:"<number>";inherits:false;initial-value:1}
19728
- @keyframes kw-fade-l{from{--kw-fade-l:1}to{--kw-fade-l:0}}
19729
- @keyframes kw-fade-r{from{--kw-fade-r:0}to{--kw-fade-r:1}}
19730
- .knobs-wt-table{max-height:520px;overflow:auto;overscroll-behavior-x:contain;outline:none;scrollbar-width:thin;scrollbar-color:transparent transparent;
19731
- transition:scrollbar-color calc(var(--_k-hover-out) * 1ms)}
19732
- .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)}
19733
- .knobs-wt-table:focus-visible{box-shadow:inset 0 0 0 2px var(--_k-focus);border-radius:calc(var(--_k-radius-panel) - 4px)}
19734
- @supports (animation-timeline:scroll()){
19735
- .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;
19736
- 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%;
19737
- --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)));
19738
- --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)));
19739
- -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}
19740
- }
19741
- .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)}
19742
- .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)}
19743
- .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;
19744
- background:var(--_k-pane-bg);box-shadow:inset 0 -1px 0 var(--_k-border)}
19745
- .knobs-wt-table td{padding:8px 12px;vertical-align:baseline;box-shadow:inset 0 -1px 0 var(--_k-border)}
19746
- .knobs-wt-table tr:last-child td{box-shadow:none}
19747
- .knobs-wt-table :is(th,td):first-child{padding-left:0}
19748
- .knobs-wt-table :is(th,td):last-child{padding-right:0}
19749
- .knobs-wt-table td:first-child{color:var(--_k-fg)}
19750
- .knobs-wt-table [data-align="right"]{text-align:right}
19751
- .knobs-wt-table [data-align="center"]{text-align:center}
19752
- .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}
19753
- .knobs-wt-table td[data-long]{min-width:24ch}
19754
- .knobs-wt-table td>.knobs-wt-steplink{margin-left:4px}
19755
- .knobs-ui .knobs-wt-table a.knobs-wt-chip{text-decoration:none}
19756
- .knobs-wt-ctone{color:var(--kw-tone)}
19757
-
19758
- /* Chapters: timestamp pills under a video; one highlight springs to the one playing. */
19759
- .knobs-wt-chapters{position:relative;display:flex;flex-wrap:wrap;gap:4px;margin-top:8px}
19760
- .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;
19761
- transition:opacity calc(var(--_k-motion-snappy-ms) * 1ms)}
19762
- .knobs-wt-chapter-mark[data-on]{opacity:1}
19763
- .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);
19764
- 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)}
19765
- .knobs-ui .knobs-wt-chapter:hover{background:var(--_ku-wash);color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
19766
- .knobs-ui .knobs-wt-chapter[aria-current]{background:transparent;color:var(--_k-fg)}
19767
- .knobs-ui .knobs-wt-chapter:focus-visible{box-shadow:inset 0 0 0 2px var(--_k-focus)}
19768
- .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)}
19769
- .knobs-wt-chapter[aria-current] .knobs-wt-chapter-time{color:var(--_k-fg-2)}
19770
- @media(prefers-reduced-motion:reduce){:is(.knobs-walkthrough,.knobs-wt-slot) :where(*,::before,::after){transition-duration:.15s!important;transition-property:opacity,background,color!important}}
19771
- `;
19772
-
19773
- // src/ui/walkthrough-blocks.ts
19774
- init_motion();
19775
- init_look();
19971
+ init_icons();
19972
+ init_look();
19973
+ init_selection_pill();
19776
19974
 
19777
19975
  // src/ui/walkthrough-model.ts
19778
19976
  var DEFAULT_VERDICTS = ["ship", "change", "unsure"];
@@ -19979,7 +20177,15 @@ function fillTable(m) {
19979
20177
  out[k] = fillCell(v);
19980
20178
  return out;
19981
20179
  });
19982
- return { kind: "table", columns, rows, ...m.caption ? { caption: m.caption } : {} };
20180
+ const primary = columns.find((c) => c.key === m.primary)?.key ?? columns[0]?.key ?? "";
20181
+ return {
20182
+ kind: "table",
20183
+ columns,
20184
+ rows,
20185
+ ...m.caption ? { caption: m.caption } : {},
20186
+ stack: m.stack ?? "auto",
20187
+ primary
20188
+ };
19983
20189
  }
19984
20190
  function fillChapters(chapters) {
19985
20191
  const id = uniqueIds();
@@ -20052,6 +20258,8 @@ function normalizeWalkthrough(doc) {
20052
20258
  ...step.caption ? { caption: step.caption } : {},
20053
20259
  ...step.question ? { question: step.question } : {},
20054
20260
  media: list.map((m) => fillMedia(m, doc.base)),
20261
+ columns: Math.max(1, Math.min(3, Math.round(step.columns ?? 1))),
20262
+ ...step.more?.trim() ? { more: step.more.trim() } : {},
20055
20263
  options,
20056
20264
  ask
20057
20265
  };
@@ -20063,8 +20271,10 @@ function normalizeWalkthrough(doc) {
20063
20271
  id,
20064
20272
  title: s.title,
20065
20273
  ...link ? { link } : {},
20274
+ ...typeof s.kicker === "string" && s.kicker.trim() ? { kicker: s.kicker.trim() } : {},
20066
20275
  ...s.status ? { status: s.status } : {},
20067
20276
  ...s.summary ? { summary: s.summary } : {},
20277
+ ...s.more?.trim() ? { more: s.more.trim() } : {},
20068
20278
  steps: own
20069
20279
  };
20070
20280
  });
@@ -20073,30 +20283,95 @@ function normalizeWalkthrough(doc) {
20073
20283
  value
20074
20284
  }));
20075
20285
  const stepRef = (ref) => findStep(steps, ref)?.key ?? null;
20286
+ const extra = (x) => ({
20287
+ ...x.status?.trim() ? { status: x.status.trim() } : {},
20288
+ trail: (x.trail ?? []).filter((t) => typeof t === "string" && t.trim())
20289
+ });
20076
20290
  const feed = sortFeed([
20077
- ...(doc.updates ?? []).map((u) => ({ kind: "update", at: parseAt(u.at), text: u.text, step: stepRef(u.step) })),
20291
+ ...(doc.updates ?? []).map((u) => ({
20292
+ kind: "update",
20293
+ at: parseAt(u.at),
20294
+ text: u.text,
20295
+ step: stepRef(u.step),
20296
+ ...extra(u)
20297
+ })),
20078
20298
  ...(doc.qa ?? []).map((x) => ({
20079
20299
  kind: "qa",
20080
20300
  at: parseAt(x.at),
20081
20301
  text: x.a,
20082
20302
  q: x.q,
20083
- step: stepRef(x.step)
20303
+ step: stepRef(x.step),
20304
+ ...extra(x)
20084
20305
  }))
20085
20306
  ]);
20086
20307
  return {
20087
20308
  id: doc.id,
20088
20309
  title: doc.title,
20089
20310
  ...doc.summary ? { summary: doc.summary } : {},
20311
+ ...doc.more?.trim() ? { more: doc.more.trim() } : {},
20090
20312
  meta,
20091
20313
  feed,
20092
20314
  verdicts: ((doc.verdicts?.length) ? doc.verdicts : DEFAULT_VERDICTS).map((v) => ({
20093
20315
  id: slug(v),
20094
20316
  label: label(v)
20095
20317
  })),
20096
- sections,
20097
- steps
20318
+ sections: withKickers(sections, doc.sections, doc.kickers),
20319
+ steps,
20320
+ prs: knownPRs(doc, sections),
20321
+ ...repoOf(doc) ? { repo: repoOf(doc) } : {}
20098
20322
  };
20099
20323
  }
20324
+ var prNumber = (href) => /\/(?:pull|merge_requests)\/(\d+)(?:[/?#]|$)/.exec(href)?.[1] ?? null;
20325
+ var repoOf = (doc) => doc.repo?.trim().replace(/^https?:\/\/github\.com\//, "").replace(/\/+$/, "") || undefined;
20326
+ function knownPRs(doc, sections) {
20327
+ const out = {};
20328
+ const repo = repoOf(doc);
20329
+ for (const s of sections) {
20330
+ const n = s.link && prNumber(s.link.href);
20331
+ if (n)
20332
+ out[n] = { href: s.link.href.replace(/(\/pull\/\d+).*$/, "$1") };
20333
+ }
20334
+ for (const [k, pr] of Object.entries(doc.prs ?? {})) {
20335
+ const n = k.replace(/^#/, "");
20336
+ if (!/^\d+$/.test(n))
20337
+ continue;
20338
+ out[n] = { ...out[n], ...pr };
20339
+ }
20340
+ if (repo) {
20341
+ for (const n of Object.keys(out))
20342
+ if (!out[n].href)
20343
+ out[n] = { ...out[n], href: `https://github.com/${repo}/pull/${n}` };
20344
+ }
20345
+ return out;
20346
+ }
20347
+ function findPR(prs, n, repo) {
20348
+ const known = prs[n];
20349
+ if (known)
20350
+ return known;
20351
+ return repo ? { href: `https://github.com/${repo}/pull/${n}` } : null;
20352
+ }
20353
+ var isPullRequest = (href) => /\/(?:pull|merge_requests)\/\d+(?:[/?#]|$)/.test(href);
20354
+ function withKickers(sections, source, kickers = "pr") {
20355
+ if (kickers === false)
20356
+ return [...sections];
20357
+ const noun = typeof kickers === "object" ? kickers.noun.trim() : "PR";
20358
+ const counted = typeof kickers === "object" ? sections : sections.filter((s) => s.link && isPullRequest(s.link.href));
20359
+ return sections.map((s, i) => {
20360
+ const own = source[i]?.kicker;
20361
+ if (own !== undefined || s.kicker)
20362
+ return s;
20363
+ const at = counted.indexOf(s);
20364
+ return at < 0 ? s : { ...s, kicker: `${noun} ${at + 1} of ${counted.length}`.trim() };
20365
+ });
20366
+ }
20367
+ function tableStacks(stack, room, natural) {
20368
+ if (stack !== "auto")
20369
+ return stack === "always";
20370
+ return room > 0 && natural > room + 1;
20371
+ }
20372
+ function noteBarReserve(bar, inset, gap) {
20373
+ return Math.max(0, Math.ceil(bar)) + inset + gap;
20374
+ }
20100
20375
  function parseAt(at) {
20101
20376
  if (!at)
20102
20377
  return null;
@@ -20381,6 +20656,9 @@ function feedbackMarkdown(fb, verdicts) {
20381
20656
  `;
20382
20657
  }
20383
20658
  var escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
20659
+ function plainMarkdown(text) {
20660
+ 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();
20661
+ }
20384
20662
  function inlineMarkdown(text) {
20385
20663
  const codes = [];
20386
20664
  let s = text.replace(/`([^`\n]+)`/g, (_, c) => {
@@ -20401,6 +20679,10 @@ function inlineMarkdown(text) {
20401
20679
  }
20402
20680
 
20403
20681
  // src/ui/walkthrough-blocks.ts
20682
+ var GLYPH = {
20683
+ go: icon("caretRight", { small: true }),
20684
+ caret: icon("caretRight", { small: true })
20685
+ };
20404
20686
  var el = (doc, tag, cls) => {
20405
20687
  const n = doc.createElement(tag);
20406
20688
  if (cls)
@@ -20430,9 +20712,10 @@ function stepButton(doc, ref, steps) {
20430
20712
  const s = ref && steps?.find(ref);
20431
20713
  if (!s || !steps)
20432
20714
  return null;
20433
- const b = text(doc, "button", "knobs-wt-steplink", s.number);
20715
+ const b = el(doc, "button", "knobs-wt-steplink");
20716
+ b.innerHTML = GLYPH.go;
20434
20717
  b.type = "button";
20435
- b.title = `Go to ${s.number} ${s.title}`;
20718
+ b.title = `Go to “${s.title}”`;
20436
20719
  b.setAttribute("aria-label", b.title);
20437
20720
  b.addEventListener("click", (e) => {
20438
20721
  e.stopPropagation();
@@ -20557,7 +20840,7 @@ function mountStack(host, m, steps) {
20557
20840
  if (target && steps) {
20558
20841
  const title = text(doc, "button", "knobs-wt-stitle", it.title);
20559
20842
  title.type = "button";
20560
- title.title = `Go to ${target.number} ${target.title}`;
20843
+ title.title = `Go to “${target.title}”`;
20561
20844
  title.addEventListener("click", (e) => {
20562
20845
  e.stopPropagation();
20563
20846
  steps.go(target.key);
@@ -20612,175 +20895,861 @@ function delta(doc, d) {
20612
20895
  s.setAttribute("aria-label", `${d.add} added, ${d.del} removed`);
20613
20896
  return s;
20614
20897
  }
20615
- function mountTable(host, m, steps) {
20616
- const doc = host.ownerDocument;
20617
- const win = doc.defaultView;
20618
- const scroller = el(doc, "div", "knobs-wt-table");
20619
- const table = el(doc, "table");
20620
- if (m.caption)
20621
- table.append(text(doc, "caption", "", m.caption));
20622
- const head = el(doc, "thead");
20623
- const hr = el(doc, "tr");
20624
- for (const c of m.columns) {
20625
- const th = text(doc, "th", "", c.label);
20626
- th.scope = "col";
20627
- th.dataset.align = c.align;
20628
- hr.append(th);
20629
- }
20630
- head.append(hr);
20631
- const body = el(doc, "tbody");
20632
- for (const r of m.rows) {
20633
- const tr = el(doc, "tr");
20634
- for (const c of m.columns) {
20635
- const td = el(doc, "td");
20636
- td.dataset.align = c.align;
20637
- if (c.mono)
20638
- td.dataset.mono = "";
20639
- const cell = r[c.key];
20640
- if (cell) {
20641
- if (cell.text.length > 40 && !c.mono && !cell.code)
20642
- td.dataset.long = "";
20643
- let content;
20644
- if (cell.badge) {
20645
- content = link(doc, "knobs-wt-chip", cell.text, cell.href);
20646
- content.dataset.tone = cell.badge;
20647
- } else {
20648
- const inner = cell.code ? text(doc, "code", "", cell.text) : null;
20649
- if (cell.href) {
20650
- content = link(doc, "knobs-wt-clink", inner ? "" : cell.text, cell.href);
20651
- if (inner)
20652
- content.append(inner);
20653
- } else
20654
- content = inner ?? text(doc, "span", "", cell.text);
20655
- if (cell.tone) {
20656
- content.classList.add("knobs-wt-ctone");
20657
- content.dataset.tone = cell.tone;
20658
- }
20659
- }
20660
- td.append(content);
20661
- const go = stepButton(doc, cell.step, steps);
20662
- if (go)
20663
- td.append(go);
20664
- }
20665
- tr.append(td);
20666
- }
20667
- body.append(tr);
20668
- }
20669
- table.append(head, body);
20670
- scroller.append(table);
20671
- host.append(scroller);
20672
- const measure = () => {
20673
- scroller.style.setProperty("--kw-thead", `${head.offsetHeight}px`);
20674
- const scrolls = scroller.scrollWidth > scroller.clientWidth + 1 || scroller.scrollHeight > scroller.clientHeight + 1;
20675
- if (scrolls) {
20676
- scroller.tabIndex = 0;
20677
- scroller.setAttribute("role", "region");
20678
- scroller.setAttribute("aria-label", m.caption ?? "Table");
20679
- } else {
20680
- scroller.removeAttribute("tabindex");
20681
- scroller.removeAttribute("role");
20682
- scroller.removeAttribute("aria-label");
20683
- }
20684
- };
20685
- const ro = win && "ResizeObserver" in win ? new win.ResizeObserver(measure) : null;
20686
- ro?.observe(scroller);
20687
- measure();
20688
- return {
20689
- dispose: () => {
20690
- ro?.disconnect();
20691
- scroller.remove();
20692
- },
20693
- context: () => ({ kind: "table" })
20694
- };
20898
+ function cellContent(doc, into, cell, steps) {
20899
+ let content;
20900
+ if (cell.badge) {
20901
+ content = link(doc, "knobs-wt-chip", cell.text, cell.href);
20902
+ content.dataset.tone = cell.badge;
20903
+ } else {
20904
+ const inner = cell.code ? text(doc, "code", "", cell.text) : null;
20905
+ if (cell.href) {
20906
+ content = link(doc, "knobs-wt-clink", inner ? "" : cell.text, cell.href);
20907
+ if (inner)
20908
+ content.append(inner);
20909
+ } else
20910
+ content = inner ?? text(doc, "span", "", cell.text);
20911
+ if (cell.tone) {
20912
+ content.classList.add("knobs-wt-ctone");
20913
+ content.dataset.tone = cell.tone;
20914
+ }
20915
+ }
20916
+ into.append(content);
20917
+ const go = stepButton(doc, cell.step, steps);
20918
+ if (go)
20919
+ into.append(go);
20920
+ }
20921
+ var cellAttrs = (td, c) => {
20922
+ td.dataset.align = c.align;
20923
+ if (c.mono)
20924
+ td.dataset.mono = "";
20925
+ };
20926
+ function mountTable(host, m, steps) {
20927
+ const doc = host.ownerDocument;
20928
+ const win = doc.defaultView;
20929
+ const box = el(doc, "div", "knobs-wt-tablebox");
20930
+ const scroller = el(doc, "div", "knobs-wt-table");
20931
+ const table = el(doc, "table");
20932
+ if (m.caption)
20933
+ table.append(text(doc, "caption", "", m.caption));
20934
+ const head = el(doc, "thead");
20935
+ const hr = el(doc, "tr");
20936
+ for (const c of m.columns) {
20937
+ const th = text(doc, "th", "", c.label);
20938
+ th.scope = "col";
20939
+ th.dataset.align = c.align;
20940
+ hr.append(th);
20941
+ }
20942
+ head.append(hr);
20943
+ const body = el(doc, "tbody");
20944
+ for (const r of m.rows) {
20945
+ const tr = el(doc, "tr");
20946
+ for (const c of m.columns) {
20947
+ const td = el(doc, "td");
20948
+ cellAttrs(td, c);
20949
+ const cell = r[c.key];
20950
+ if (cell) {
20951
+ if (cell.text.length > 40 && !c.mono && !cell.code)
20952
+ td.dataset.long = "";
20953
+ cellContent(doc, td, cell, steps);
20954
+ }
20955
+ tr.append(td);
20956
+ }
20957
+ body.append(tr);
20958
+ }
20959
+ table.append(head, body);
20960
+ scroller.append(table);
20961
+ const stack = el(doc, "div", "knobs-wt-tstack");
20962
+ stack.setAttribute("role", "list");
20963
+ if (m.caption)
20964
+ stack.setAttribute("aria-label", m.caption);
20965
+ const primary = m.columns.find((c) => c.key === m.primary) ?? m.columns[0];
20966
+ for (const r of m.rows) {
20967
+ const item = el(doc, "div", "knobs-wt-trow");
20968
+ item.setAttribute("role", "listitem");
20969
+ const title = el(doc, "div", "knobs-wt-ttitle");
20970
+ if (primary) {
20971
+ cellAttrs(title, primary);
20972
+ const cell = r[primary.key];
20973
+ if (cell)
20974
+ cellContent(doc, title, cell, steps);
20975
+ }
20976
+ const line = el(doc, "div", "knobs-wt-tline");
20977
+ line.append(title);
20978
+ const badges = el(doc, "div", "knobs-wt-tbadges");
20979
+ for (const c of m.columns) {
20980
+ const cell = r[c.key];
20981
+ if (c !== primary && cell?.badge)
20982
+ cellContent(doc, badges, cell, steps);
20983
+ }
20984
+ if (badges.childNodes.length)
20985
+ line.append(badges);
20986
+ item.append(line);
20987
+ const terms = el(doc, "dl", "knobs-wt-tterms");
20988
+ for (const c of m.columns) {
20989
+ if (c === primary)
20990
+ continue;
20991
+ const cell = r[c.key];
20992
+ if (!cell || cell.text === "" || cell.badge)
20993
+ continue;
20994
+ const dd = el(doc, "dd");
20995
+ cellAttrs(dd, c);
20996
+ dd.removeAttribute("data-align");
20997
+ cellContent(doc, dd, cell, steps);
20998
+ terms.append(text(doc, "dt", "", c.label), dd);
20999
+ }
21000
+ if (terms.childNodes.length)
21001
+ item.append(terms);
21002
+ stack.append(item);
21003
+ }
21004
+ if (m.caption)
21005
+ stack.append(text(doc, "p", "knobs-wt-tcaption", m.caption));
21006
+ box.append(scroller, stack);
21007
+ host.append(box);
21008
+ let natural = 0;
21009
+ const measureNatural = () => {
21010
+ if (!box.isConnected)
21011
+ return 0;
21012
+ const was = box.dataset.stacked;
21013
+ delete box.dataset.stacked;
21014
+ box.dataset.measure = "";
21015
+ const w = table.getBoundingClientRect().width;
21016
+ delete box.dataset.measure;
21017
+ if (was !== undefined)
21018
+ box.dataset.stacked = was;
21019
+ return w;
21020
+ };
21021
+ const measure = () => {
21022
+ if (!natural)
21023
+ natural = measureNatural();
21024
+ const stacked = tableStacks(m.stack, box.clientWidth, natural);
21025
+ box.toggleAttribute("data-stacked", stacked);
21026
+ scroller.style.setProperty("--kw-thead", `${head.offsetHeight}px`);
21027
+ const scrolls = !stacked && (scroller.scrollWidth > scroller.clientWidth + 1 || scroller.scrollHeight > scroller.clientHeight + 1);
21028
+ if (scrolls) {
21029
+ scroller.tabIndex = 0;
21030
+ scroller.setAttribute("role", "region");
21031
+ scroller.setAttribute("aria-label", m.caption ?? "Table");
21032
+ } else {
21033
+ scroller.removeAttribute("tabindex");
21034
+ scroller.removeAttribute("role");
21035
+ scroller.removeAttribute("aria-label");
21036
+ }
21037
+ };
21038
+ if (m.stack === "always")
21039
+ box.dataset.stacked = "";
21040
+ const ro = win && "ResizeObserver" in win ? new win.ResizeObserver(measure) : null;
21041
+ ro?.observe(box);
21042
+ measure();
21043
+ doc.fonts?.ready.then(() => {
21044
+ if (!box.isConnected)
21045
+ return;
21046
+ natural = 0;
21047
+ measure();
21048
+ });
21049
+ return {
21050
+ dispose: () => {
21051
+ ro?.disconnect();
21052
+ box.remove();
21053
+ },
21054
+ context: () => ({ kind: "table" })
21055
+ };
21056
+ }
21057
+ function chapterPills(host, chapters, io) {
21058
+ const doc = host.ownerDocument;
21059
+ const list = el(doc, "div", "knobs-wt-chapters");
21060
+ list.setAttribute("role", "group");
21061
+ list.setAttribute("aria-label", "Chapters");
21062
+ let at = -2;
21063
+ const rows = chapters.map((c, i) => {
21064
+ const b = el(doc, "button", "knobs-wt-chapter");
21065
+ b.type = "button";
21066
+ b.append(text(doc, "span", "knobs-wt-chapter-n", String(i + 1)));
21067
+ b.append(text(doc, "span", "knobs-wt-chapter-label", c.label));
21068
+ b.append(text(doc, "span", "knobs-wt-chapter-time", clock(c.start)));
21069
+ b.addEventListener("click", () => {
21070
+ io.onEvent?.({ what: "seek", time: Math.round(c.start * 100) / 100, detail: c.label });
21071
+ io.seek(c.start);
21072
+ select(i);
21073
+ });
21074
+ list.append(b);
21075
+ return b;
21076
+ });
21077
+ host.append(list);
21078
+ const pill = selectionPill(list, {
21079
+ selected: () => rows[at] ?? null,
21080
+ rows: () => rows
21081
+ });
21082
+ const select = (i) => {
21083
+ if (i === at)
21084
+ return;
21085
+ at = i;
21086
+ rows.forEach((b, k) => k === at ? b.setAttribute("aria-current", "true") : b.removeAttribute("aria-current"));
21087
+ pill.update();
21088
+ };
21089
+ const sync = () => select(chapterAt(chapters, io.now()));
21090
+ sync();
21091
+ return {
21092
+ sync,
21093
+ get current() {
21094
+ return chapters[at] ?? null;
21095
+ },
21096
+ dispose() {
21097
+ pill.dispose();
21098
+ list.remove();
21099
+ }
21100
+ };
21101
+ }
21102
+ function disclosure(host, options) {
21103
+ const doc = host.ownerDocument;
21104
+ const win = doc.defaultView;
21105
+ let open = options.open === true;
21106
+ const root = el(doc, "div", "knobs-wt-disc");
21107
+ const toggle = el(doc, "button", "knobs-wt-disc-toggle");
21108
+ toggle.type = "button";
21109
+ toggle.innerHTML = GLYPH.caret;
21110
+ toggle.append(text(doc, "span", "", options.label));
21111
+ if (options.count !== undefined)
21112
+ toggle.append(text(doc, "span", "knobs-wt-disc-count", String(options.count)));
21113
+ const viewport = el(doc, "div", "knobs-wt-disc-body");
21114
+ const content = el(doc, "div", "knobs-wt-disc-content");
21115
+ content.append(options.content);
21116
+ viewport.append(content);
21117
+ root.append(toggle, viewport);
21118
+ host.append(root);
21119
+ const STATE = springMotion(SPRINGS.default);
21120
+ const h = new Spring(0, STATE.omega, STATE.zeta, 0.25);
21121
+ let full = 0;
21122
+ const paint = (moving) => {
21123
+ const k = full > 0 ? Math.max(0, Math.min(1, h.x / full)) : open ? 1 : 0;
21124
+ viewport.style.height = !moving && open ? "" : `${Math.max(0, h.x).toFixed(2)}px`;
21125
+ viewport.toggleAttribute("data-moving", moving);
21126
+ content.style.opacity = (open ? Math.min(1, k * 1.25) : Math.max(0, k * 1.5 - 0.5)).toFixed(3);
21127
+ };
21128
+ const loop = frameLoop(win, (dt) => {
21129
+ h.step(dt);
21130
+ const moving = !h.resting;
21131
+ paint(moving);
21132
+ return moving;
21133
+ });
21134
+ const sync = () => {
21135
+ root.toggleAttribute("data-open", open);
21136
+ toggle.setAttribute("aria-expanded", String(open));
21137
+ viewport.inert = !open;
21138
+ };
21139
+ sync();
21140
+ h.snap(0);
21141
+ paint(false);
21142
+ if (open) {
21143
+ viewport.style.height = "";
21144
+ content.style.opacity = "1";
21145
+ }
21146
+ const api = {
21147
+ element: root,
21148
+ get open() {
21149
+ return open;
21150
+ },
21151
+ toggle(next = !open) {
21152
+ if (next === open)
21153
+ return;
21154
+ open = next;
21155
+ sync();
21156
+ full = content.offsetHeight;
21157
+ if (reducedMotion(win)) {
21158
+ loop.stop();
21159
+ h.snap(open ? full : 0);
21160
+ viewport.style.height = open ? "" : "0px";
21161
+ content.style.opacity = open ? "1" : "0";
21162
+ return;
21163
+ }
21164
+ if (h.resting)
21165
+ h.snap(open ? 0 : full);
21166
+ h.set(open ? full : 0);
21167
+ paint(true);
21168
+ loop.wake();
21169
+ },
21170
+ dispose() {
21171
+ loop.stop();
21172
+ root.remove();
21173
+ }
21174
+ };
21175
+ toggle.addEventListener("click", () => api.toggle());
21176
+ return api;
21177
+ }
21178
+ var PR_PATHS = {
21179
+ pull: "M104,64A32,32,0,1,0,64,95v66a32,32,0,1,0,16,0V95A32.06,32.06,0,0,0,104,64ZM56,64A16,16,0,1,1,72,80,16,16,0,0,1,56,64ZM88,192a16,16,0,1,1-16-16A16,16,0,0,1,88,192Zm120-31V110.63a23.85,23.85,0,0,0-7-17L163.31,56H192a8,8,0,0,0,0-16H144a8,8,0,0,0-8,8V96a8,8,0,0,0,16,0V67.31L189.66,105a8,8,0,0,1,2.34,5.66V161a32,32,0,1,0,16,0Zm-8,47a16,16,0,1,1,16-16A16,16,0,0,1,200,208Z",
21180
+ merge: "M208,112a32.05,32.05,0,0,0-30.69,23l-42.21-6a8,8,0,0,1-4.95-2.71L94.43,84.55A32,32,0,1,0,72,87v82a32,32,0,1,0,16,0V101.63l30,35a24,24,0,0,0,14.83,8.14l44,6.28A32,32,0,1,0,208,112ZM64,56A16,16,0,1,1,80,72,16,16,0,0,1,64,56ZM96,200a16,16,0,1,1-16-16A16,16,0,0,1,96,200Zm112-40a16,16,0,1,1,16-16A16,16,0,0,1,208,160Z"
21181
+ };
21182
+ var prGlyph = (merged) => `<svg class="knobs-icon" viewBox="0 0 256 256" width="12" height="12" fill="currentColor" aria-hidden="true" focusable="false"><path d="${merged ? PR_PATHS.merge : PR_PATHS.pull}"/></svg>`;
21183
+ var PR_STATE = { merged: "Merged", open: "Open", draft: "Draft", closed: "Closed" };
21184
+ function prChip(doc, n, pr) {
21185
+ const chip = el(doc, pr.href ? "a" : "span", "knobs-wt-pr");
21186
+ if (pr.href) {
21187
+ chip.setAttribute("href", pr.href);
21188
+ chip.setAttribute("target", "_blank");
21189
+ chip.setAttribute("rel", "noreferrer");
21190
+ }
21191
+ if (pr.state)
21192
+ chip.dataset.state = pr.state;
21193
+ chip.innerHTML = prGlyph(pr.state === "merged");
21194
+ chip.append(text(doc, "span", "", `#${n}`));
21195
+ const tip = [`#${n}`, pr.title, pr.state ? PR_STATE[pr.state] : ""].filter(Boolean).join(" · ");
21196
+ chip.title = tip;
21197
+ chip.setAttribute("aria-label", `Pull request ${tip}`);
21198
+ return chip;
21199
+ }
21200
+ var PR_URL = /^https?:\/\/[^/]+\/.+\/(?:pull|merge_requests)\/(\d+)\/?(?:[?#].*)?$/;
21201
+ var PR_REF = /(^|[^\w&/#])#(\d{3,7})\b/g;
21202
+ function linkPRs(root, find) {
21203
+ const doc = root.ownerDocument;
21204
+ for (const a of Array.from(root.querySelectorAll("a[href]"))) {
21205
+ if (a.classList.contains("knobs-wt-pr") || a.closest(".knobs-wt-slot,button"))
21206
+ continue;
21207
+ const n = PR_URL.exec(a.getAttribute("href") ?? "")?.[1];
21208
+ const words = a.textContent?.trim() ?? "";
21209
+ if (!n || !(words === `#${n}` || words === n || words === a.getAttribute("href")))
21210
+ continue;
21211
+ a.replaceWith(prChip(doc, n, { href: a.href, ...find(n) }));
21212
+ }
21213
+ const walker = doc.createTreeWalker(root, 5);
21214
+ const hits = [];
21215
+ for (let t = walker.nextNode();t; t = walker.nextNode()) {
21216
+ if (t.nodeType !== 3)
21217
+ continue;
21218
+ const p = t.parentElement;
21219
+ if (!p || p.closest("a,code,pre,button,.knobs-wt-slot,.knobs-wt-pr"))
21220
+ continue;
21221
+ PR_REF.lastIndex = 0;
21222
+ if (PR_REF.test(t.nodeValue ?? ""))
21223
+ hits.push(t);
21224
+ }
21225
+ for (const t of hits) {
21226
+ const value = t.nodeValue ?? "";
21227
+ const frag = doc.createDocumentFragment();
21228
+ let last = 0;
21229
+ let changed = false;
21230
+ PR_REF.lastIndex = 0;
21231
+ for (let m = PR_REF.exec(value);m; m = PR_REF.exec(value)) {
21232
+ const pr = find(m[2]);
21233
+ if (!pr)
21234
+ continue;
21235
+ const at = m.index + m[1].length;
21236
+ frag.append(value.slice(last, at), prChip(doc, m[2], pr));
21237
+ last = at + m[2].length + 1;
21238
+ changed = true;
21239
+ }
21240
+ if (!changed)
21241
+ continue;
21242
+ frag.append(value.slice(last));
21243
+ t.replaceWith(frag);
21244
+ }
21245
+ }
21246
+
21247
+ // src/ui/walkthrough-media.ts
21248
+ init_compare_model();
21249
+ init_glide();
21250
+ init_icons();
21251
+ init_look();
21252
+
21253
+ // src/ui/walkthrough-css.ts
21254
+ init_motion();
21255
+ var snappy3 = springCss(SPRINGS.snappy);
21256
+ var calm = springCss(SPRINGS.calm);
21257
+ var state = springCss(SPRINGS.default);
21258
+ var quick = springCss(FADE);
21259
+ 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)";
21260
+ var walkthroughCss = `
21261
+ :where(.knobs-walkthrough,.knobs-walkthrough *,.knobs-wt-slot,.knobs-wt-slot *){box-sizing:border-box}
21262
+ .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;
21263
+ --kw-measure:var(--k-walkthrough-measure,68ch);--kw-width:var(--k-walkthrough-width,960px);--kw-rail-w:220px;--kw-good:var(--k-walkthrough-good,var(--_k-ok));--kw-info:var(--k-walkthrough-info,var(--_k-info));
21264
+ --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);
21265
+ 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;
21266
+ letter-spacing:normal;-webkit-font-smoothing:antialiased}
21267
+ .knobs-walkthrough [hidden]{display:none!important}
21268
+ /* (0,0,1): a host's element rules stop here, and every class rule below wins over it. */
21269
+ :where(.knobs-walkthrough) :is(h1,h2,h3,p,ol,ul,dl,dd){margin:0;font:inherit;letter-spacing:inherit}
21270
+ .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}
21271
+ .knobs-walkthrough a:hover{text-decoration-color:currentColor}
21272
+ .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)}
21273
+ :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;
21274
+ background:transparent;border:0;border-radius:0;margin:0;padding:0;min-width:0;box-shadow:none;outline:none}
21275
+
21276
+ /* ——— layout: the contents rail sticky at the left edge (right if asked), and the column centred
21277
+ in the room beside it, up to --kw-width: media take the column, prose stops at the measure.
21278
+ One column where it's narrow ——— */
21279
+ .knobs-wt-layout{display:grid;grid-template-columns:var(--kw-rail-w) minmax(0,1fr);gap:var(--_k-space-column);align-items:start}
21280
+ .knobs-walkthrough[data-rail="right"] .knobs-wt-layout{grid-template-columns:minmax(0,1fr) var(--kw-rail-w)}
21281
+ .knobs-walkthrough[data-rail="right"] .knobs-wt-main{grid-column:1;grid-row:1}
21282
+ .knobs-walkthrough[data-rail="right"] .knobs-wt-rail{grid-column:2;grid-row:1}
21283
+ .knobs-walkthrough[data-rail="none"] .knobs-wt-layout{grid-template-columns:minmax(0,1fr)}
21284
+ .knobs-wt-main{justify-self:center;width:100%;max-width:var(--kw-width);display:flex;flex-direction:column;gap:var(--_k-space-group);min-width:0;padding-bottom:12px}
21285
+ /* The intro, the updates and each section: a section's room apart, the hairline in the middle. */
21286
+ .knobs-wt-flow{display:flex;flex-direction:column;gap:calc(var(--_k-space-section) / 2);min-width:0}
21287
+ .knobs-wt-flow>*+*{padding-top:calc(var(--_k-space-section) / 2);box-shadow:inset 0 1px 0 var(--_k-border-subtle)}
21288
+ @container knobs-wt (max-width:960px){.knobs-walkthrough[data-rail] .knobs-wt-layout{grid-template-columns:minmax(0,1fr);gap:var(--_k-space-title)}
21289
+ .knobs-walkthrough[data-rail] .knobs-wt-layout>*{grid-column:auto;grid-row:auto}}
21290
+
21291
+ /* ——— one line of facts: small, faint, joined by ·, its trailing edge fading ——— */
21292
+ .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}}
21293
+ .knobs-wt-metaline>*{flex:none}
21294
+ .knobs-wt-metaline>*+*::before{content:"·";margin-right:var(--_k-space-inside);color:var(--_k-fg-3)}
21295
+ .knobs-walkthrough .knobs-wt-metaline a:not(.knobs-wt-pr){display:inline-flex;align-items:baseline;color:inherit;text-decoration:none;transition:color calc(var(--_k-hover-out) * 1ms)}
21296
+ .knobs-walkthrough .knobs-wt-metaline a:not(.knobs-wt-pr):hover{color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
21297
+ .knobs-wt-metaline svg{align-self:center;width:12px;height:12px;margin-left:2px}
21298
+ /* A pull request: a small chip, its state's icon and number in a quiet tint of the state (open
21299
+ the success hue, merged the violet series, draft and closed neutral), its title on hover. */
21300
+ .knobs-ui .knobs-wt-pr{--kw-pr:var(--_k-fg-2);display:inline-flex;align-items:center;gap:3px;height:18px;padding:0 5px 0 4px;margin:0 1px;border-radius:calc(var(--_k-radius) / 2);vertical-align:1px;
21301
+ background:color-mix(in oklch,var(--kw-pr) 12%,transparent);color:var(--kw-pr);font:var(--_k-weight-label) var(--_k-type-small)/1 var(--_k-font);font-variant-numeric:tabular-nums;white-space:nowrap;text-decoration:none;
21302
+ transition:background calc(var(--_k-hover-out) * 1ms)}
21303
+ .knobs-ui .knobs-wt-pr[data-state="open"]{--kw-pr:color-mix(in oklch,var(--_k-ok) 60%,var(--_k-fg-2))}
21304
+ .knobs-ui .knobs-wt-pr[data-state="merged"]{--kw-pr:color-mix(in oklch,var(--_k-series-5) 60%,var(--_k-fg-2))}
21305
+ .knobs-ui .knobs-wt-pr:is([data-state="draft"],[data-state="closed"]){--kw-pr:var(--_k-fg-3)}
21306
+ .knobs-ui a.knobs-wt-pr:is(:hover,:focus-visible){background:color-mix(in oklch,var(--kw-pr) 22%,transparent);transition-duration:calc(var(--_k-hover-in) * 1ms)}
21307
+ .knobs-wt-pr svg{flex:none}
21308
+ .knobs-wt-metaline .knobs-wt-pr{align-self:center;margin:0}
21309
+ .knobs-wt-kicker-lead{color:var(--_k-fg);font-weight:var(--kw-strong)}
21310
+ .knobs-wt-metaline [data-status="on"]{color:var(--_k-fg)}
21311
+ .knobs-wt-metaline [data-status="warn"]{color:var(--_k-warn)}
21312
+
21313
+ /* ——— the page's head ——— */
21314
+ .knobs-wt-head{display:flex;flex-direction:column;gap:var(--_k-space-title)}
21315
+ .knobs-wt-title{font-size:var(--_k-type-heading)!important;line-height:var(--_k-leading-heading);font-weight:var(--kw-strong)!important;text-wrap:balance}
21316
+ .knobs-wt-summary{max-width:var(--kw-measure);color:var(--_k-fg-2);text-wrap:pretty}
21317
+ .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)}
21318
+ .knobs-wt-meta>span{display:inline-flex;gap:var(--_k-space-inside);white-space:nowrap}
21319
+ .knobs-wt-meta b{font-weight:var(--_k-weight);color:var(--_k-fg-2);font-family:var(--_k-mono)}
21320
+
21321
+ /* ——— a section: a PR or topic. Its kicker leads the meta line, its title under it (16/24, under
21322
+ the page's 20/28), both on the column's edge ——— */
21323
+ .knobs-wt-section{display:flex;flex-direction:column;gap:var(--_k-space-group);min-width:0}
21324
+ .knobs-wt-sintro{display:flex;flex-direction:column;gap:var(--_k-space-title);min-width:0}
21325
+ .knobs-wt-shead{display:flex;flex-direction:column;gap:var(--_k-space-inside);min-width:0;scroll-margin-top:16px}
21326
+ .knobs-wt-heading{font-size:var(--_k-type-title)!important;line-height:var(--_k-leading-title);font-weight:var(--kw-strong)!important;text-wrap:balance}
21327
+ .knobs-wt-ssum{max-width:var(--kw-measure);color:var(--_k-fg-2);text-wrap:pretty}
21328
+ .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;
21329
+ --kw-tone:var(--_k-fg-2);color:var(--kw-tone);background:color-mix(in srgb,var(--kw-tone) 14%,transparent)}
21330
+ .knobs-wt-chip::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor}
21331
+ :is(.knobs-wt-chip,.knobs-wt-toc-glyph)[data-tone="good"]{--kw-tone:var(--kw-good)}
21332
+ :is(.knobs-wt-chip,.knobs-wt-toc-glyph)[data-tone="warn"]{--kw-tone:var(--_k-warn)}
21333
+ :is(.knobs-wt-chip,.knobs-wt-toc-glyph)[data-tone="bad"]{--kw-tone:var(--_k-danger)}
21334
+ :is(.knobs-wt-chip,.knobs-wt-toc-glyph)[data-tone="info"]{--kw-tone:var(--kw-info)}
21335
+ :is(.knobs-wt-chip,.knobs-wt-toc-glyph)[data-tone="dim"]{--kw-tone:var(--_k-fg-2)}
21336
+
21337
+ /* ——— a step: on the page by default, a group's room between steps with a hairline in the middle;
21338
+ or (cards) one card each, the one surface its pieces sit on ——— */
21339
+ .knobs-walkthrough:not([data-cards]) .knobs-wt-step:not([data-full]){padding:0;border-radius:0;background:transparent;box-shadow:none}
21340
+ .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}
21341
+ .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);
21342
+ scroll-margin-top:16px;outline:none;container:knobs-wt-step / inline-size}
21343
+ .knobs-walkthrough[data-cards]{--kw-under:var(--kw-card)}
21344
+ .knobs-walkthrough[data-cards][data-frame="flat"] .knobs-wt-step{box-shadow:inset 0 0 0 1px var(--_k-border)}
21345
+ .knobs-wt-step-head{display:flex;flex-direction:column;gap:var(--_k-space-inside);min-width:0}
21346
+ .knobs-wt-stepTitle{min-width:0;font-weight:var(--kw-strong)!important;text-wrap:balance}
21347
+ .knobs-wt-caption{max-width:var(--kw-measure);color:var(--_k-fg-2);text-wrap:pretty}
21348
+ .knobs-wt-question{font-weight:var(--kw-strong)!important;max-width:var(--kw-measure)}
21349
+ .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}
21350
+ @container knobs-wt-step (max-width:640px){.knobs-wt-media{grid-template-columns:minmax(0,1fr)}}
21351
+
21352
+ /* ——— the feed: updates and answers, newest first. Tight inside an item (4 / 8 / 4), a group's
21353
+ room between items with the hairline in the middle; no boxes ——— */
21354
+ .knobs-wt-feed{display:flex;flex-direction:column;gap:var(--_k-space-title);min-width:0;scroll-margin-top:16px}
21355
+ .knobs-wt-feed-head{display:flex;align-items:baseline;gap:var(--_k-space-sibling)}
21356
+ .knobs-wt-feed-title{font-weight:var(--kw-strong)!important}
21357
+ .knobs-wt-feed-count{font-size:var(--_k-type-small);line-height:var(--_k-leading-small);color:var(--_k-fg-3)}
21358
+ .knobs-wt-feed-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:var(--_k-space-group);min-width:0}
21359
+ .knobs-wt-fi{position:relative;display:flex;flex-direction:column;gap:var(--_k-space-sibling);min-width:0}
21360
+ .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)}
21361
+ .knobs-wt-fi-q,.knobs-wt-fi-a{display:flex;flex-direction:column;gap:var(--_k-space-inside);min-width:0}
21362
+ .knobs-wt-fi-ask{max-width:var(--kw-measure);color:var(--_k-fg);font-weight:var(--kw-strong);text-wrap:pretty}
21363
+ .knobs-wt-fi-text{max-width:var(--kw-measure);color:var(--_k-fg);text-wrap:pretty}
21364
+ .knobs-wt-fi-answer{max-width:var(--kw-measure);color:var(--_k-fg-2);text-wrap:pretty}
21365
+ :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;
21366
+ transition:color calc(var(--_k-hover-out) * 1ms)}
21367
+ .knobs-ui .knobs-wt-fi-step:is(:hover,:focus-visible){color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
21368
+ :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;
21369
+ 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)}
21370
+ .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)}
21371
+ .knobs-wt-steplink svg{width:12px;height:12px}
21372
+
21373
+ /* ——— a disclosure: a small faint toggle with a caret, no fill. The body's height springs (in
21374
+ script) and its content fades in place, top-anchored, under a soft bottom mask ——— */
21375
+ .knobs-wt-disc{display:flex;flex-direction:column;min-width:0}
21376
+ .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;
21377
+ border-radius:calc(var(--_k-radius) / 2);transition:color calc(var(--_k-hover-out) * 1ms)}
21378
+ .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)}
21379
+ .knobs-ui .knobs-wt-disc-toggle:focus-visible{box-shadow:0 0 0 2px var(--_k-focus)}
21380
+ .knobs-wt-disc-toggle svg{flex:none;width:12px;height:12px;transition:rotate var(--kw-state-ms) var(--kw-state)}
21381
+ .knobs-wt-disc[data-open]>.knobs-wt-disc-toggle svg{rotate:90deg}
21382
+ .knobs-wt-disc-count{color:var(--_k-fg-3)}
21383
+ .knobs-wt-disc-body{overflow:hidden}
21384
+ .knobs-wt-disc[data-open]>.knobs-wt-disc-body:not([data-moving]){overflow:visible}
21385
+ .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)}
21386
+ .knobs-wt-disc-content{padding-top:var(--_k-space-inside)}
21387
+ /* "More" under a caption or summary: the rest of its words, in the same voice. */
21388
+ .knobs-wt-more-text{display:flex;flex-direction:column;gap:var(--_k-space-sibling);max-width:var(--kw-measure);color:var(--_k-fg-2);text-wrap:pretty}
21389
+ /* A trail sits on the toggle's label edge: faint lines, the time trailing. */
21390
+ .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}
21391
+ .knobs-wt-trail li{display:flex;align-items:baseline;gap:var(--_k-space-sibling);min-width:0;white-space:nowrap}
21392
+ .knobs-wt-trail-text{flex:0 1 auto;min-width:0;overflow:hidden;${fade2}}
21393
+ .knobs-wt-trail-at{flex:none}
21394
+
21395
+ /* ——— a step's sent notes ——— */
21396
+ .knobs-wt-notes{display:flex;flex-direction:column}
21397
+ .knobs-wt-notes:empty{display:none}
21398
+ .knobs-wt-n{display:flex;align-items:flex-start;gap:var(--_k-space-sibling);padding:6px 0;overflow:hidden;
21399
+ 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)}
21400
+ .knobs-wt-n[data-entering]{opacity:0;filter:blur(3px)}
21401
+ .knobs-wt-n[data-leaving]{opacity:0;filter:blur(3px);padding-block:0}
21402
+ .knobs-wt-n+.knobs-wt-n{box-shadow:inset 0 1px 0 var(--_k-border-subtle)}
21403
+ .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)}
21404
+ .knobs-wt-n-meta[data-seek]{cursor:pointer}
21405
+ .knobs-wt-n-meta[data-seek]:hover{color:var(--_k-fg)}
21406
+ .knobs-wt-n-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:var(--_k-space-sibling)}
21407
+ .knobs-wt-n-voice{height:30px;max-width:320px}
21408
+ .knobs-wt-n-ink{display:block;width:120px;max-height:80px;border-radius:calc(var(--_k-radius) / 2);background:var(--_k-fill)}
21409
+ .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}
21410
+
21411
+ /* ——— the note bar, docked at the bottom of the column. It holds its own place at the page's end
21412
+ (so the last step stays clear of it), and script keeps its height as the scroller's
21413
+ scroll-padding-bottom ——— */
21414
+ .knobs-wt-dock{position:sticky;bottom:12px;z-index:40;display:grid;pointer-events:none}
21415
+ .knobs-wt-dock>*{pointer-events:auto}
21416
+ .knobs-wt-dock>:is(.knobs-wt-notebar,.knobs-wt-launch){grid-area:1/1}
21417
+ .knobs-wt-dock[data-mode="docked"]>.knobs-wt-launch{display:none}
21418
+ /* Collapsed: the launcher and the bar swap in place, a quick fade with a little scale and blur
21419
+ from the launcher's corner; the exit is the shorter. */
21420
+ :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);
21421
+ 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%;
21422
+ 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)}
21423
+ .knobs-ui .knobs-wt-launch:is(:hover,:focus-visible){color:var(--_k-fg)}
21424
+ .knobs-ui .knobs-wt-launch:focus-visible{box-shadow:var(--_k-focus-ring)}
21425
+ .knobs-wt-launch svg{width:16px;height:16px}
21426
+ .knobs-wt-launch kbd{font:inherit;font-weight:var(--_k-weight);color:var(--_k-fg-3)}
21427
+ .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)}
21428
+ .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;
21429
+ transition-duration:calc(var(--kw-quick-ms) * .8)}
21430
+ .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);
21431
+ box-shadow:var(--_ku-pane-ring),var(--_k-shadow-overlay)}
21432
+ .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}}
21433
+ .knobs-wt-row{display:flex;align-items:flex-end;gap:4px}
21434
+ :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;
21435
+ border-radius:0;margin:0;padding:0;min-width:0;box-shadow:none;outline:none}
21436
+ .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)}
21437
+ .knobs-ui .knobs-wt-input::placeholder{color:var(--_k-fg-3)}
21438
+ .knobs-wt-attach{display:flex;gap:4px;align-items:center;align-self:center}
21439
+ .knobs-wt-attach:empty{display:none}
21440
+ .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}
21441
+ .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}
21442
+ .knobs-ui .knobs-wt-x:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
21443
+ .knobs-wt-x svg{width:12px;height:12px}
21444
+ .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);
21445
+ color:var(--_k-fg-2);cursor:pointer;transition:background .15s,color .15s}
21446
+ .knobs-ui .knobs-wt-tool:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
21447
+ .knobs-ui .knobs-wt-tool[aria-pressed="true"]{background:var(--_k-fill-selected);color:var(--_k-fg)}
21448
+ .knobs-wt-tool svg{width:16px;height:16px}
21449
+ .knobs-wt-rec{font-size:var(--_k-type-small)}
21450
+ .knobs-wt-rec:empty{display:none}
21451
+ .knobs-ui .knobs-wt-tool[data-send]{color:var(--_k-fg-3)}
21452
+ .knobs-ui .knobs-wt-tool[data-send][data-ready]{background:var(--_k-accent);color:var(--_k-bg)}
21453
+ /* Back: in place over the bar for a few seconds after a note moves the page on. */
21454
+ .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;
21455
+ 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;
21456
+ 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)}
21457
+ .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)}
21458
+ .knobs-ui .knobs-wt-back:hover{background:var(--_k-fill-selected)}
21459
+ .knobs-wt-back svg{flex:none;width:12px;height:12px}
21460
+ .knobs-wt-back-label{min-width:0;overflow:hidden;white-space:nowrap;${fade2}}
21461
+
21462
+ /* ——— ink over a step's media ——— */
21463
+ .knobs-wt-media,.knobs-wt-options{position:relative}
21464
+ .knobs-wt-ink{position:absolute;inset:0;z-index:5;width:100%;height:100%;overflow:visible;pointer-events:none;touch-action:none}
21465
+ .knobs-wt-ink[data-on]{pointer-events:auto;cursor:crosshair}
21466
+ .knobs-wt-ink path{fill:none;stroke:var(--kw-ink);stroke-width:3px;vector-effect:non-scaling-stroke;stroke-linecap:round;stroke-linejoin:round}
21467
+
21468
+ /* ——— full-window: the step over the page above the note bar, its media filling it; its place held
21469
+ in the page. The one raised surface ——— */
21470
+ .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}
21471
+ .knobs-wt-scrim[data-on]{pointer-events:auto}
21472
+ .knobs-wt-hold-card{flex:none}
21473
+ .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)}
21474
+ .knobs-wt-step[data-full]::before{content:none}
21475
+ .knobs-wt-step[data-full]>:not(.knobs-wt-media){flex:none}
21476
+ .knobs-wt-step[data-full] .knobs-wt-notes{display:none}
21477
+ .knobs-wt-step[data-full]>.knobs-wt-media{flex:1;min-height:0;grid-auto-rows:minmax(0,1fr)}
21478
+ .knobs-wt-step[data-full] .knobs-wt-slot{display:flex;flex-direction:column;min-height:0}
21479
+ .knobs-wt-step[data-full] .knobs-wt-body{flex:1;min-height:0}
21480
+ .knobs-wt-step[data-full] .knobs-wt-fit{height:100%;transition:none}
21481
+ .knobs-wt-step[data-full] .knobs-wt-frame{height:100%;aspect-ratio:auto}
21482
+
21483
+ /* ——— a taste test: the options side by side, Kit's pick checked and the rest stepping back. On
21484
+ the step's card, so no fill of their own ——— */
21485
+ .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)}
21486
+ @container knobs-wt-step (max-width:640px){.knobs-wt-options{grid-template-columns:minmax(0,1fr)}}
21487
+ .knobs-wt-option{position:relative;display:flex;flex-direction:column;gap:var(--_k-space-sibling);min-width:0}
21488
+ .knobs-wt-option>:not(.knobs-wt-pick){transition:opacity var(--kw-snappy-ms) var(--kw-snappy)}
21489
+ .knobs-wt-options[data-has-pick] .knobs-wt-option:not([data-picked])>:not(.knobs-wt-pick){opacity:.55}
21490
+ .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;
21491
+ transition:background calc(var(--_k-hover-out) * 1ms)}
21492
+ .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)}
21493
+ .knobs-wt-letter{flex:none;font-weight:var(--kw-strong);color:var(--_k-fg)}
21494
+ .knobs-wt-olabel{flex:1;min-width:0;font-weight:var(--kw-strong);color:var(--_k-fg)}
21495
+ .knobs-wt-suggested{flex:none;font-size:var(--_k-type-small);color:var(--_k-fg-3)}
21496
+ .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;
21497
+ transition:background var(--kw-snappy-ms) var(--kw-snappy),color var(--kw-snappy-ms) var(--kw-snappy)}
21498
+ .knobs-wt-check svg{width:12px;height:12px}
21499
+ .knobs-wt-option[data-picked] .knobs-wt-check{background:var(--_k-accent);color:var(--_k-bg)}
21500
+ .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);
21501
+ font:var(--kw-strong) var(--_k-type-small)/1 var(--_k-font);opacity:.5;transition:opacity .15s}
21502
+ .knobs-wt-step[data-current] .knobs-wt-key{opacity:1}
21503
+ .knobs-wt-ocap{color:var(--_k-fg-2);max-width:var(--kw-measure)}
21504
+
21505
+ /* ——— the rail: progress, contents, export ——— */
21506
+ .knobs-wt-rail{position:sticky;top:var(--_k-pad-page);display:flex;flex-direction:column;gap:var(--_k-space-title);max-height:calc(100vh - var(--_k-pad-page) * 2);min-height:0;font-size:var(--_k-type-small);line-height:var(--_k-leading-small)}
21507
+ .knobs-wt-progress{display:flex;flex-direction:column;gap:var(--_k-space-sibling)}
21508
+ .knobs-wt-count{display:flex;align-items:baseline;justify-content:space-between;color:var(--_k-fg-2)}
21509
+ .knobs-wt-count b{color:var(--_k-fg);font-weight:var(--kw-strong)}
21510
+ .knobs-wt-bar{position:relative;height:4px;border-radius:2px;overflow:hidden;background:var(--_k-fill)}
21511
+ .knobs-wt-fill{position:absolute;inset:0;border-radius:inherit;background:var(--_k-accent);transform-origin:0 50%;transform:scaleX(var(--kw-done,0))}
21512
+ .knobs-walkthrough[data-settled] .knobs-wt-fill{transition:transform var(--kw-calm-ms) var(--kw-calm)}
21513
+ /* The contents: every row on one text edge and in one colour, title rows (32 high, the control
21514
+ radius) and the steps under them (20 high, half of it), bled out so their text sits on the
21515
+ rail's edge; the selection pill slides to the current step, and base rows never brighten. */
21516
+ .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}
21517
+ .knobs-wt-toc::-webkit-scrollbar{display:none}
21518
+ .knobs-wt-toc-group{display:flex;flex-direction:column;min-width:0}
21519
+ .knobs-ui .knobs-wt-toc-row{position:relative;display:grid;grid-template-columns:minmax(0,1fr) auto;column-gap:var(--_k-space-sibling);align-items:center;flex:none;width:100%;height:32px;
21520
+ 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(--_k-weight);text-align:left;white-space:nowrap;cursor:pointer;
21521
+ transition:color calc(var(--_k-hover-out) * 1ms)}
21522
+ .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)}
21523
+ .knobs-ui .knobs-wt-toc-row:is(:hover,:focus-visible){color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
21524
+ .knobs-ui .knobs-wt-toc-row:focus-visible{box-shadow:inset 0 0 0 1px var(--_k-focus)}
21525
+ .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)}
21526
+ .knobs-wt-toc-label{min-width:0;overflow:hidden;${fade2}}
21527
+ .knobs-pick-copy .knobs-wt-toc-end{color:var(--_k-fg-2)}
21528
+ /* A step's state: an open ring, a filled dot once answered, nothing to answer a faint ring. */
21529
+ .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);
21530
+ transition:background var(--kw-snappy-ms) var(--kw-snappy),box-shadow var(--kw-snappy-ms) var(--kw-snappy)}
21531
+ .knobs-wt-toc-glyph[data-tone="accent"]{--kw-tone:var(--_k-accent)}
21532
+ .knobs-wt-toc-glyph[data-done]{background:var(--kw-tone)}
21533
+ .knobs-wt-toc-glyph[data-tone="none"]{box-shadow:inset 0 0 0 1px var(--_k-border)}
21534
+ .knobs-wt-actions{display:flex;flex-wrap:wrap;gap:4px}
21535
+ .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;
21536
+ transition:background .15s,color .15s}
21537
+ .knobs-ui .knobs-wt-btn:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
21538
+ .knobs-ui .knobs-wt-btn[data-primary]{background:var(--_k-fill-selected);color:var(--_k-fg)}
21539
+ .knobs-ui .knobs-wt-btn[data-primary]:is(:hover,:focus-visible){background:color-mix(in srgb,var(--_k-fg) 16%,transparent)}
21540
+ .knobs-wt-keys{color:var(--_k-fg-3);font-size:var(--_k-type-small);line-height:var(--_k-leading-small)}
21541
+ .knobs-wt-keys kbd{font:inherit;color:var(--_k-fg-2)}
21542
+ /* Narrow: the contents go; with review on, the rail becomes a bar along the top with the
21543
+ progress and the export. */
21544
+ @container knobs-wt (max-width:960px){
21545
+ .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)}
21546
+ .knobs-wt-progress{flex:1}.knobs-wt-toc,.knobs-wt-keys{display:none}
21547
+ .knobs-walkthrough:not([data-review]) .knobs-wt-rail{display:none}
21548
+ .knobs-wt-step,.knobs-wt-shead,.knobs-wt-feed{scroll-margin-top:60px}
21549
+ }
21550
+
21551
+ /* ——— media slots. Only a picture's frame (a quiet fill while it loads or letterboxes) and a code
21552
+ block (a well) are bounded; a metric, a callout, a table, a stack and a history sit on the page,
21553
+ set apart by space, hairlines and their own colours. On a card (.knobs-surface) code drops its
21554
+ well for hairlines, so nothing draws a fill and a ring of its own there ——— */
21555
+ .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);
21556
+ 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);
21557
+ --kw-good:var(--k-walkthrough-good,var(--_k-ok));--kw-info:var(--k-walkthrough-info,var(--_k-info));--kw-frame-r:var(--_k-radius-panel)}
21558
+ .knobs-surface .knobs-wt-slot{--kw-frame-r:calc(var(--_k-radius-panel) - 4px)}
21559
+ /* A slot's label: sentence case as written, small and faint, never a heading. */
21560
+ .knobs-wt-slot-label{display:block;min-width:0;overflow:hidden;white-space:nowrap;font:var(--_k-weight) var(--_k-type-small)/var(--_k-leading-small) var(--_k-font);letter-spacing:normal;text-transform:none;color:var(--_k-fg-3);${fade2}}
21561
+ .knobs-wt-body{min-width:0}
21562
+ /* One frame for every slot that has one (a picture, a video, a code block, a compare's stage, a
21563
+ slot still loading): the panel radius and a quiet fill, no ring; concentric on a card. */
21564
+ .knobs-wt-slot .knobs-cmp-stage{border-radius:var(--kw-frame-r)}
21565
+ /* A compare is exactly as tall as its pictures at this width, so there's no letterbox: the two
21566
+ sides edge to edge (side) or one (overlaid), plus its mode bar, the timeline and their two gaps
21567
+ (92 px), and the frame strip with its gap (158 px). */
21568
+ .knobs-wt-fit{width:100%;height:calc(100cqw / 2 / var(--kw-aspect,1.7778) + var(--kw-chrome,92px))}
21569
+ .knobs-wt-fit[data-overlay]{height:calc(100cqw / var(--kw-aspect,1.7778) + var(--kw-chrome,92px))}
21570
+ .knobs-wt-fit[data-strip]{--kw-chrome:158px}
21571
+ .knobs-wt-fit[data-settled]{transition:height var(--k-wt-calm-ms,${calm.duration}ms) ${calm.easing}}
21572
+ .knobs-wt-frame{position:relative;width:100%;overflow:hidden;border-radius:var(--kw-frame-r);background:var(--_k-fill);aspect-ratio:var(--kw-aspect,16/9)}
21573
+ .knobs-wt-frame>:is(video,img){position:absolute;inset:0;display:block;width:100%;height:100%;object-fit:contain;margin:0;border:0}
21574
+ .knobs-wt-frame[data-natural]{aspect-ratio:auto;background:transparent}
21575
+ .knobs-wt-frame[data-natural]>img{position:static;height:auto}
21576
+ .knobs-wt-code{margin:0;padding:var(--_k-pad-card);max-height:420px;overflow:auto;border-radius:var(--kw-frame-r);background:var(--_k-fill);color:var(--_k-fg);
21577
+ font:var(--_k-type-label)/var(--_k-leading-body) var(--_k-mono);white-space:pre;tab-size:2;scrollbar-width:thin}
21578
+ .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)}
21579
+ .knobs-wt-code[data-terminal]{white-space:pre-wrap;overflow-wrap:anywhere}
21580
+ .knobs-wt-prompt{color:var(--_k-fg-3);user-select:none;-webkit-user-select:none}
21581
+ .knobs-wt-cmd{color:var(--_k-fg);font-weight:var(--_k-weight-label)}
21582
+ .knobs-wt-out{color:var(--_k-fg-2)}
21583
+ .knobs-wt-metric{display:flex;flex-direction:column;gap:var(--_k-space-sibling)}
21584
+ .knobs-wt-mlabel{color:var(--_k-fg-2)}
21585
+ .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"}
21586
+ .knobs-wt-mfrom{color:var(--_k-fg-3)}
21587
+ .knobs-wt-mto{font-size:var(--_k-type-title);line-height:var(--_k-leading-title);font-weight:var(--_k-weight-label)}
21588
+ .knobs-wt-mof{color:var(--_k-fg-3)}
21589
+ .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)}
21590
+ .knobs-wt-mdelta[data-tone="good"]{--kw-tone:var(--kw-good)}
21591
+ .knobs-wt-mdelta[data-tone="bad"]{--kw-tone:var(--_k-danger)}
21592
+ .knobs-wt-mtrack{position:relative;height:4px;border-radius:2px;background:color-mix(in srgb,var(--_k-fg) 8%,transparent)}
21593
+ .knobs-wt-mtrack>span{position:absolute;top:0;bottom:0;left:0;border-radius:inherit}
21594
+ .knobs-wt-mtrack>.knobs-wt-mafter{background:var(--_k-accent)}
21595
+ .knobs-wt-mtrack>.knobs-wt-mbefore{background:color-mix(in srgb,var(--_k-fg) 30%,transparent)}
21596
+ .knobs-wt-callout{--kw-tone:var(--kw-info);display:flex;gap:var(--_k-space-sibling);max-width:var(--kw-measure);color:var(--_k-fg)}
21597
+ .knobs-wt-callout::before{content:"";flex:none;width:6px;height:6px;margin-top:7px;border-radius:50%;background:var(--kw-tone)}
21598
+ .knobs-wt-callout[data-tone="good"]{--kw-tone:var(--kw-good)}
21599
+ .knobs-wt-callout[data-tone="warn"]{--kw-tone:var(--_k-warn)}
21600
+ .knobs-wt-callout[data-tone="bad"]{--kw-tone:var(--_k-danger)}
21601
+ .knobs-wt-text{max-width:var(--kw-measure);color:var(--_k-fg-2)}
21602
+ .knobs-wt-hold{border-radius:var(--kw-frame-r);background:var(--_k-fill)}
21603
+ .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)}
21604
+
21605
+ /* ——— data blocks: history, stack, table, chapters. On the step's card, so no card of their own:
21606
+ hairlines and fills only. Tones read the status tokens; hover fades are the panel's own. ——— */
21607
+ :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)}
21608
+ :where(.knobs-wt-slot) [data-tone="good"]{--kw-tone:var(--kw-good)}
21609
+ :where(.knobs-wt-slot) [data-tone="warn"]{--kw-tone:var(--_k-warn)}
21610
+ :where(.knobs-wt-slot) [data-tone="bad"]{--kw-tone:var(--_k-danger)}
21611
+ :where(.knobs-wt-slot) [data-tone="info"]{--kw-tone:var(--kw-info)}
21612
+ :where(.knobs-wt-slot) [data-tone="dim"]{--kw-tone:var(--_k-fg-3)}
21613
+ .knobs-wt-tags{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}
21614
+ .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);
21615
+ text-decoration:none;white-space:nowrap;transition:background calc(var(--_k-hover-out) * 1ms),color calc(var(--_k-hover-out) * 1ms)}
21616
+ .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)}
21617
+
21618
+ /* A history: dates down the left, a spine of tone dots joined by a hairline (broken around each
21619
+ dot), the event beside it. Inferred events take a ring for a dot and a dashed tag. */
21620
+ .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)}
21621
+ .knobs-wt-hgroup{display:flex;flex-direction:column;gap:6px;min-width:0}
21622
+ .knobs-wt-hhead{display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 10px}
21623
+ .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)}
21624
+ .knobs-wt-hsum{color:var(--_k-fg-3);font-size:var(--_k-type-small);font-variant-numeric:tabular-nums}
21625
+ .knobs-wt-hlist{list-style:none;margin:0;padding:0;display:flex;flex-direction:column}
21626
+ .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}
21627
+ .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)}
21628
+ .knobs-wt-hage{color:var(--_k-fg-3);font-size:var(--_k-type-small);line-height:var(--_k-leading-small)}
21629
+ .knobs-wt-hspine{grid-area:spine;position:relative;align-self:stretch;margin-block:-5px}
21630
+ .knobs-wt-hspine::before,.knobs-wt-hspine::after{content:"";position:absolute;left:6.5px;width:1px;background:var(--kw-rail)}
21631
+ .knobs-wt-hspine::before{top:0;height:calc(var(--kw-hy) - 6px)}
21632
+ .knobs-wt-hspine::after{top:calc(var(--kw-hy) + 6px);bottom:0}
21633
+ .knobs-wt-hev:first-child .knobs-wt-hspine::before,.knobs-wt-hev:last-child .knobs-wt-hspine::after{content:none}
21634
+ .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))}
21635
+ .knobs-wt-hev[data-type="release"] .knobs-wt-hdot{border-radius:1.5px;rotate:45deg;scale:.9}
21636
+ .knobs-wt-hev[data-type="audit"] .knobs-wt-hdot{border-radius:1.5px}
21637
+ .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))}
21638
+ .knobs-wt-hmain{grid-area:main;display:flex;flex-direction:column;gap:2px;min-width:0}
21639
+ .knobs-wt-htitle{display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 8px;font-weight:var(--_k-weight-label);color:var(--_k-fg)}
21640
+ .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}
21641
+ .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}
21642
+ .knobs-ui .knobs-wt-hdetail{margin:0;max-width:var(--k-walkthrough-measure,68ch);color:var(--_k-fg-2)}
21643
+ @container knobs-wt-slot (max-width:520px){
21644
+ .knobs-wt-hev{grid-template-columns:14px minmax(0,1fr);grid-template-areas:"spine when" "spine main"}
21645
+ .knobs-wt-hwhen{flex-direction:row;flex-wrap:wrap;align-items:baseline;column-gap:8px}
21646
+ }
21647
+
21648
+ /* A stack: the trunk at the top, each PR hanging off the branch it merges into, rails drawn as a
21649
+ git graph (solid, so where two meet they don't darken). A PR with a step goes there on a click. */
21650
+ .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)}
21651
+ .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);
21652
+ border-radius:max(0px,calc(var(--_k-radius-panel) - 8px));transition:background calc(var(--_k-hover-out) * 1ms)}
21653
+ .knobs-wt-srow[data-trunk]{--kw-spb:0px;grid-template-columns:auto minmax(0,1fr)}
21654
+ .knobs-wt-srow[data-go]{cursor:pointer}
21655
+ .knobs-wt-srow[data-go]:hover{background:var(--_ku-wash);transition-duration:calc(var(--_k-hover-in) * 1ms)}
21656
+ .knobs-wt-lanes{display:flex;align-self:stretch;margin-block:calc(var(--kw-spt) * -1) calc(var(--kw-spb) * -1)}
21657
+ .knobs-wt-lane{position:relative;flex:none;width:16px}
21658
+ .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)}
21659
+ .knobs-wt-lane[data-down]::before{top:calc(var(--kw-sy) + 3.5px)}
21660
+ .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}
21661
+ .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)}
21662
+ .knobs-wt-srow[data-trunk] .knobs-wt-lane[data-rail="node"]::after{background:var(--_k-fg-2)}
21663
+ .knobs-wt-trunk{font:var(--_k-weight-label) var(--_k-type-label)/var(--kw-line) var(--_k-mono);color:var(--_k-fg-2)}
21664
+ .knobs-wt-smain{display:flex;flex-direction:column;gap:1px;min-width:0}
21665
+ .knobs-wt-sline{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 8px}
21666
+ .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}
21667
+ .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}
21668
+ .knobs-ui a.knobs-wt-spr:is(:hover,:focus-visible){color:var(--_k-fg);text-decoration:underline}
21669
+ .knobs-ui .knobs-wt-stitle{font-weight:var(--_k-weight-label);line-height:var(--kw-line);color:var(--_k-fg);text-align:left}
21670
+ .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)}
21671
+ .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)}
21672
+ .knobs-ui .knobs-wt-ssummary{margin:0;max-width:var(--k-walkthrough-measure,68ch);color:var(--_k-fg-2)}
21673
+ .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)}
21674
+ .knobs-wt-delta{display:inline-flex;gap:6px}
21675
+ .knobs-wt-add{color:var(--_k-diff-add)}
21676
+ .knobs-wt-del{color:var(--_k-diff-del)}
21677
+ .knobs-wt-sstatus{margin-top:calc((var(--kw-line) - 20px) / 2)}
21678
+ .knobs-wt-stack[data-flat] .knobs-wt-srow{grid-template-columns:minmax(0,1fr) auto}
21679
+ .knobs-wt-stack[data-flat] .knobs-wt-srow+.knobs-wt-srow{box-shadow:inset 0 1px 0 var(--_k-border)}
21680
+ @container knobs-wt-slot (max-width:520px){
21681
+ .knobs-wt-srow:not([data-trunk]){grid-template-columns:auto minmax(0,1fr)}
21682
+ .knobs-wt-stack[data-flat] .knobs-wt-srow{grid-template-columns:minmax(0,1fr)}
21683
+ .knobs-wt-sstatus{grid-column:-2;justify-self:start;margin-top:4px}
20695
21684
  }
20696
- var grid2 = (v) => Math.round(v * 64) / 64;
20697
- function chapterPills(host, chapters, io) {
20698
- const doc = host.ownerDocument;
20699
- const win = doc.defaultView;
20700
- const bar = el(doc, "div", "knobs-wt-chapters");
20701
- bar.setAttribute("role", "group");
20702
- bar.setAttribute("aria-label", "Chapters");
20703
- const mark = el(doc, "span", "knobs-wt-chapter-mark");
20704
- mark.setAttribute("aria-hidden", "true");
20705
- bar.append(mark);
20706
- const pills = chapters.map((c) => {
20707
- const b = el(doc, "button", "knobs-wt-chapter");
20708
- b.type = "button";
20709
- b.append(text(doc, "span", "knobs-wt-chapter-time", clock(c.start)));
20710
- b.append(text(doc, "span", "knobs-wt-chapter-label", c.label));
20711
- b.addEventListener("click", () => {
20712
- io.onEvent?.({ what: "seek", time: Math.round(c.start * 100) / 100, detail: c.label });
20713
- io.seek(c.start);
20714
- sync();
20715
- });
20716
- bar.append(b);
20717
- return b;
20718
- });
20719
- host.append(bar);
20720
- const x = new Spring(0, MOTION.calm.omega, MOTION.calm.zeta, 0.25);
20721
- const y = new Spring(0, MOTION.calm.omega, MOTION.calm.zeta, 0.25);
20722
- const w = new Spring(0, MOTION.calm.omega, MOTION.calm.zeta, 0.25);
20723
- const draw = () => {
20724
- const left = grid2(x.x);
20725
- mark.style.transform = `translate(${left}px,${grid2(y.x)}px)`;
20726
- mark.style.width = `${grid2(x.x + w.x) - left}px`;
20727
- };
20728
- const loop = frameLoop(win, (dt) => {
20729
- x.step(dt);
20730
- y.step(dt);
20731
- w.step(dt);
20732
- draw();
20733
- return !x.resting || !y.resting || !w.resting;
20734
- });
20735
- let at = -2;
20736
- let shown = false;
20737
- const place = (snap) => {
20738
- const p = pills[at];
20739
- if (!p) {
20740
- mark.removeAttribute("data-on");
20741
- pills.forEach((b) => b.removeAttribute("aria-current"));
20742
- shown = false;
20743
- return;
20744
- }
20745
- pills.forEach((b, i) => i === at ? b.setAttribute("aria-current", "true") : b.removeAttribute("aria-current"));
20746
- const to = [p.offsetLeft, p.offsetTop, p.offsetWidth];
20747
- if (snap || !shown || reducedMotion(win)) {
20748
- x.snap(to[0]);
20749
- y.snap(to[1]);
20750
- w.snap(to[2]);
20751
- draw();
20752
- } else {
20753
- x.set(to[0]);
20754
- y.set(to[1]);
20755
- w.set(to[2]);
20756
- loop.wake();
20757
- }
20758
- mark.style.height = `${p.offsetHeight}px`;
20759
- mark.dataset.on = "";
20760
- shown = true;
20761
- };
20762
- const sync = () => {
20763
- const next = chapterAt(chapters, io.now());
20764
- if (next === at)
20765
- return;
20766
- at = next;
20767
- place(false);
20768
- };
20769
- sync();
20770
- const ro = "ResizeObserver" in win ? new win.ResizeObserver(() => place(true)) : null;
20771
- ro?.observe(bar);
20772
- return {
20773
- sync,
20774
- get current() {
20775
- return chapters[at] ?? null;
20776
- },
20777
- dispose() {
20778
- loop.stop();
20779
- ro?.disconnect();
20780
- bar.remove();
20781
- }
20782
- };
21685
+
21686
+ /* A table: a real one, its head stuck while it scrolls, both ways; edges fade where there's more
21687
+ (the top one under the head), and thin bars show on hover. */
21688
+ /* Too narrow for its columns, each row becomes a block: its primary cell titling the rest as term
21689
+ and value, the blocks parted by hairlines. Scrolling sideways is the last resort (stack: never). */
21690
+ .knobs-wt-tablebox{min-width:0}
21691
+ .knobs-wt-tablebox[data-measure] table{width:min-content}
21692
+ .knobs-wt-tstack{display:none;flex-direction:column;min-width:0}
21693
+ .knobs-wt-tablebox[data-stacked]>.knobs-wt-table{display:none}
21694
+ .knobs-wt-tablebox[data-stacked]>.knobs-wt-tstack{display:flex}
21695
+ .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)}
21696
+ .knobs-wt-trow:first-child{padding-top:0}
21697
+ .knobs-wt-trow:last-of-type{padding-bottom:0;box-shadow:none}
21698
+ .knobs-wt-tline{display:flex;align-items:baseline;justify-content:space-between;gap:var(--_k-space-title);min-width:0}
21699
+ .knobs-wt-tbadges{flex:none;display:flex;align-items:center;gap:var(--_k-space-inside)}
21700
+ .knobs-wt-ttitle{flex:1 1 auto;min-width:0;color:var(--_k-fg);font-weight:var(--_k-weight-label);overflow-wrap:anywhere}
21701
+ .knobs-wt-ttitle[data-mono]{font-family:var(--_k-mono);font-size:var(--_k-type-label)}
21702
+ .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}
21703
+ .knobs-wt-tterms dt{color:var(--_k-fg-3);font-size:var(--_k-type-small)}
21704
+ .knobs-wt-tterms dd{min-width:0;color:var(--_k-fg-2);overflow-wrap:anywhere}
21705
+ .knobs-wt-tterms dd[data-mono]{font-family:var(--_k-mono);font-size:var(--_k-type-label);color:var(--_k-fg)}
21706
+ .knobs-wt-tcaption{padding-top:var(--_k-space-sibling);color:var(--_k-fg-3);font-size:var(--_k-type-small)}
21707
+ @property --kw-fade-l{syntax:"<number>";inherits:false;initial-value:1}
21708
+ @property --kw-fade-r{syntax:"<number>";inherits:false;initial-value:1}
21709
+ @keyframes kw-fade-l{from{--kw-fade-l:1}to{--kw-fade-l:0}}
21710
+ @keyframes kw-fade-r{from{--kw-fade-r:0}to{--kw-fade-r:1}}
21711
+ .knobs-wt-table{max-height:520px;overflow:auto;overscroll-behavior-x:contain;outline:none;scrollbar-width:thin;scrollbar-color:transparent transparent;
21712
+ transition:scrollbar-color calc(var(--_k-hover-out) * 1ms)}
21713
+ .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)}
21714
+ .knobs-wt-table:focus-visible{box-shadow:inset 0 0 0 2px var(--_k-focus);border-radius:var(--kw-frame-r)}
21715
+ @supports (animation-timeline:scroll()){
21716
+ .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;
21717
+ 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%;
21718
+ --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)));
21719
+ --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)));
21720
+ -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}
20783
21721
  }
21722
+ .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)}
21723
+ .knobs-wt-table caption{caption-side:bottom;padding-top:8px;text-align:left;color:var(--_k-fg-3);font-size:var(--_k-type-small)}
21724
+ .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;
21725
+ background:var(--kw-under,var(--_k-pane-bg));box-shadow:inset 0 -1px 0 var(--_k-border)}
21726
+ .knobs-wt-table td{padding:8px 12px;vertical-align:baseline;box-shadow:inset 0 -1px 0 var(--_k-border)}
21727
+ .knobs-wt-table tr:last-child td{box-shadow:none}
21728
+ .knobs-wt-table :is(th,td):first-child{padding-left:0}
21729
+ .knobs-wt-table :is(th,td):last-child{padding-right:0}
21730
+ .knobs-wt-table td:first-child{color:var(--_k-fg)}
21731
+ .knobs-wt-table [data-align="right"]{text-align:right}
21732
+ .knobs-wt-table [data-align="center"]{text-align:center}
21733
+ .knobs-wt-table td[data-mono]{font-family:var(--_k-mono);font-size:var(--_k-type-label);color:var(--_k-fg);white-space:nowrap}
21734
+ .knobs-wt-table td[data-long]{min-width:24ch}
21735
+ .knobs-ui :is(.knobs-wt-table,.knobs-wt-tstack) a.knobs-wt-chip{text-decoration:none}
21736
+ .knobs-wt-ctone{color:var(--kw-tone)}
21737
+
21738
+ /* Chapters: a readout list under a video or compare: number, label, start; the selection pill
21739
+ on the one playing, its rows bled out so their text sits on the media's edge. */
21740
+ .knobs-wt-body:has(>.knobs-wt-chapters){display:flex;flex-direction:column;gap:var(--_k-space-title)}
21741
+ .knobs-wt-chapters{position:relative;display:flex;flex-direction:column;margin:0 calc(var(--_k-space-sibling) * -1)}
21742
+ .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);
21743
+ 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)}
21744
+ .knobs-ui .knobs-wt-chapter:is(:hover,:focus-visible){color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
21745
+ .knobs-ui .knobs-wt-chapter:focus-visible{box-shadow:inset 0 0 0 1px var(--_k-focus)}
21746
+ .knobs-wt-chapter-n{font-size:var(--_k-type-small);color:var(--_k-fg-3)}
21747
+ .knobs-wt-chapter-label{min-width:0;overflow:hidden;${fade2}}
21748
+ .knobs-wt-chapter-time{font:var(--_k-weight) var(--_k-type-small)/1 var(--_k-mono);color:var(--_k-fg-3)}
21749
+ .knobs-pick-copy :is(.knobs-wt-chapter-n,.knobs-wt-chapter-time){color:var(--_k-fg-2)}
21750
+ @media(prefers-reduced-motion:reduce){:is(.knobs-walkthrough,.knobs-wt-slot) :where(*,::before,::after){transition-duration:.15s!important;transition-property:opacity,background,color!important}}
21751
+ @media(prefers-reduced-motion:reduce){.knobs-wt-disc-content{transition:opacity .15s}}
21752
+ `;
20784
21753
 
20785
21754
  // src/ui/walkthrough-media.ts
20786
21755
  function injectWalkthroughStyles(doc) {
@@ -21309,7 +22278,7 @@ function mediaSlot(container, options) {
21309
22278
  }
21310
22279
  };
21311
22280
  }
21312
- var CHECK3 = svg('<path d="m3.5 8.4 3 3 6-6.6"/>');
22281
+ var CHECK3 = icon("check", { small: true });
21313
22282
 
21314
22283
  // src/ui/walkthrough.ts
21315
22284
  var el2 = (doc, tag, cls) => {
@@ -21327,16 +22296,20 @@ var lookOf2 = (o) => ({
21327
22296
  ...o.preset ? { preset: o.preset } : {},
21328
22297
  ...o.theme ? { theme: o.theme } : {}
21329
22298
  });
21330
- var grid3 = (v) => Math.round(v * 64) / 64;
22299
+ var grid2 = (v) => Math.round(v * 64) / 64;
21331
22300
  var ICON = {
21332
- full: svg('<path d="M3 6.5V3h3.5M13 6.5V3H9.5M3 9.5V13h3.5M13 9.5V13H9.5"/>'),
21333
- exit: svg('<path d="M6.5 3v3.5H3M9.5 3v3.5H13M6.5 13V9.5H3M9.5 13V9.5H13"/>'),
21334
- 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"/>'),
21335
- 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"/>'),
22301
+ full: icon("cornersOut"),
22302
+ exit: icon("cornersIn"),
22303
+ pen: icon("pencilSimple"),
22304
+ mic: icon("microphone"),
21336
22305
  stop: svg('<rect x="4.5" y="4.5" width="7" height="7" rx="1.5" fill="currentColor" stroke="none"/>'),
21337
- send: svg('<path d="M8 13V3.5M4 7.5 8 3.5l4 4"/>'),
21338
- close: svg('<path d="M4.5 4.5l7 7M11.5 4.5l-7 7"/>')
22306
+ send: icon("paperPlaneRight"),
22307
+ close: icon("x", { small: true }),
22308
+ back: icon("arrowLeft", { small: true }),
22309
+ out: icon("arrowUpRight", { small: true })
21339
22310
  };
22311
+ var DOCK_INSET = 12;
22312
+ var DOCK_GAP = 12;
21340
22313
  var CALM2 = springMotion(SPRINGS.calm);
21341
22314
  var SMOOTH = springMotion(SPRINGS.smooth);
21342
22315
  var BACK_MS = 4500;
@@ -21352,16 +22325,25 @@ function tasteTest(container, options) {
21352
22325
  ...o.media !== undefined ? { media: fillMedia(o.media) } : {},
21353
22326
  suggested: o.suggested === true
21354
22327
  });
21355
- let picked = options.picked ?? null;
22328
+ const pickable = options.pickable !== false;
22329
+ let picked = pickable ? options.picked ?? null : null;
21356
22330
  const root = el2(doc, "div", "knobs-wt-options");
21357
- root.setAttribute("role", "radiogroup");
21358
- root.setAttribute("aria-label", "Options");
22331
+ if (pickable) {
22332
+ root.setAttribute("role", "radiogroup");
22333
+ root.setAttribute("aria-label", "Options");
22334
+ } else
22335
+ root.dataset.static = "";
21359
22336
  root.style.setProperty("--kw-n", String(Math.min(list.length, 3)));
21360
22337
  if (!container.closest(".knobs-ui"))
21361
22338
  applyLook2(root, options);
21362
22339
  const slots = [];
21363
22340
  const tiles = list.map((o, i) => {
21364
22341
  const tile = el2(doc, "div", "knobs-wt-option");
22342
+ if (!pickable) {
22343
+ const name = el2(doc, "span", "knobs-wt-slot-label");
22344
+ name.textContent = o.label;
22345
+ tile.append(name);
22346
+ }
21365
22347
  const pick = el2(doc, "button", "knobs-wt-pick");
21366
22348
  pick.type = "button";
21367
22349
  pick.setAttribute("role", "radio");
@@ -21390,7 +22372,8 @@ function tasteTest(container, options) {
21390
22372
  api.pick(next);
21391
22373
  options.onPick?.(next);
21392
22374
  });
21393
- tile.append(pick);
22375
+ if (pickable)
22376
+ tile.append(pick);
21394
22377
  if (o.media)
21395
22378
  slots.push(mediaSlot(tile, {
21396
22379
  ...look,
@@ -21469,8 +22452,13 @@ function walkthrough(container, options) {
21469
22452
  let count = null;
21470
22453
  let fill = null;
21471
22454
  let tocEl = null;
21472
- let mark = null;
22455
+ let pill = null;
22456
+ let tocRows = [];
22457
+ let discs = [];
21473
22458
  let full = null;
22459
+ const review = options.review === true;
22460
+ const noteMode = review ? options.noteBar ?? "collapsed" : "hidden";
22461
+ let reserve = noteBarReserve(52, DOCK_INSET, DOCK_GAP);
21474
22462
  const viewOf = (key) => views.find((v) => v.step.key === key);
21475
22463
  const feedback = () => collectFeedback(page, state);
21476
22464
  const markdown = () => feedbackMarkdown(feedback(), page.verdicts);
@@ -21494,9 +22482,6 @@ function walkthrough(container, options) {
21494
22482
  else
21495
22483
  run();
21496
22484
  };
21497
- const pillY = new Spring(0, CALM2.omega, CALM2.zeta, 0.25);
21498
- const pillH = new Spring(0, CALM2.omega, CALM2.zeta, 0.25);
21499
- let pillPlaced = false;
21500
22485
  const scroll = {
21501
22486
  spring: new Spring(0, CALM2.omega, CALM2.zeta, 0.5),
21502
22487
  to: null,
@@ -21504,13 +22489,6 @@ function walkthrough(container, options) {
21504
22489
  };
21505
22490
  const motion = frameLoop(win, (dt) => {
21506
22491
  let more = false;
21507
- if (mark) {
21508
- pillY.step(dt);
21509
- pillH.step(dt);
21510
- mark.style.transform = `translateY(${grid3(pillY.x)}px)`;
21511
- mark.style.height = `${grid3(pillY.x + pillH.x) - grid3(pillY.x)}px`;
21512
- more ||= !pillY.resting || !pillH.resting;
21513
- }
21514
22492
  if (scroll.to && scroll.el) {
21515
22493
  scroll.spring.set(scrollTarget(scroll.to, scroll.el));
21516
22494
  scroll.spring.step(dt);
@@ -21574,21 +22552,12 @@ function walkthrough(container, options) {
21574
22552
  v.glyph.dataset.tone = v.step.ask === "none" ? "none" : verdict ? verdictTone(verdict, page.verdicts.findIndex((x) => x.id === verdict)) : done ? "accent" : "dim";
21575
22553
  };
21576
22554
  const placeMark = () => {
21577
- if (!mark || !tocEl)
22555
+ if (!pill || !tocEl)
21578
22556
  return;
22557
+ pill.update();
21579
22558
  const row = viewOf(current)?.toc;
21580
22559
  if (!row)
21581
- return void mark.removeAttribute("data-on");
21582
- mark.dataset.on = "";
21583
- if (!pillPlaced || reduced()) {
21584
- pillY.snap(row.offsetTop);
21585
- pillH.snap(row.offsetHeight);
21586
- pillPlaced = true;
21587
- } else {
21588
- pillY.set(row.offsetTop);
21589
- pillH.set(row.offsetHeight);
21590
- }
21591
- motion.wake();
22560
+ return;
21592
22561
  const top = row.offsetTop;
21593
22562
  const bottom = top + row.offsetHeight;
21594
22563
  if (top < tocEl.scrollTop + 8)
@@ -21605,6 +22574,9 @@ function walkthrough(container, options) {
21605
22574
  const v = viewOf(key);
21606
22575
  v?.card.setAttribute("data-current", "");
21607
22576
  v?.toc?.setAttribute("aria-current", "step");
22577
+ for (const r of tocRows)
22578
+ if (r.dataset.section)
22579
+ r.toggleAttribute("data-active", r.dataset.section === v?.step.section);
21608
22580
  if (key)
21609
22581
  record({ what: "step", step: key });
21610
22582
  placeMark();
@@ -21621,6 +22593,16 @@ function walkthrough(container, options) {
21621
22593
  else
21622
22594
  break;
21623
22595
  }
22596
+ const s = scrollerOf(root);
22597
+ if (s.scrollTop > 0 && s.scrollTop >= s.scrollHeight - s.clientHeight - 2) {
22598
+ const bottom = (s === doc.scrollingElement ? win.innerHeight : s.getBoundingClientRect().bottom) - reserve;
22599
+ for (let i = views.length - 1;i >= 0; i--) {
22600
+ if (views[i].card.getBoundingClientRect().top < bottom) {
22601
+ pick = views[i].step.key;
22602
+ break;
22603
+ }
22604
+ }
22605
+ }
21624
22606
  setCurrent(pick);
21625
22607
  };
21626
22608
  let scrollRaf = 0;
@@ -21663,15 +22645,20 @@ function walkthrough(container, options) {
21663
22645
  };
21664
22646
  const scrim = el2(doc, "div", "knobs-wt-scrim");
21665
22647
  scrim.addEventListener("click", () => setFull(false));
21666
- const fullRect = () => ({ x: 12, y: 12, w: win.innerWidth - 24, h: win.innerHeight - 24 });
22648
+ const fullRect = () => ({
22649
+ x: DOCK_INSET,
22650
+ y: DOCK_INSET,
22651
+ w: win.innerWidth - DOCK_INSET * 2,
22652
+ h: win.innerHeight - DOCK_INSET - reserve
22653
+ });
21667
22654
  const placeFull = (f) => {
21668
22655
  const a = f.hold.getBoundingClientRect();
21669
22656
  const b = fullRect();
21670
22657
  const k = reduced() ? Math.round(f.p.target) : f.p.x;
21671
- const left = grid3(a.left + (b.x - a.left) * k);
21672
- const top = grid3(a.top + (b.y - a.top) * k);
21673
- const right = grid3(a.right + (b.x + b.w - a.right) * k);
21674
- const bottom = grid3(a.bottom + (b.y + b.h - a.bottom) * k);
22658
+ const left = grid2(a.left + (b.x - a.left) * k);
22659
+ const top = grid2(a.top + (b.y - a.top) * k);
22660
+ const right = grid2(a.right + (b.x + b.w - a.right) * k);
22661
+ const bottom = grid2(a.bottom + (b.y + b.h - a.bottom) * k);
21675
22662
  const s = f.v.card.style;
21676
22663
  s.left = `${left}px`;
21677
22664
  s.top = `${top}px`;
@@ -21808,6 +22795,7 @@ function walkthrough(container, options) {
21808
22795
  ink.on = true;
21809
22796
  ink.layer.dataset.on = "";
21810
22797
  root.dataset.inking = "";
22798
+ bar.setOpen(true);
21811
22799
  bar.sync();
21812
22800
  };
21813
22801
  const voice = {
@@ -21847,6 +22835,7 @@ function walkthrough(container, options) {
21847
22835
  });
21848
22836
  voice.recorder = rec;
21849
22837
  voice.started = performance.now();
22838
+ bar.setOpen(true);
21850
22839
  rec.start();
21851
22840
  voice.timer = win.setInterval(() => bar.sync(), 250);
21852
22841
  bar.sync();
@@ -21873,7 +22862,10 @@ function walkthrough(container, options) {
21873
22862
  if (!v)
21874
22863
  return;
21875
22864
  backTo = key;
21876
- backPill.textContent = `← Back to ${v.step.number}`;
22865
+ backPill.innerHTML = ICON.back;
22866
+ const words = el2(doc, "span", "knobs-wt-back-label");
22867
+ words.textContent = `Back to ${plainMarkdown(v.step.title)}`;
22868
+ backPill.append(words);
21877
22869
  backPill.dataset.on = "";
21878
22870
  win.clearTimeout(backTimer);
21879
22871
  backTimer = win.setTimeout(hideBack, BACK_MS);
@@ -21905,7 +22897,41 @@ function walkthrough(container, options) {
21905
22897
  sendBtn.dataset.send = "";
21906
22898
  row.append(input, attach, fullBtn, penBtn, micBtn, sendBtn);
21907
22899
  box.append(on, row);
21908
- dock.append(backPill, box);
22900
+ const launch = el2(doc, "button", "knobs-wt-launch");
22901
+ launch.type = "button";
22902
+ launch.innerHTML = ICON.pen;
22903
+ launch.append("Note");
22904
+ const kbd = el2(doc, "kbd");
22905
+ kbd.textContent = "n";
22906
+ launch.append(kbd);
22907
+ launch.setAttribute("aria-label", "Write a note (n)");
22908
+ launch.addEventListener("click", () => setOpen(true, true));
22909
+ dock.dataset.mode = noteMode;
22910
+ dock.append(backPill, box, launch);
22911
+ let open = noteMode !== "collapsed";
22912
+ const setOpen = (on, focus = false) => {
22913
+ if (noteMode === "collapsed" && on !== open) {
22914
+ open = on;
22915
+ dock.toggleAttribute("data-open", on);
22916
+ box.inert = !on;
22917
+ launch.inert = on;
22918
+ syncReserve();
22919
+ }
22920
+ if (on && focus)
22921
+ input.focus({ preventScroll: true });
22922
+ };
22923
+ dock.toggleAttribute("data-open", open);
22924
+ box.inert = !open;
22925
+ launch.inert = open;
22926
+ const empty = () => !input.value.trim() && !voice.pending && !voice.recorder && !ink.on && !ink.paths.length;
22927
+ const onOutside = (e) => {
22928
+ if (open && noteMode === "collapsed" && !dock.contains(e.target) && empty())
22929
+ setOpen(false);
22930
+ };
22931
+ doc.addEventListener("pointerdown", onOutside, true);
22932
+ const ro = "ResizeObserver" in win ? new win.ResizeObserver(() => syncReserve()) : null;
22933
+ ro?.observe(box);
22934
+ ro?.observe(launch);
21909
22935
  const fit = () => {
21910
22936
  input.style.height = "auto";
21911
22937
  input.style.height = `${Math.min(140, input.scrollHeight)}px`;
@@ -21921,6 +22947,8 @@ function walkthrough(container, options) {
21921
22947
  } else if (e.key === "Escape") {
21922
22948
  e.preventDefault();
21923
22949
  input.blur();
22950
+ if (empty())
22951
+ setOpen(false);
21924
22952
  viewOf(current)?.card.focus({ preventScroll: true });
21925
22953
  }
21926
22954
  });
@@ -21940,13 +22968,13 @@ function walkthrough(container, options) {
21940
22968
  const sync = () => {
21941
22969
  const v = viewOf(current);
21942
22970
  const ctx = describeMedia(mediaContext(v));
21943
- const where = v ? `${v.step.number} ${v.step.title}` : "General note";
22971
+ const where = v ? plainMarkdown(v.step.title) : "General note";
21944
22972
  const text = ctx ? `${where} · ${ctx}` : where;
21945
22973
  if (text !== lastOn) {
21946
22974
  on.textContent = text;
21947
22975
  lastOn = text;
21948
22976
  }
21949
- input.placeholder = v ? `Note on ${v.step.number}… (n)` : "A note on the whole page… (n)";
22977
+ input.placeholder = v ? "A note on this step… (n)" : "A note on the whole page… (n)";
21950
22978
  const attached = `${voice.pending?.url ?? ""}|${ink.paths.length}`;
21951
22979
  if (attached !== lastAttached) {
21952
22980
  lastAttached = attached;
@@ -21985,14 +23013,45 @@ function walkthrough(container, options) {
21985
23013
  };
21986
23014
  return {
21987
23015
  dock,
23016
+ box,
23017
+ launch,
21988
23018
  input,
21989
23019
  sync,
23020
+ get open() {
23021
+ return open;
23022
+ },
23023
+ setOpen,
23024
+ empty,
23025
+ unobserve: () => {
23026
+ ro?.disconnect();
23027
+ doc.removeEventListener("pointerdown", onOutside, true);
23028
+ },
21990
23029
  clear() {
21991
23030
  input.value = "";
21992
23031
  fit();
21993
23032
  }
21994
23033
  };
21995
23034
  })();
23035
+ let padded = null;
23036
+ function syncReserve() {
23037
+ if (disposed)
23038
+ return;
23039
+ const h = (bar.open ? bar.box : bar.launch).offsetHeight;
23040
+ if (noteMode === "hidden")
23041
+ reserve = 0;
23042
+ else if (h)
23043
+ reserve = noteBarReserve(h, DOCK_INSET, DOCK_GAP);
23044
+ root.style.setProperty("--kw-reserve", `${reserve}px`);
23045
+ const s = scrollerOf(root);
23046
+ if (padded?.el !== s) {
23047
+ if (padded)
23048
+ padded.el.style.scrollPaddingBottom = padded.was;
23049
+ padded = { el: s, was: s.style.scrollPaddingBottom };
23050
+ }
23051
+ s.style.scrollPaddingBottom = `${reserve}px`;
23052
+ if (full)
23053
+ placeFull(full);
23054
+ }
21996
23055
  const ticker = win.setInterval(() => !doc.hidden && bar.sync(), 500);
21997
23056
  const send = (text = bar.input.value) => {
21998
23057
  if (voice.recorder)
@@ -22028,6 +23087,10 @@ function walkthrough(container, options) {
22028
23087
  syncProgress();
22029
23088
  emit();
22030
23089
  bar.sync();
23090
+ if (noteMode === "collapsed") {
23091
+ bar.input.blur();
23092
+ bar.setOpen(false);
23093
+ }
22031
23094
  if (v && options.advance !== false) {
22032
23095
  const to = adjacentStep(views.map((x) => x.step.key), v.step.key, 1);
22033
23096
  if (to && to !== v.step.key) {
@@ -22035,7 +23098,8 @@ function walkthrough(container, options) {
22035
23098
  setFull(false);
22036
23099
  go(to);
22037
23100
  showBack(v.step.key);
22038
- bar.input.focus({ preventScroll: true });
23101
+ if (noteMode === "docked")
23102
+ bar.input.focus({ preventScroll: true });
22039
23103
  }
22040
23104
  }
22041
23105
  };
@@ -22122,28 +23186,121 @@ function walkthrough(container, options) {
22122
23186
  row.addEventListener("transitionend", done, { once: true });
22123
23187
  win.setTimeout(done, 600);
22124
23188
  };
23189
+ const metaLine = (cls) => el2(doc, "div", `knobs-wt-metaline ${cls}`);
23190
+ const metaText = (line, text, cls) => {
23191
+ const s = el2(doc, "span", cls);
23192
+ s.textContent = text;
23193
+ line.append(s);
23194
+ return s;
23195
+ };
23196
+ const findPRIn = (n) => findPR(page.prs, n, page.repo);
23197
+ const metaLink = (line, text, href) => {
23198
+ const n = prNumber(href);
23199
+ if (n) {
23200
+ const chip = prChip(doc, n, { href, ...findPRIn(n) });
23201
+ line.append(chip);
23202
+ return chip;
23203
+ }
23204
+ const a = el2(doc, "a");
23205
+ a.href = href;
23206
+ a.target = "_blank";
23207
+ a.rel = "noreferrer";
23208
+ a.textContent = text;
23209
+ a.insertAdjacentHTML("beforeend", ICON.out);
23210
+ line.append(a);
23211
+ return a;
23212
+ };
23213
+ const feedItem = (f, now) => {
23214
+ const li = el2(doc, "li", "knobs-wt-fi");
23215
+ li.dataset.kind = f.kind;
23216
+ const q = el2(doc, "div", "knobs-wt-fi-q");
23217
+ const meta = metaLine("knobs-wt-fi-meta");
23218
+ if (f.at !== null) {
23219
+ const when = metaText(meta, ago(f.at, now));
23220
+ when.title = new Date(f.at).toLocaleString();
23221
+ } else
23222
+ metaText(meta, f.kind === "qa" ? "Answer" : "Update");
23223
+ const step = f.step ? page.steps.find((s) => s.key === f.step) : undefined;
23224
+ if (step) {
23225
+ const section = page.sections.find((s) => s.id === step.section);
23226
+ const b = el2(doc, "button", "knobs-wt-fi-step");
23227
+ b.type = "button";
23228
+ b.textContent = section?.link?.label ?? plainMarkdown(step.title);
23229
+ b.title = `Go to “${plainMarkdown(step.title)}”`;
23230
+ b.addEventListener("click", () => go(step.key));
23231
+ meta.append(b);
23232
+ }
23233
+ if (f.status) {
23234
+ const id = f.status.toLowerCase();
23235
+ const shipped = /^(ship|shipped|merged|done)$/.test(id);
23236
+ const changes = /^(changes?|changes queued)$/.test(id);
23237
+ const s = metaText(meta, shipped ? "Shipped" : changes ? "Changes queued" : f.status);
23238
+ s.dataset.status = shipped ? "on" : changes ? "warn" : "";
23239
+ }
23240
+ q.append(meta);
23241
+ if (f.q)
23242
+ q.append(prose2(doc, "p", "knobs-wt-fi-ask", f.q));
23243
+ else
23244
+ q.append(prose2(doc, "p", "knobs-wt-fi-text", f.text));
23245
+ li.append(q);
23246
+ if (f.q || f.trail.length) {
23247
+ const a = el2(doc, "div", "knobs-wt-fi-a");
23248
+ if (f.q)
23249
+ a.append(prose2(doc, "div", "knobs-wt-fi-answer", f.text));
23250
+ if (f.trail.length) {
23251
+ const list = el2(doc, "ol", "knobs-wt-trail");
23252
+ for (const line of f.trail) {
23253
+ const i = line.indexOf(" · ");
23254
+ const row = el2(doc, "li");
23255
+ const words = el2(doc, "span", "knobs-wt-trail-text");
23256
+ words.textContent = i < 0 ? line : line.slice(i + 3);
23257
+ row.append(words);
23258
+ if (i >= 0) {
23259
+ const at = el2(doc, "span", "knobs-wt-trail-at");
23260
+ at.textContent = line.slice(0, i);
23261
+ row.append(at);
23262
+ }
23263
+ list.append(row);
23264
+ }
23265
+ discs.push(disclosure(a, { label: "Context", count: f.trail.length, content: list }));
23266
+ }
23267
+ li.append(a);
23268
+ }
23269
+ return li;
23270
+ };
22125
23271
  const build = () => {
22126
23272
  for (const v of views) {
22127
23273
  v.slots.forEach((s) => s.dispose());
22128
23274
  v.taste?.dispose();
22129
23275
  }
23276
+ for (const d of discs)
23277
+ d.dispose();
23278
+ discs = [];
23279
+ pill?.dispose();
23280
+ pill = null;
23281
+ tocRows = [];
22130
23282
  if (full) {
22131
23283
  full.p.snap(0);
22132
23284
  closeFull();
22133
23285
  }
22134
23286
  clearInk();
22135
23287
  views = [];
22136
- pillPlaced = false;
22137
23288
  root.textContent = "";
22138
23289
  root.removeAttribute("data-settled");
23290
+ root.dataset.rail = options.rail === false ? "none" : options.rail === "right" ? "right" : "left";
23291
+ root.toggleAttribute("data-cards", options.cards === true);
23292
+ root.toggleAttribute("data-review", review);
22139
23293
  const layout = el2(doc, "div", "knobs-wt-layout");
22140
23294
  const main = el2(doc, "div", "knobs-wt-main");
23295
+ const flow = el2(doc, "div", "knobs-wt-flow");
22141
23296
  const head = el2(doc, "header", "knobs-wt-head");
22142
23297
  const h1 = el2(doc, "h1", "knobs-wt-title");
22143
23298
  h1.textContent = page.title;
22144
23299
  head.append(h1);
22145
23300
  if (page.summary)
22146
23301
  head.append(prose2(doc, "p", "knobs-wt-summary", page.summary));
23302
+ if (page.more)
23303
+ head.append(more(page.more));
22147
23304
  if (page.meta.length) {
22148
23305
  const meta = el2(doc, "div", "knobs-wt-meta");
22149
23306
  for (const m of page.meta) {
@@ -22164,57 +23321,51 @@ function walkthrough(container, options) {
22164
23321
  }
22165
23322
  head.append(meta);
22166
23323
  }
22167
- main.append(head);
23324
+ flow.append(head);
23325
+ let feedEl = null;
22168
23326
  if (page.feed.length) {
22169
- const feed = el2(doc, "section", "knobs-wt-feed");
22170
- feed.setAttribute("aria-label", "Latest");
22171
- const list = el2(doc, "ol", "knobs-wt-feed-list knobs-scroll-fade");
23327
+ feedEl = el2(doc, "section", "knobs-wt-feed");
23328
+ feedEl.id = "wt-latest";
23329
+ feedEl.setAttribute("aria-label", "Updates and answers");
23330
+ const fh = el2(doc, "header", "knobs-wt-feed-head");
23331
+ const h = el2(doc, "h2", "knobs-wt-feed-title");
23332
+ h.textContent = page.feed.some((f) => f.kind === "qa") ? "Updates & Q&A" : "Updates";
23333
+ const n = el2(doc, "span", "knobs-wt-feed-count");
23334
+ n.textContent = String(page.feed.length);
23335
+ fh.append(h, n);
23336
+ const list = el2(doc, "ol", "knobs-wt-feed-list");
22172
23337
  const now = Date.now();
22173
- for (const f of page.feed) {
22174
- const li = el2(doc, "li", "knobs-wt-fi");
22175
- li.dataset.kind = f.kind;
22176
- const when = el2(doc, "span", "knobs-wt-fi-when");
22177
- when.textContent = f.at === null ? "" : ago(f.at, now);
22178
- if (f.at !== null)
22179
- when.title = new Date(f.at).toLocaleString();
22180
- const kind = el2(doc, "span", "knobs-wt-fi-kind");
22181
- kind.textContent = f.kind === "qa" ? "Answer" : "Update";
22182
- const body = el2(doc, "div", "knobs-wt-fi-body");
22183
- if (f.q)
22184
- body.append(prose2(doc, "p", "knobs-wt-fi-q", f.q));
22185
- body.append(prose2(doc, "p", "knobs-wt-fi-text", f.text));
22186
- li.append(when, kind, body);
22187
- if (f.step) {
22188
- const target = f.step;
22189
- const link = el2(doc, "button", "knobs-wt-fi-step");
22190
- link.type = "button";
22191
- link.textContent = page.steps.find((s) => s.key === target)?.number ?? "";
22192
- link.title = "Go to the step";
22193
- link.addEventListener("click", () => go(target));
22194
- li.append(link);
22195
- }
22196
- list.append(li);
22197
- }
22198
- feed.append(list);
22199
- main.append(feed);
23338
+ for (const f of page.feed)
23339
+ list.append(feedItem(f, now));
23340
+ feedEl.append(fh, list);
23341
+ flow.append(feedEl);
22200
23342
  }
22201
23343
  let rail = null;
22202
- count = fill = tocEl = mark = null;
23344
+ count = fill = tocEl = null;
22203
23345
  if (options.rail !== false) {
22204
23346
  rail = el2(doc, "aside", "knobs-wt-rail");
22205
- rail.setAttribute("aria-label", "Progress");
22206
- const progress = el2(doc, "div", "knobs-wt-progress");
22207
- count = el2(doc, "div", "knobs-wt-count");
22208
- const meter = el2(doc, "div", "knobs-wt-bar");
22209
- fill = el2(doc, "div", "knobs-wt-fill");
22210
- meter.append(fill);
22211
- progress.append(count, meter);
23347
+ rail.setAttribute("aria-label", "Contents");
22212
23348
  tocEl = el2(doc, "nav", "knobs-wt-toc");
22213
23349
  tocEl.setAttribute("aria-label", "Contents");
22214
- mark = el2(doc, "div", "knobs-wt-toc-mark");
22215
- tocEl.append(mark);
22216
- rail.append(progress, tocEl, actions(true));
22217
- if (options.keys !== false) {
23350
+ if (feedEl) {
23351
+ const group = el2(doc, "div", "knobs-wt-toc-group");
23352
+ const target = feedEl;
23353
+ group.append(tocRow("Latest", false, () => target.scrollIntoView({ block: "start", behavior: reduced() ? "auto" : "smooth" })));
23354
+ tocEl.append(group);
23355
+ }
23356
+ if (review) {
23357
+ const progress = el2(doc, "div", "knobs-wt-progress");
23358
+ count = el2(doc, "div", "knobs-wt-count");
23359
+ const meter = el2(doc, "div", "knobs-wt-bar");
23360
+ fill = el2(doc, "div", "knobs-wt-fill");
23361
+ meter.append(fill);
23362
+ progress.append(count, meter);
23363
+ rail.append(progress);
23364
+ }
23365
+ rail.append(tocEl);
23366
+ if (review)
23367
+ rail.append(actions(true));
23368
+ if (review && options.keys !== false) {
22218
23369
  const keys = el2(doc, "div", "knobs-wt-keys");
22219
23370
  keys.innerHTML = "<kbd>j</kbd> <kbd>k</kbd> steps · <kbd>←</kbd> <kbd>→</kbd> seek · <kbd>f</kbd> full · <kbd>n</kbd> note" + (page.steps.some((s) => s.ask === "pick") ? " · <kbd>1</kbd>–<kbd>9</kbd> pick" : "");
22220
23371
  rail.append(keys);
@@ -22224,45 +23375,47 @@ function walkthrough(container, options) {
22224
23375
  const sec = el2(doc, "section", "knobs-wt-section");
22225
23376
  sec.id = `wt-${section.id}`;
22226
23377
  const sh = el2(doc, "header", "knobs-wt-shead");
22227
- if (section.link || section.status) {
22228
- const kicker = el2(doc, "div", "knobs-wt-kicker");
22229
- if (section.link) {
22230
- const a = el2(doc, "a");
22231
- a.href = section.link.href;
22232
- a.target = "_blank";
22233
- a.rel = "noreferrer";
22234
- a.textContent = `${section.link.label} ↗`;
22235
- kicker.append(a);
22236
- }
22237
- if (section.status) {
22238
- const s = statusLook(section.status);
22239
- const chip = el2(doc, "span", "knobs-wt-chip");
22240
- chip.dataset.tone = s.tone;
22241
- chip.textContent = s.label;
22242
- kicker.append(chip);
23378
+ const status = review ? section.status : undefined;
23379
+ if (section.kicker || section.link || status) {
23380
+ const meta = metaLine("knobs-wt-kicker");
23381
+ if (section.kicker)
23382
+ metaText(meta, section.kicker, "knobs-wt-kicker-lead");
23383
+ if (section.link)
23384
+ metaLink(meta, section.link.label, section.link.href);
23385
+ if (status) {
23386
+ const s = statusLook(status);
23387
+ metaText(meta, s.label, "knobs-wt-status").dataset.tone = s.tone;
22243
23388
  }
22244
- sh.append(kicker);
23389
+ sh.append(meta);
22245
23390
  }
22246
- const h2 = el2(doc, "h2", "knobs-wt-stitle");
22247
- h2.textContent = section.title;
23391
+ const h2 = el2(doc, "h2", "knobs-wt-heading");
23392
+ h2.innerHTML = inlineMarkdown(section.title);
22248
23393
  sh.append(h2);
23394
+ const intro = el2(doc, "div", "knobs-wt-sintro");
23395
+ intro.append(sh);
22249
23396
  if (section.summary)
22250
- sh.append(prose2(doc, "p", "knobs-wt-ssum", section.summary));
22251
- sec.append(sh);
23397
+ intro.append(prose2(doc, "p", "knobs-wt-ssum", section.summary));
23398
+ if (section.more)
23399
+ intro.append(more(section.more));
23400
+ sec.append(intro);
23401
+ let group = null;
22252
23402
  if (tocEl) {
22253
- const t = el2(doc, "div", "knobs-wt-toc-section");
22254
- t.textContent = section.link ? `${section.link.label} ${section.title}` : section.title;
22255
- t.title = section.title;
22256
- tocEl.append(t);
23403
+ group = el2(doc, "div", "knobs-wt-toc-group");
23404
+ const first = section.steps[0]?.key;
23405
+ group.append(tocRow(plainMarkdown(section.title), false, () => first ? go(first) : undefined, section.id));
23406
+ tocEl.append(group);
22257
23407
  }
22258
23408
  for (const step of section.steps) {
22259
- const v = stepView(step);
23409
+ const v = stepView(step, group);
22260
23410
  sec.append(v.card);
22261
23411
  views.push(v);
22262
23412
  }
22263
- main.append(sec);
23413
+ flow.append(sec);
22264
23414
  }
22265
- main.append(bar.dock);
23415
+ linkPRs(flow, findPRIn);
23416
+ main.append(flow);
23417
+ if (noteMode !== "hidden")
23418
+ main.append(bar.dock);
22266
23419
  if (rail)
22267
23420
  layout.append(rail);
22268
23421
  layout.append(main);
@@ -22272,16 +23425,46 @@ function walkthrough(container, options) {
22272
23425
  for (const n of notesOn(state, v.step.key))
22273
23426
  renderNote(v, n);
22274
23427
  }
23428
+ if (tocEl)
23429
+ pill = selectionPill(tocEl, {
23430
+ selected: () => viewOf(current)?.toc ?? null,
23431
+ rows: () => tocRows
23432
+ });
22275
23433
  syncProgress();
22276
23434
  current = null;
22277
23435
  fromScroll();
22278
23436
  placeMark();
22279
23437
  bar.sync();
23438
+ syncReserve();
22280
23439
  win.requestAnimationFrame(() => win.requestAnimationFrame(() => {
22281
23440
  if (!disposed && !reduced())
22282
23441
  root.dataset.settled = "";
22283
23442
  }));
22284
23443
  };
23444
+ const more = (text) => {
23445
+ const box = el2(doc, "div", "knobs-wt-more");
23446
+ const body = el2(doc, "div", "knobs-wt-more-text");
23447
+ for (const para of text.split(/\n\s*\n/))
23448
+ if (para.trim())
23449
+ body.append(prose2(doc, "p", "", para.trim()));
23450
+ discs.push(disclosure(box, { label: "More", content: body }));
23451
+ return box;
23452
+ };
23453
+ const tocRow = (label, sub, run, section) => {
23454
+ const b = el2(doc, "button", sub ? "knobs-wt-toc-row knobs-wt-toc-sub" : "knobs-wt-toc-row");
23455
+ b.type = "button";
23456
+ if (section)
23457
+ b.dataset.section = section;
23458
+ const t = el2(doc, "span", "knobs-wt-toc-label");
23459
+ t.textContent = label;
23460
+ const e = el2(doc, "span", "knobs-wt-toc-end");
23461
+ b.title = label;
23462
+ b.append(t, e);
23463
+ b.addEventListener("mousedown", (ev) => ev.preventDefault());
23464
+ b.addEventListener("click", run);
23465
+ tocRows.push(b);
23466
+ return b;
23467
+ };
22285
23468
  const actions = (rail) => {
22286
23469
  const row = el2(doc, "div", "knobs-wt-actions");
22287
23470
  const btn = (text, run, primary = false) => {
@@ -22322,37 +23505,38 @@ function walkthrough(container, options) {
22322
23505
  toast("Couldn't reach the clipboard", { tone: "warn", look, doc });
22323
23506
  }
22324
23507
  };
22325
- const stepView = (step) => {
22326
- const card = el2(doc, "article", "knobs-wt-step knobs-surface");
23508
+ const stepView = (step, group) => {
23509
+ const card = el2(doc, "article", options.cards ? "knobs-wt-step knobs-surface" : "knobs-wt-step");
22327
23510
  card.id = `wt-${step.key.replace("/", "--")}`;
22328
23511
  card.tabIndex = -1;
22329
23512
  card.dataset.key = step.key;
22330
- card.setAttribute("aria-label", `${step.number} ${step.title}`);
23513
+ card.setAttribute("aria-label", plainMarkdown(step.title));
22331
23514
  const head = el2(doc, "header", "knobs-wt-step-head");
22332
- const num = el2(doc, "span", "knobs-wt-num");
22333
- num.textContent = step.number;
22334
23515
  const h3 = el2(doc, "h3", "knobs-wt-stepTitle");
22335
- h3.textContent = step.title;
22336
- head.append(num, h3);
22337
- card.append(head);
23516
+ h3.innerHTML = inlineMarkdown(step.title);
23517
+ head.append(h3);
22338
23518
  if (step.caption)
22339
- card.append(prose2(doc, "p", "knobs-wt-caption", step.caption));
23519
+ head.append(prose2(doc, "p", "knobs-wt-caption", step.caption));
23520
+ if (step.more)
23521
+ head.append(more(step.more));
23522
+ card.append(head);
22340
23523
  const slots = [];
22341
23524
  const onEvent = (e) => record({ ...e, step: step.key });
22342
23525
  let media = null;
22343
23526
  if (step.media.length) {
22344
23527
  media = el2(doc, "div", "knobs-wt-media");
22345
- media.style.setProperty("--kw-n", String(Math.min(step.media.length, 3)));
23528
+ media.style.setProperty("--kw-n", String(Math.min(step.media.length, step.columns)));
22346
23529
  for (const m of step.media)
22347
23530
  slots.push(mediaSlot(media, { ...look, media: m, onEvent, steps: stepLinks }));
22348
23531
  card.append(media);
22349
23532
  }
22350
23533
  let taste = null;
22351
- if (step.question)
23534
+ if (step.question && review)
22352
23535
  card.append(prose2(doc, "p", "knobs-wt-question", step.question));
22353
23536
  if (step.ask === "pick") {
22354
23537
  taste = tasteTest(card, {
22355
23538
  ...look,
23539
+ pickable: review,
22356
23540
  options: step.options,
22357
23541
  picked: state.steps[step.key]?.pick ?? null,
22358
23542
  onPick: (id) => pick(step.key, id),
@@ -22361,7 +23545,8 @@ function walkthrough(container, options) {
22361
23545
  media ??= taste.element;
22362
23546
  }
22363
23547
  const notes = el2(doc, "div", "knobs-wt-notes");
22364
- card.append(notes);
23548
+ if (review)
23549
+ card.append(notes);
22365
23550
  card.addEventListener("focusin", () => {
22366
23551
  if (scroll.to)
22367
23552
  return;
@@ -22370,16 +23555,13 @@ function walkthrough(container, options) {
22370
23555
  });
22371
23556
  let toc = null;
22372
23557
  let glyph = null;
22373
- if (tocEl) {
22374
- toc = el2(doc, "button", "knobs-wt-toc-step");
22375
- toc.type = "button";
22376
- glyph = el2(doc, "span", "knobs-wt-toc-glyph");
22377
- const label = el2(doc, "span", "knobs-wt-toc-label");
22378
- label.textContent = step.title;
22379
- toc.title = `${step.number} ${step.title}`;
22380
- toc.append(glyph, label);
22381
- toc.addEventListener("click", () => go(step.key));
22382
- tocEl.append(toc);
23558
+ if (group) {
23559
+ toc = tocRow(plainMarkdown(step.title), true, () => go(step.key));
23560
+ if (review) {
23561
+ glyph = el2(doc, "span", "knobs-wt-toc-glyph");
23562
+ toc.querySelector(".knobs-wt-toc-end").append(glyph);
23563
+ }
23564
+ group.append(toc);
22383
23565
  }
22384
23566
  return { step, card, media, notes, toc, glyph, taste, slots };
22385
23567
  };
@@ -22438,19 +23620,25 @@ function walkthrough(container, options) {
22438
23620
  setFull(false);
22439
23621
  return go(next);
22440
23622
  }
23623
+ if ((key === "Home" || key === "End") && views.length) {
23624
+ claim();
23625
+ if (full)
23626
+ setFull(false);
23627
+ return go(views[key === "Home" ? 0 : views.length - 1].step.key);
23628
+ }
22441
23629
  if (key === "ArrowLeft" || key === "ArrowRight") {
22442
23630
  const d = (key === "ArrowLeft" ? -1 : 1) * (e.shiftKey ? 2 : 0.5);
22443
23631
  if (seekBy(d))
22444
23632
  claim();
22445
23633
  return;
22446
23634
  }
22447
- if (key === "n") {
23635
+ if (key === "n" && noteMode !== "hidden") {
22448
23636
  claim();
22449
- bar.input.focus({ preventScroll: true });
23637
+ bar.setOpen(true, true);
22450
23638
  return;
22451
23639
  }
22452
23640
  const v = viewOf(current);
22453
- if (!v || e.shiftKey)
23641
+ if (!v || e.shiftKey || !review)
22454
23642
  return;
22455
23643
  const choice = digitChoice(v.step, state.steps[v.step.key], key);
22456
23644
  if (choice === undefined)
@@ -22525,6 +23713,12 @@ function walkthrough(container, options) {
22525
23713
  win.removeEventListener(type, stopScroll);
22526
23714
  doc.removeEventListener("keydown", onKey);
22527
23715
  unregisterHints();
23716
+ bar.unobserve();
23717
+ pill?.dispose();
23718
+ for (const d of discs)
23719
+ d.dispose();
23720
+ if (padded)
23721
+ padded.el.style.scrollPaddingBottom = padded.was;
22528
23722
  for (const v of views) {
22529
23723
  v.slots.forEach((s) => s.dispose());
22530
23724
  v.taste?.dispose();
@@ -22609,6 +23803,7 @@ export {
22609
23803
  mediaKind,
22610
23804
  mediaSlot,
22611
23805
  menu,
23806
+ minutes,
22612
23807
  modeForKey,
22613
23808
  motionBlur,
22614
23809
  moveHead,
@@ -22628,6 +23823,7 @@ export {
22628
23823
  plainAccess,
22629
23824
  planBento,
22630
23825
  planPill,
23826
+ player,
22631
23827
  popover,
22632
23828
  railSegments,
22633
23829
  rank,