moontide-mcp 2.0.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +44 -6
  2. package/dist/ask.js +30 -8
  3. package/dist/mcp.js +60 -14
  4. package/dist/protocol.js +89 -2
  5. package/dist/reference.js +22 -0
  6. package/dist/session.js +53 -0
  7. package/package.json +1 -1
  8. package/page/ask-view.js +21 -7
  9. package/page/ask.js +292 -34
  10. package/page/change-timing.js +25 -4
  11. package/page/harmony.js +13 -0
  12. package/page/index.html +4 -3
  13. package/page/jam-model.js +11 -2
  14. package/page/jam-view.js +17 -0
  15. package/page/lights.js +278 -0
  16. package/page/main.js +1723 -140
  17. package/page/midi-clock.js +1 -1
  18. package/page/midi-out.js +12 -0
  19. package/page/mixer-groups.js +114 -0
  20. package/page/mixer-model.js +172 -29
  21. package/page/mixer-rewrite.js +30 -1
  22. package/page/mixer-view.js +282 -13
  23. package/page/note-edit.js +267 -0
  24. package/page/note-locate.js +346 -0
  25. package/page/note-panel.js +381 -0
  26. package/page/note-tag.js +126 -0
  27. package/page/page-rewrite.js +184 -0
  28. package/page/song-edit.js +230 -0
  29. package/page/song-fit.js +250 -0
  30. package/page/song-grid-model.js +491 -0
  31. package/page/song-grid-view.js +710 -0
  32. package/page/song-prefs.js +100 -0
  33. package/page/structure-check.js +216 -0
  34. package/page/structure-view.js +38 -0
  35. package/page/structure.js +760 -0
  36. package/page/style.css +879 -84
  37. package/page/takeover.js +20 -5
  38. package/page/timeline-edit-view.js +1956 -0
  39. package/page/timeline-gesture.js +279 -0
  40. package/page/timeline.js +196 -3
  41. package/page/tracks.js +37 -8
  42. package/page/ui-state.js +10 -5
  43. package/page/views.js +25 -8
  44. package/reference/functions.md +1 -0
  45. package/reference/midi.md +2 -0
  46. package/reference/structure.md +131 -0
package/page/style.css CHANGED
@@ -365,10 +365,11 @@ body {
365
365
 
366
366
  /* A segmented group: a 1px frame on the dominant fill, no padding and no gap, so it is 1.75rem tall plus its frame and
367
367
  fits the slim bar. The buttons inside are the hit targets (at least 1.75rem square). The Edits timing group of the
368
- Settings card is the third of them and shares every rule below; it only sets its own text size. */
368
+ Settings card and the launch group of the Song grid's toolbar share every rule below; they only set their own text size. */
369
369
  #view-buttons,
370
370
  #layout-buttons,
371
- #change-timing {
371
+ #change-timing,
372
+ #song-launch {
372
373
  display: inline-flex;
373
374
  flex: none;
374
375
  align-items: stretch;
@@ -383,7 +384,8 @@ body {
383
384
 
384
385
  #view-buttons button,
385
386
  #layout-buttons button,
386
- #change-timing button {
387
+ #change-timing button,
388
+ #song-launch button {
387
389
  display: inline-flex;
388
390
  align-items: center;
389
391
  justify-content: center;
@@ -411,7 +413,8 @@ body {
411
413
 
412
414
  #view-buttons button:hover,
413
415
  #layout-buttons button:hover,
414
- #change-timing button:hover {
416
+ #change-timing button:hover,
417
+ #song-launch button:hover {
415
418
  background: var(--surface-2);
416
419
  color: var(--text);
417
420
  }
@@ -420,7 +423,8 @@ body {
420
423
  changes width when the selection moves. Muted text never sits on this fill. */
421
424
  #view-buttons button[aria-pressed="true"],
422
425
  #layout-buttons button[aria-pressed="true"],
423
- #change-timing button[aria-pressed="true"] {
426
+ #change-timing button[aria-pressed="true"],
427
+ #song-launch button[aria-pressed="true"] {
424
428
  background: var(--surface-2);
425
429
  color: var(--text);
426
430
  box-shadow: inset 0 0 0 1px var(--border);
@@ -428,7 +432,8 @@ body {
428
432
 
429
433
  #view-buttons button:focus-visible,
430
434
  #layout-buttons button:focus-visible,
431
- #change-timing button:focus-visible {
435
+ #change-timing button:focus-visible,
436
+ #song-launch button:focus-visible {
432
437
  outline: 1px solid var(--text-muted);
433
438
  outline-offset: -1px;
434
439
  }
@@ -440,7 +445,8 @@ body {
440
445
  max-width: 100%;
441
446
  }
442
447
 
443
- #change-timing button {
448
+ #change-timing button,
449
+ #song-launch button {
444
450
  font-size: 0.8125rem;
445
451
  }
446
452
 
@@ -1645,7 +1651,9 @@ body {
1645
1651
  grid-row: 1;
1646
1652
  }
1647
1653
 
1648
- .lane[data-drum-rows] .lane-events {
1654
+ /* A lane's event area fills the lane's whole cell, so the rings, ghosts and washes drawn over it (the mark layer fills the same
1655
+ cell) sit on the notes whatever height the lane's name column gives the lane. */
1656
+ .lane-events {
1649
1657
  align-self: stretch;
1650
1658
  width: 100%;
1651
1659
  height: 100%;
@@ -1692,6 +1700,234 @@ body {
1692
1700
  .lane[data-drum-rows="7"] { min-height: 8.75rem; }
1693
1701
  .lane[data-drum-rows="8"] { min-height: 10rem; }
1694
1702
 
1703
+ /* The editable Timeline: the panel under the lanes, and the marks on a lane. The panel is a quiet strip that is always the
1704
+ same height, so the lanes do not move under the pointer when a note is picked; its two text lines keep their room even
1705
+ when empty. A selected note gets a solid ring, a preview a dashed one, the other places of the same token a dotted one
1706
+ and a range of cycles a wash with two edges: outlines and words only, never the accent colors, which keep their uses
1707
+ (the playhead is the only accent on the Timeline). The marks sit on top of a lane in its own grid cell, never take the
1708
+ pointer and are hidden from screen readers. Nothing here moves, fades or animates. */
1709
+ #timeline-note {
1710
+ flex: 0 0 auto;
1711
+ box-sizing: border-box;
1712
+ display: flex;
1713
+ flex-wrap: wrap;
1714
+ align-items: center;
1715
+ gap: 0.25rem 1rem;
1716
+ padding: 0.5rem 1rem;
1717
+ background: var(--secondary);
1718
+ border-top: 1px solid var(--border);
1719
+ font-size: 0.8125rem;
1720
+ font-weight: 400;
1721
+ line-height: 1.5;
1722
+ }
1723
+
1724
+ #timeline-note[hidden] {
1725
+ display: none;
1726
+ }
1727
+
1728
+ .tl-note-where {
1729
+ min-width: 0;
1730
+ min-height: 2rem;
1731
+ display: flex;
1732
+ align-items: center;
1733
+ color: var(--text-muted);
1734
+ overflow-wrap: anywhere;
1735
+ }
1736
+
1737
+ /* The spaces at the start of the position text are kept when the lane name and the position sit side by side. */
1738
+ .tl-note-where > span {
1739
+ white-space: pre-wrap;
1740
+ }
1741
+
1742
+ .tl-lane-name {
1743
+ font-weight: 600;
1744
+ color: var(--text);
1745
+ }
1746
+
1747
+ .tl-note-state {
1748
+ color: var(--text-muted);
1749
+ }
1750
+
1751
+ .tl-note-controls {
1752
+ display: flex;
1753
+ flex-wrap: wrap;
1754
+ align-items: center;
1755
+ gap: 0.5rem 1rem;
1756
+ }
1757
+
1758
+ .tl-note-controls[hidden] {
1759
+ display: none;
1760
+ }
1761
+
1762
+ .tl-group {
1763
+ display: flex;
1764
+ align-items: center;
1765
+ gap: 0.25rem;
1766
+ }
1767
+
1768
+ /* A range offers only Ask Claude: the groups and the Make a rest button are hidden, and their display must not undo that. */
1769
+ .tl-group[hidden],
1770
+ .tl-btn[hidden] {
1771
+ display: none;
1772
+ }
1773
+
1774
+ .tl-label {
1775
+ color: var(--text-muted);
1776
+ margin-right: 0.25rem;
1777
+ }
1778
+
1779
+ .tl-value {
1780
+ font-family: var(--font-mono);
1781
+ color: var(--text);
1782
+ margin-right: 0.25rem;
1783
+ }
1784
+
1785
+ .tl-fixed {
1786
+ color: var(--text-muted);
1787
+ margin-right: 0.25rem;
1788
+ }
1789
+
1790
+ .tl-btn {
1791
+ box-sizing: border-box;
1792
+ min-width: 2rem;
1793
+ height: 2rem;
1794
+ padding: 0 0.5rem;
1795
+ background: var(--surface-2);
1796
+ border: 1px solid var(--border);
1797
+ color: var(--text);
1798
+ font-family: inherit;
1799
+ font-size: 0.8125rem;
1800
+ font-weight: 400;
1801
+ line-height: 1.5;
1802
+ cursor: pointer;
1803
+ }
1804
+
1805
+ .tl-btn:hover:not([aria-disabled="true"]) {
1806
+ border-color: var(--text-muted);
1807
+ }
1808
+
1809
+ .tl-btn[aria-disabled="true"] {
1810
+ border-style: dashed;
1811
+ cursor: default;
1812
+ }
1813
+
1814
+ .tl-btn:focus-visible {
1815
+ outline: 1px solid var(--text-muted);
1816
+ outline-offset: 1px;
1817
+ }
1818
+
1819
+ #timeline-note-line,
1820
+ .tl-note-hint {
1821
+ flex: 1 0 100%;
1822
+ margin: 0;
1823
+ min-height: 1.5rem;
1824
+ color: var(--text-muted);
1825
+ overflow-wrap: anywhere;
1826
+ }
1827
+
1828
+ #timeline-note-line[data-kind="refused"],
1829
+ #timeline-note-line[data-kind="result"],
1830
+ #timeline-note-line[data-kind="notice"],
1831
+ #timeline-note-line[data-kind="preview"] {
1832
+ color: var(--text);
1833
+ }
1834
+
1835
+ #workspace[data-layout="b"] #timeline-note-line,
1836
+ #workspace[data-layout="b"] .tl-note-hint {
1837
+ min-height: 4rem;
1838
+ }
1839
+
1840
+ /* In the side panel the buttons wrap into rows when a note is selected; the panel reserves their room, so it keeps one height
1841
+ whether nothing, a note or a drag is shown and the lanes do not move when a note is picked. */
1842
+ #workspace[data-layout="b"] #timeline-note {
1843
+ min-height: 19rem;
1844
+ }
1845
+
1846
+ #timeline-lanes:focus-visible {
1847
+ outline: 1px solid var(--text-muted);
1848
+ outline-offset: -1px;
1849
+ }
1850
+
1851
+ #timeline-lanes[data-dragging] {
1852
+ user-select: none;
1853
+ }
1854
+
1855
+ /* The pointer only confirms what the words say: a cross over empty lane area, a hand over a note, up-and-down arrows while an
1856
+ editable note is dragged, the plain arrow while a note the page cannot edit is dragged, a cross for a drag across the lanes.
1857
+ The lanes hold the pointer during a drag, so they carry the cursor themselves while one lasts. */
1858
+ #timeline-lanes .lane-events {
1859
+ cursor: crosshair;
1860
+ }
1861
+
1862
+ #timeline-lanes[data-hover="note"] .lane-events {
1863
+ cursor: pointer;
1864
+ }
1865
+
1866
+ #timeline-lanes[data-dragging="pitch"],
1867
+ #timeline-lanes[data-dragging="pitch"] .lane-events {
1868
+ cursor: ns-resize;
1869
+ }
1870
+
1871
+ #timeline-lanes[data-dragging="refused"],
1872
+ #timeline-lanes[data-dragging="refused"] .lane-events {
1873
+ cursor: default;
1874
+ }
1875
+
1876
+ #timeline-lanes[data-dragging="range"],
1877
+ #timeline-lanes[data-dragging="range"] .lane-events {
1878
+ cursor: crosshair;
1879
+ }
1880
+
1881
+ .lane-marks {
1882
+ grid-column: 2;
1883
+ grid-row: 1;
1884
+ position: relative;
1885
+ pointer-events: none;
1886
+ min-width: 0;
1887
+ }
1888
+
1889
+ .tl-ring,
1890
+ .tl-ghost,
1891
+ .tl-same {
1892
+ position: absolute;
1893
+ box-sizing: border-box;
1894
+ min-width: 0.5rem;
1895
+ outline-offset: 0.25rem;
1896
+ }
1897
+
1898
+ .tl-ring {
1899
+ outline: 1px solid var(--text);
1900
+ }
1901
+
1902
+ .tl-ghost {
1903
+ outline: 1px dashed var(--text);
1904
+ }
1905
+
1906
+ .tl-same {
1907
+ outline: 1px dotted var(--text-muted);
1908
+ }
1909
+
1910
+ /* The element the lanes name with aria-activedescendant: it carries the name of the active note or lane for screen readers and
1911
+ paints nothing. It is not hidden from them, so it is clipped to nothing instead of taking display: none. */
1912
+ .tl-active {
1913
+ position: absolute;
1914
+ width: 1px;
1915
+ height: 1px;
1916
+ overflow: hidden;
1917
+ clip-path: inset(50%);
1918
+ pointer-events: none;
1919
+ }
1920
+
1921
+ .tl-range {
1922
+ position: absolute;
1923
+ top: 0;
1924
+ bottom: 0;
1925
+ box-sizing: border-box;
1926
+ background: color-mix(in srgb, var(--text) 14%, transparent);
1927
+ border-left: 1px solid var(--text-muted);
1928
+ border-right: 1px solid var(--text-muted);
1929
+ }
1930
+
1695
1931
  /* The arrange() sections above the lanes — same grid columns as .lane, so
1696
1932
  a section span lines up with the ruler/lane track column beneath it. No accent is used here
1697
1933
  (accent stays reserved for "live now"/"structural" states, never a static
@@ -2289,6 +2525,7 @@ body {
2289
2525
  #parts-view {
2290
2526
  flex: 1;
2291
2527
  min-height: 0;
2528
+ min-width: 0;
2292
2529
  display: flex;
2293
2530
  flex-direction: column;
2294
2531
  padding: 1rem;
@@ -2439,132 +2676,487 @@ body {
2439
2676
  background: var(--accent);
2440
2677
  }
2441
2678
 
2442
- /* Strudel inserts the CodeMirror container as the NEXT SIBLING of <strudel-editor>,
2443
- not inside it. Fill the available height and let CodeMirror handle its own native
2444
- scrolling/wrapping — no text/font restyling here. The editor itself (.cm-editor) needs the
2445
- height too: CodeMirror scrolls only inside a .cm-editor of bounded height; without it the
2446
- editor grew to its content and #editor-area cut off every line past its bottom. */
2447
- #editor-area > strudel-editor + div,
2448
- #editor-area > strudel-editor + div > .cm-editor {
2449
- height: 100%;
2450
- }
2451
-
2452
- #audio-unlock-overlay {
2453
- position: fixed;
2454
- inset: 0;
2455
- margin: 0;
2456
- padding: 1rem;
2457
- border: none;
2458
- background: rgba(11, 13, 16, 0.85);
2679
+ /* The lines above the Song grid in the Parts view. #song-notes sits between the message and the grid so its lines do not
2680
+ scroll away with a long grid. No accent, no motion. */
2681
+ #song-notes {
2459
2682
  display: flex;
2460
2683
  flex-direction: column;
2461
- align-items: center;
2462
- justify-content: center;
2463
- cursor: pointer;
2464
- font-family: inherit;
2465
- /* Stays above the Ask Claude box (z-index 1000). */
2466
- z-index: 2000;
2684
+ gap: 0.5rem;
2685
+ padding-bottom: 0.5rem;
2686
+ border-bottom: 1px solid var(--border);
2467
2687
  }
2468
2688
 
2469
- /* The display rule above beats the browser's [hidden] default, so hiding needs its own rule. */
2470
- #audio-unlock-overlay[hidden] {
2689
+ #song-notes[hidden] {
2471
2690
  display: none;
2472
2691
  }
2473
2692
 
2474
- /* Overlay counterpart of the top-bar accent rule (#top-bar [data-accent="true"]):
2475
- the overlay is a button with its own browser text color, so the CTA span needs
2476
- its own accent rule to render in teal. */
2477
- #audio-unlock-overlay [data-accent="true"] {
2478
- color: var(--accent);
2479
- }
2480
-
2481
- #audio-unlock-cta {
2482
- font-size: 1.5rem;
2483
- font-weight: 600;
2484
- line-height: 1.2;
2485
- }
2486
-
2487
- #audio-error {
2488
- margin-top: 2rem;
2693
+ .song-note {
2694
+ margin: 0;
2489
2695
  font-size: 1rem;
2490
2696
  font-weight: 400;
2491
2697
  line-height: 1.5;
2492
2698
  color: var(--text);
2699
+ overflow-wrap: anywhere;
2493
2700
  }
2494
2701
 
2495
- #audio-error[hidden] {
2496
- display: none;
2702
+ .song-note[data-kind="check-failed"] {
2703
+ color: var(--text-muted);
2497
2704
  }
2498
2705
 
2499
- /* The Mixer view. #mixer-view fills the panel like #timeline-view; #mixer-strips is its
2500
- only scroll container (both axes). Strips never wrap and never shrink below 11rem. */
2501
- #mixer-view {
2502
- flex: 1;
2503
- min-height: 0;
2504
- overflow: hidden;
2706
+ /* The toolbar above the Song grid: what a launched section waits for, and where the music is. #song-launch shares the
2707
+ segmented group look of the top bar (the selector lists near #view-buttons). No accent, no motion. */
2708
+ #song-bar {
2505
2709
  display: flex;
2506
- flex-direction: column;
2710
+ flex-wrap: wrap;
2711
+ align-items: center;
2712
+ column-gap: 1rem;
2713
+ row-gap: 0.5rem;
2507
2714
  }
2508
2715
 
2509
- #mixer-view[hidden] {
2716
+ #song-bar[hidden] {
2510
2717
  display: none;
2511
2718
  }
2512
2719
 
2513
- /* The one notice line above the strips: disconnected, or a hand edit that was not evaluated. */
2514
- #mixer-notice {
2720
+ #song-launch-label,
2721
+ #song-now {
2722
+ margin: 0;
2515
2723
  font-size: 0.8125rem;
2516
2724
  font-weight: 400;
2517
2725
  line-height: 1.5;
2518
2726
  color: var(--text-muted);
2519
- margin: 0;
2520
- padding: 0.5rem 1rem;
2521
- border-bottom: 1px solid var(--border);
2522
2727
  }
2523
2728
 
2524
- #mixer-notice[hidden] {
2729
+ #song-now[hidden] {
2525
2730
  display: none;
2526
2731
  }
2527
2732
 
2528
- #mixer-message {
2733
+ /* The one sentence under the toolbar: why the grid is off, what waits, or why a gesture changed nothing. It is the only
2734
+ live region of the view and changes only on a gesture or a state change. A queued launch and one that needs no code
2735
+ change are information, so they are muted; the rest read in the plain text color. No accent, no motion. */
2736
+ #song-line {
2737
+ margin: 0;
2529
2738
  font-size: 1rem;
2530
2739
  font-weight: 400;
2531
2740
  line-height: 1.5;
2741
+ color: var(--text);
2742
+ overflow-wrap: anywhere;
2743
+ }
2744
+
2745
+ #song-line[data-kind="queued"],
2746
+ #song-line[data-kind="already"] {
2532
2747
  color: var(--text-muted);
2533
- margin: 0;
2534
- padding: 1rem;
2535
2748
  }
2536
2749
 
2537
- #mixer-message[hidden] {
2750
+ #song-line[hidden] {
2538
2751
  display: none;
2539
2752
  }
2540
2753
 
2541
- #mixer-body {
2542
- display: flex;
2754
+ /* The Song grid: sections as columns, groups and tracks as rows. Only .song-grid-scroll scrolls, in both directions; the
2755
+ name column and the header row stay in place (sticky) and are opaque so scrolled cells slide under them. The section
2756
+ header is the Parts card (.part-card), so the playing and queued colors come from the rules above. Nothing here uses an
2757
+ accent, a destructive or a Jam color, and nothing moves: no transition and no animation. */
2758
+ #parts-rows[data-view="song"] {
2543
2759
  flex: 1;
2544
2760
  min-height: 0;
2761
+ min-width: 0;
2762
+ overflow: hidden;
2545
2763
  }
2546
2764
 
2547
- #mixer-body[hidden] {
2548
- display: none;
2765
+ .song-grid-scroll {
2766
+ flex: 1;
2767
+ min-width: 0;
2768
+ min-height: 0;
2769
+ overflow: auto;
2770
+ scroll-padding-left: 10rem;
2771
+ scroll-padding-top: 7rem;
2549
2772
  }
2550
2773
 
2551
- /* The MIDI tracks that play no notes, in one quiet line under the strips. */
2552
- #mixer-nonotes {
2553
- font-size: 0.8125rem;
2554
- font-weight: 400;
2555
- line-height: 1.5;
2556
- color: var(--text-muted);
2557
- margin: 0;
2558
- padding: 0.5rem 1rem;
2559
- border-top: 1px solid var(--border);
2774
+ .song-grid {
2775
+ border-collapse: separate;
2776
+ border-spacing: 0;
2777
+ table-layout: fixed;
2778
+ width: 100%;
2779
+ min-width: calc(10rem + var(--song-columns) * 10rem);
2780
+ background: var(--dominant);
2560
2781
  }
2561
2782
 
2562
- #mixer-nonotes[hidden] {
2783
+ .song-col-name {
2784
+ width: 10rem;
2785
+ }
2786
+
2787
+ .song-grid col[data-playing="true"] {
2788
+ background: var(--secondary);
2789
+ }
2790
+
2791
+ .song-grid tr[hidden] {
2563
2792
  display: none;
2564
2793
  }
2565
2794
 
2566
- #mixer-strips {
2567
- flex: 1;
2795
+ .song-grid th,
2796
+ .song-grid td {
2797
+ padding: 0;
2798
+ min-width: 0;
2799
+ text-align: left;
2800
+ vertical-align: top;
2801
+ font-size: 0.8125rem;
2802
+ font-weight: 400;
2803
+ line-height: 1.5;
2804
+ color: var(--text);
2805
+ border-right: 1px solid var(--border);
2806
+ border-bottom: 1px solid var(--border);
2807
+ }
2808
+
2809
+ .song-grid thead th {
2810
+ position: sticky;
2811
+ top: 0;
2812
+ z-index: 2;
2813
+ background: var(--dominant);
2814
+ }
2815
+
2816
+ .song-grid .song-colhead {
2817
+ padding: 0.25rem;
2818
+ }
2819
+
2820
+ .song-grid .song-corner {
2821
+ position: sticky;
2822
+ left: 0;
2823
+ z-index: 3;
2824
+ padding: 0.5rem;
2825
+ color: var(--text-muted);
2826
+ }
2827
+
2828
+ .song-grid .song-rowhead {
2829
+ position: sticky;
2830
+ left: 0;
2831
+ z-index: 1;
2832
+ background: var(--dominant);
2833
+ padding: 0.25rem 0.5rem;
2834
+ padding-inline-start: calc(0.5rem + var(--depth, 0) * 1rem);
2835
+ overflow-wrap: anywhere;
2836
+ vertical-align: middle;
2837
+ }
2838
+
2839
+ .song-grid .song-rowhead[data-deep="true"] {
2840
+ padding-inline-start: 4.5rem;
2841
+ }
2842
+
2843
+ .song-group-row .song-rowhead,
2844
+ .song-group-row td {
2845
+ background: var(--secondary);
2846
+ }
2847
+
2848
+ .song-head {
2849
+ width: 100%;
2850
+ box-sizing: border-box;
2851
+ margin: 0;
2852
+ }
2853
+
2854
+ .song-head .part-name {
2855
+ font-weight: 600;
2856
+ }
2857
+
2858
+ .song-head-line {
2859
+ display: flex;
2860
+ flex-wrap: wrap;
2861
+ align-items: baseline;
2862
+ column-gap: 0.5rem;
2863
+ }
2864
+
2865
+ .song-head .song-head-action,
2866
+ .song-head .song-used {
2867
+ font-family: inherit;
2868
+ font-size: 0.8125rem;
2869
+ font-weight: 400;
2870
+ line-height: 1.5;
2871
+ color: var(--text-muted);
2872
+ }
2873
+
2874
+ .song-head .song-harmony {
2875
+ max-height: 3em;
2876
+ overflow: hidden;
2877
+ overflow-wrap: anywhere;
2878
+ font-size: 0.8125rem;
2879
+ font-weight: 400;
2880
+ line-height: 1.5;
2881
+ color: var(--text-muted);
2882
+ }
2883
+
2884
+ .song-fold {
2885
+ display: inline-flex;
2886
+ align-items: baseline;
2887
+ gap: 0.5rem;
2888
+ max-width: 100%;
2889
+ margin: 0;
2890
+ padding: 0;
2891
+ background: transparent;
2892
+ border: none;
2893
+ color: var(--text);
2894
+ font-family: inherit;
2895
+ font-size: 0.8125rem;
2896
+ font-weight: 600;
2897
+ line-height: 1.5;
2898
+ text-align: left;
2899
+ cursor: pointer;
2900
+ overflow-wrap: anywhere;
2901
+ }
2902
+
2903
+ .song-fold:focus-visible {
2904
+ outline: 1px solid var(--text-muted);
2905
+ outline-offset: 0.25rem;
2906
+ }
2907
+
2908
+ .song-fold-name {
2909
+ min-width: 0;
2910
+ }
2911
+
2912
+ .song-count {
2913
+ font-weight: 400;
2914
+ color: var(--text-muted);
2915
+ }
2916
+
2917
+ .song-cell {
2918
+ display: flex;
2919
+ flex-direction: column;
2920
+ justify-content: center;
2921
+ width: 100%;
2922
+ min-height: 3rem;
2923
+ box-sizing: border-box;
2924
+ margin: 0;
2925
+ padding: 0.25rem 0.5rem;
2926
+ background: transparent;
2927
+ border: 1px solid transparent;
2928
+ color: var(--text);
2929
+ font-family: inherit;
2930
+ font-size: 0.8125rem;
2931
+ font-weight: 400;
2932
+ line-height: 1.5;
2933
+ text-align: left;
2934
+ cursor: pointer;
2935
+ }
2936
+
2937
+ .song-cell:hover:not([aria-disabled="true"]) {
2938
+ border-color: var(--text-muted);
2939
+ }
2940
+
2941
+ .song-cell:focus-visible {
2942
+ outline: 1px solid var(--text-muted);
2943
+ outline-offset: -1px;
2944
+ }
2945
+
2946
+ /* A control the line switched off stays focusable and keeps its text; only the pointer says it does nothing. */
2947
+ .song-cell[aria-disabled="true"],
2948
+ .song-head[aria-disabled="true"] {
2949
+ cursor: default;
2950
+ }
2951
+
2952
+ .song-variation {
2953
+ white-space: nowrap;
2954
+ overflow: hidden;
2955
+ text-overflow: ellipsis;
2956
+ }
2957
+
2958
+ .song-cell[data-kind="rest"] .song-variation,
2959
+ .song-wait,
2960
+ .song-fit {
2961
+ color: var(--text-muted);
2962
+ }
2963
+
2964
+ /* The second line of a cell: whether a fixed variation fits its section's harmony, or that the cell's change waits for its
2965
+ line. Words in the muted colour, two lines at most; the whole text is in the cell's title. */
2966
+ .song-wait,
2967
+ .song-fit {
2968
+ max-height: 3em;
2969
+ overflow: hidden;
2970
+ overflow-wrap: anywhere;
2971
+ }
2972
+
2973
+ .song-cell[aria-expanded="true"] {
2974
+ border-color: var(--text-muted);
2975
+ }
2976
+
2977
+ /* The list that opens on a cell. It is fixed to the window and lives outside the grid's scroller, so the scroller never
2978
+ clips it; the page puts it below the cell or above it and keeps it inside the window. */
2979
+ #song-cell-list {
2980
+ position: fixed;
2981
+ z-index: 5;
2982
+ box-sizing: border-box;
2983
+ min-width: 10rem;
2984
+ max-width: 20rem;
2985
+ max-height: 16rem;
2986
+ overflow-y: auto;
2987
+ margin: 0;
2988
+ padding: 0.25rem;
2989
+ background: var(--secondary);
2990
+ border: 1px solid var(--text-muted);
2991
+ }
2992
+
2993
+ #song-cell-list[hidden] {
2994
+ display: none;
2995
+ }
2996
+
2997
+ .song-option {
2998
+ display: flex;
2999
+ flex-wrap: wrap;
3000
+ align-items: baseline;
3001
+ column-gap: 0.5rem;
3002
+ padding: 0.25rem 0.5rem;
3003
+ font-size: 0.8125rem;
3004
+ line-height: 1.5;
3005
+ color: var(--text);
3006
+ cursor: pointer;
3007
+ overflow-wrap: anywhere;
3008
+ }
3009
+
3010
+ .song-option[data-active="true"] {
3011
+ box-shadow: inset 0 0 0 1px var(--text-muted);
3012
+ }
3013
+
3014
+ .song-option-note {
3015
+ color: var(--text-muted);
3016
+ }
3017
+
3018
+ .song-list-note {
3019
+ padding: 0.25rem 0.5rem;
3020
+ font-size: 0.8125rem;
3021
+ line-height: 1.5;
3022
+ overflow-wrap: anywhere;
3023
+ }
3024
+
3025
+ .song-option-rest {
3026
+ border-top: 1px solid var(--border);
3027
+ }
3028
+
3029
+ .song-grid .song-outside-note {
3030
+ padding: 0.25rem 0.5rem;
3031
+ color: var(--text-muted);
3032
+ }
3033
+
3034
+ /* Strudel inserts the CodeMirror container as the NEXT SIBLING of <strudel-editor>,
3035
+ not inside it. Fill the available height and let CodeMirror handle its own native
3036
+ scrolling/wrapping — no text/font restyling here. The editor itself (.cm-editor) needs the
3037
+ height too: CodeMirror scrolls only inside a .cm-editor of bounded height; without it the
3038
+ editor grew to its content and #editor-area cut off every line past its bottom. */
3039
+ #editor-area > strudel-editor + div,
3040
+ #editor-area > strudel-editor + div > .cm-editor {
3041
+ height: 100%;
3042
+ }
3043
+
3044
+ #audio-unlock-overlay {
3045
+ position: fixed;
3046
+ inset: 0;
3047
+ margin: 0;
3048
+ padding: 1rem;
3049
+ border: none;
3050
+ background: rgba(11, 13, 16, 0.85);
3051
+ display: flex;
3052
+ flex-direction: column;
3053
+ align-items: center;
3054
+ justify-content: center;
3055
+ cursor: pointer;
3056
+ font-family: inherit;
3057
+ /* Stays above the Ask Claude box (z-index 1000). */
3058
+ z-index: 2000;
3059
+ }
3060
+
3061
+ /* The display rule above beats the browser's [hidden] default, so hiding needs its own rule. */
3062
+ #audio-unlock-overlay[hidden] {
3063
+ display: none;
3064
+ }
3065
+
3066
+ /* Overlay counterpart of the top-bar accent rule (#top-bar [data-accent="true"]):
3067
+ the overlay is a button with its own browser text color, so the CTA span needs
3068
+ its own accent rule to render in teal. */
3069
+ #audio-unlock-overlay [data-accent="true"] {
3070
+ color: var(--accent);
3071
+ }
3072
+
3073
+ #audio-unlock-cta {
3074
+ font-size: 1.5rem;
3075
+ font-weight: 600;
3076
+ line-height: 1.2;
3077
+ }
3078
+
3079
+ #audio-error {
3080
+ margin-top: 2rem;
3081
+ font-size: 1rem;
3082
+ font-weight: 400;
3083
+ line-height: 1.5;
3084
+ color: var(--text);
3085
+ }
3086
+
3087
+ #audio-error[hidden] {
3088
+ display: none;
3089
+ }
3090
+
3091
+ /* The Mixer view. #mixer-view fills the panel like #timeline-view; #mixer-strips is its
3092
+ only scroll container (both axes). Strips never wrap and never shrink below 11rem. */
3093
+ #mixer-view {
3094
+ flex: 1;
3095
+ min-height: 0;
3096
+ overflow: hidden;
3097
+ display: flex;
3098
+ flex-direction: column;
3099
+ }
3100
+
3101
+ #mixer-view[hidden] {
3102
+ display: none;
3103
+ }
3104
+
3105
+ /* The one notice line above the strips: disconnected, or a hand edit that was not evaluated. */
3106
+ #mixer-notice {
3107
+ font-size: 0.8125rem;
3108
+ font-weight: 400;
3109
+ line-height: 1.5;
3110
+ color: var(--text-muted);
3111
+ margin: 0;
3112
+ padding: 0.5rem 1rem;
3113
+ border-bottom: 1px solid var(--border);
3114
+ }
3115
+
3116
+ #mixer-notice[hidden] {
3117
+ display: none;
3118
+ }
3119
+
3120
+ #mixer-message {
3121
+ font-size: 1rem;
3122
+ font-weight: 400;
3123
+ line-height: 1.5;
3124
+ color: var(--text-muted);
3125
+ margin: 0;
3126
+ padding: 1rem;
3127
+ }
3128
+
3129
+ #mixer-message[hidden] {
3130
+ display: none;
3131
+ }
3132
+
3133
+ #mixer-body {
3134
+ display: flex;
3135
+ flex: 1;
3136
+ min-height: 0;
3137
+ }
3138
+
3139
+ #mixer-body[hidden] {
3140
+ display: none;
3141
+ }
3142
+
3143
+ /* The MIDI tracks that play no notes, in one quiet line under the strips. */
3144
+ #mixer-nonotes {
3145
+ font-size: 0.8125rem;
3146
+ font-weight: 400;
3147
+ line-height: 1.5;
3148
+ color: var(--text-muted);
3149
+ margin: 0;
3150
+ padding: 0.5rem 1rem;
3151
+ border-top: 1px solid var(--border);
3152
+ }
3153
+
3154
+ #mixer-nonotes[hidden] {
3155
+ display: none;
3156
+ }
3157
+
3158
+ #mixer-strips {
3159
+ flex: 1;
2568
3160
  min-width: 0;
2569
3161
  min-height: 0;
2570
3162
  overflow: auto;
@@ -2603,6 +3195,196 @@ body {
2603
3195
  .mx-strip[data-color="7"] { --strip-color: var(--track-7); }
2604
3196
  .mx-strip[data-color="8"] { --strip-color: var(--track-8); }
2605
3197
 
3198
+ /* A group of the song is a frame around the strips of its tracks (and of its sub-groups). The head names the group and
3199
+ stays at the left of the strips area while a wide group scrolls; from the fifth level the frame drops its padding, so a
3200
+ deep tree keeps room for names. A folded group shows its head only. */
3201
+ .mx-group {
3202
+ flex: 0 0 auto;
3203
+ align-self: flex-start;
3204
+ min-height: 100%;
3205
+ min-width: 0;
3206
+ box-sizing: border-box;
3207
+ display: flex;
3208
+ flex-direction: column;
3209
+ gap: 0.5rem;
3210
+ padding: 0.5rem;
3211
+ border: 1px solid var(--border);
3212
+ border-top: 0.25rem solid var(--border);
3213
+ }
3214
+
3215
+ .mx-group[data-deep="true"] {
3216
+ padding: 0;
3217
+ gap: 0;
3218
+ }
3219
+
3220
+ .mx-group-head {
3221
+ position: sticky;
3222
+ left: 1rem;
3223
+ align-self: flex-start;
3224
+ box-sizing: border-box;
3225
+ width: 11rem;
3226
+ display: flex;
3227
+ flex-direction: column;
3228
+ gap: 0.25rem;
3229
+ background: var(--secondary);
3230
+ }
3231
+
3232
+ .mx-group-title {
3233
+ display: flex;
3234
+ align-items: center;
3235
+ gap: 0.5rem;
3236
+ min-width: 0;
3237
+ min-height: 1.5rem;
3238
+ }
3239
+
3240
+ .mx-fold {
3241
+ flex: 1 1 auto;
3242
+ min-width: 0;
3243
+ display: flex;
3244
+ align-items: baseline;
3245
+ gap: 0.5rem;
3246
+ min-height: 1.5rem;
3247
+ margin: 0;
3248
+ padding: 0;
3249
+ background: none;
3250
+ border: 0;
3251
+ color: var(--text);
3252
+ font-family: inherit;
3253
+ font-size: 0.8125rem;
3254
+ font-weight: 600;
3255
+ line-height: 1.25;
3256
+ text-align: left;
3257
+ cursor: pointer;
3258
+ }
3259
+
3260
+ .mx-fold-chevron {
3261
+ flex: 0 0 auto;
3262
+ font-weight: 400;
3263
+ color: var(--text-muted);
3264
+ }
3265
+
3266
+ .mx-fold:hover .mx-fold-chevron {
3267
+ color: var(--text);
3268
+ }
3269
+
3270
+ .mx-fold-name {
3271
+ min-width: 0;
3272
+ overflow: hidden;
3273
+ text-overflow: ellipsis;
3274
+ white-space: nowrap;
3275
+ }
3276
+
3277
+ .mx-fold:focus-visible {
3278
+ outline: 1px solid var(--text-muted);
3279
+ outline-offset: -1px;
3280
+ }
3281
+
3282
+ .mx-group-line {
3283
+ display: flex;
3284
+ align-items: center;
3285
+ gap: 0.25rem;
3286
+ }
3287
+
3288
+ .mx-group-count {
3289
+ flex: 1 1 auto;
3290
+ min-width: 0;
3291
+ overflow: hidden;
3292
+ text-overflow: ellipsis;
3293
+ white-space: nowrap;
3294
+ font-size: 0.8125rem;
3295
+ font-weight: 400;
3296
+ line-height: 1.25;
3297
+ color: var(--text-muted);
3298
+ }
3299
+
3300
+ /* Some of the group's tracks are muted or soloed: a dashed outline in the text colour (the count line says how many). */
3301
+ .mx-group-head .mx-mute[aria-pressed="mixed"],
3302
+ .mx-group-head .mx-solo[aria-pressed="mixed"] {
3303
+ border-style: dashed;
3304
+ border-color: var(--text);
3305
+ color: var(--text);
3306
+ }
3307
+
3308
+ .mx-group-head [data-writes="true"]:disabled {
3309
+ cursor: default;
3310
+ }
3311
+
3312
+ .mx-group-note {
3313
+ margin: 0;
3314
+ font-size: 0.8125rem;
3315
+ font-weight: 400;
3316
+ line-height: 1.25;
3317
+ color: var(--text-muted);
3318
+ overflow-wrap: anywhere;
3319
+ }
3320
+
3321
+ .mx-group-note[hidden] {
3322
+ display: none;
3323
+ }
3324
+
3325
+ .mx-group-body {
3326
+ flex: 1 1 auto;
3327
+ min-width: 0;
3328
+ display: flex;
3329
+ align-items: stretch;
3330
+ gap: 0.5rem;
3331
+ padding-top: 0.5rem;
3332
+ border-top: 1px solid var(--border);
3333
+ }
3334
+
3335
+ .mx-group-body[hidden] {
3336
+ display: none;
3337
+ }
3338
+
3339
+ .mx-group-body > .mx-strip,
3340
+ .mx-group-body > .mx-group {
3341
+ align-self: stretch;
3342
+ min-height: 0;
3343
+ }
3344
+
3345
+ /* A track strip's name row: the light, then the name button. The light's box never changes size, so a blink moves nothing.
3346
+ The loop in the page writes the fade as the fill's opacity; nothing here animates. */
3347
+ .mx-name-row {
3348
+ display: flex;
3349
+ align-items: center;
3350
+ gap: 0.5rem;
3351
+ min-height: 1.5rem;
3352
+ min-width: 0;
3353
+ }
3354
+
3355
+ .mx-name-row > button.mx-name {
3356
+ flex: 1 1 auto;
3357
+ width: auto;
3358
+ min-width: 0;
3359
+ }
3360
+
3361
+ .mx-light {
3362
+ flex: 0 0 auto;
3363
+ position: relative;
3364
+ box-sizing: border-box;
3365
+ width: 0.75rem;
3366
+ height: 0.75rem;
3367
+ border: 1px solid var(--text-muted);
3368
+ border-radius: 50%;
3369
+ }
3370
+
3371
+ .mx-light[hidden] {
3372
+ display: none;
3373
+ }
3374
+
3375
+ .mx-light-fill {
3376
+ position: absolute;
3377
+ inset: 0;
3378
+ border-radius: 50%;
3379
+ background: var(--text);
3380
+ opacity: 0;
3381
+ }
3382
+
3383
+ /* The steady state of motion off only: lit while the track played within the last cycle. */
3384
+ .mx-light[data-lit="true"] .mx-light-fill {
3385
+ opacity: 1;
3386
+ }
3387
+
2606
3388
  .mx-name {
2607
3389
  font-size: 0.8125rem;
2608
3390
  font-weight: 600;
@@ -3794,6 +4576,19 @@ button.mx-name:focus-visible {
3794
4576
  color: var(--text-muted);
3795
4577
  }
3796
4578
 
4579
+ /* The section whose harmony the line shows, after the value: shown only for a piece with sections. */
4580
+ #jam-declared-section {
4581
+ flex: none;
4582
+ font-size: 0.8125rem;
4583
+ font-weight: 400;
4584
+ line-height: 1.5;
4585
+ color: var(--text-muted);
4586
+ }
4587
+
4588
+ #jam-declared-section[hidden] {
4589
+ display: none;
4590
+ }
4591
+
3797
4592
  #jam-degrees {
3798
4593
  display: flex;
3799
4594
  flex-direction: row;