moontide-mcp 2.0.2 → 2.2.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 (57) hide show
  1. package/README.md +94 -6
  2. package/dist/ask.js +76 -15
  3. package/dist/mcp.js +198 -26
  4. package/dist/protocol.js +130 -2
  5. package/dist/reference.js +22 -0
  6. package/dist/request-status.js +221 -0
  7. package/dist/session.js +256 -7
  8. package/package.json +1 -1
  9. package/page/ask-view.js +64 -57
  10. package/page/ask.js +363 -188
  11. package/page/bar-model.js +43 -2
  12. package/page/change-timing.js +60 -4
  13. package/page/harmony.js +13 -0
  14. package/page/index.html +7 -4
  15. package/page/jam-analysis.js +68 -23
  16. package/page/jam-model.js +196 -31
  17. package/page/jam-resolve.js +271 -0
  18. package/page/jam-view.js +182 -10
  19. package/page/lights.js +300 -0
  20. package/page/main.js +2171 -188
  21. package/page/midi-clock.js +23 -4
  22. package/page/midi-out.js +17 -2
  23. package/page/mixer-groups.js +114 -0
  24. package/page/mixer-model.js +172 -29
  25. package/page/mixer-rewrite.js +30 -1
  26. package/page/mixer-view.js +289 -14
  27. package/page/note-edit.js +267 -0
  28. package/page/note-locate.js +346 -0
  29. package/page/note-panel.js +402 -0
  30. package/page/note-tag.js +141 -0
  31. package/page/page-rewrite.js +184 -0
  32. package/page/query-check.js +21 -2
  33. package/page/request-status-view.js +419 -0
  34. package/page/request-status.js +686 -0
  35. package/page/song-edit.js +230 -0
  36. package/page/song-fit.js +250 -0
  37. package/page/song-grid-model.js +506 -0
  38. package/page/song-grid-view.js +710 -0
  39. package/page/song-prefs.js +100 -0
  40. package/page/structure-check.js +216 -0
  41. package/page/structure-view.js +38 -0
  42. package/page/structure.js +761 -0
  43. package/page/style.css +1218 -65
  44. package/page/takeover.js +20 -5
  45. package/page/tempo-glide.js +330 -0
  46. package/page/tempo-guard.js +62 -10
  47. package/page/timeline-edit-view.js +1956 -0
  48. package/page/timeline-gesture.js +279 -0
  49. package/page/timeline.js +197 -3
  50. package/page/top-bar-view.js +18 -13
  51. package/page/tracks.js +37 -8
  52. package/page/ui-state.js +10 -5
  53. package/page/views.js +39 -9
  54. package/reference/functions.md +1 -0
  55. package/reference/midi.md +2 -0
  56. package/reference/structure.md +131 -0
  57. package/reference/transitions.md +13 -1
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
 
@@ -450,34 +456,6 @@ body {
450
456
  font-size: 0.8125rem;
451
457
  }
452
458
 
453
- /* The status of the latest request to Claude. No display rule: #top-bar > span styles it and the browser's [hidden]
454
- works unaided, so nothing to say takes no width. It sits before the Session button in the markup, so it is right of
455
- the Layout group. It holds the long sentence and a short form: the short one is for fit step 3 and is aria-hidden. */
456
- #ask-status {
457
- flex: none;
458
- }
459
-
460
- #ask-status .ask-short {
461
- display: none;
462
- }
463
-
464
- /* Fit step 3 and 4: the short form shows, and the long sentence is visually hidden but kept in the accessibility tree
465
- (never display: none), the recipe of #change-status. */
466
- #top-bar[data-fit="3"] #ask-status .ask-short,
467
- #top-bar[data-fit="4"] #ask-status .ask-short {
468
- display: inline;
469
- }
470
-
471
- #top-bar[data-fit="3"] #ask-status .ask-long,
472
- #top-bar[data-fit="4"] #ask-status .ask-long {
473
- position: absolute;
474
- width: 1px;
475
- height: 1px;
476
- overflow: hidden;
477
- clip-path: inset(50%);
478
- white-space: nowrap;
479
- }
480
-
481
459
  /* The cycle and beat beside the tempo, quiet decoration (Label size, mono, muted). The hidden attribute
482
460
  hides it while nothing plays, so it has no display rule. Fit step 1 drops it with the wordmark (.bar-deco). */
483
461
  #top-bar #cycle-beat {
@@ -488,9 +466,7 @@ body {
488
466
  }
489
467
 
490
468
  /* Whether page requests reach Claude live. No display rule: #top-bar > span
491
- styles it and the browser's [hidden] works unaided. After #ask-status in the markup, same order,
492
- so it sits right of the request status and left of the motion switch, and the request status
493
- appearing never moves it. */
469
+ styles it and the browser's [hidden] works unaided. */
494
470
  #page-requests {
495
471
  flex-shrink: 0;
496
472
  }
@@ -1009,6 +985,300 @@ body {
1009
985
  font-weight: 400;
1010
986
  }
1011
987
 
988
+ /* The Requests chip, before the Session button: the same box as every control of the bar (1.75rem high, 0.75rem at the
989
+ sides), so it lines up with its neighbour. It is hidden while no request was sent, and the count in its pill is the
990
+ requests still in flight. Neutral colors only: the accent means live now and red belongs to the failed-change banner.
991
+ No transition and no animation: everything changes in one frame. */
992
+ #requests-toggle {
993
+ flex: none;
994
+ display: inline-flex;
995
+ align-items: center;
996
+ gap: 0.25rem;
997
+ box-sizing: border-box;
998
+ min-width: 1.75rem;
999
+ min-height: 1.75rem;
1000
+ padding: 0 0.75rem;
1001
+ background: transparent;
1002
+ border: 1px solid transparent;
1003
+ border-radius: 0.5rem;
1004
+ color: var(--text-muted);
1005
+ font-family: inherit;
1006
+ font-size: 0.9375rem;
1007
+ font-weight: 400;
1008
+ line-height: 1.5;
1009
+ white-space: nowrap;
1010
+ cursor: pointer;
1011
+ }
1012
+
1013
+ #requests-toggle[hidden] {
1014
+ display: none;
1015
+ }
1016
+
1017
+ #requests-toggle[data-inflight="true"] {
1018
+ color: var(--text);
1019
+ }
1020
+
1021
+ /* The word is the text color on hover, never muted on the fill. */
1022
+ #requests-toggle:hover {
1023
+ background: var(--surface-2);
1024
+ color: var(--text);
1025
+ }
1026
+
1027
+ #requests-toggle[aria-expanded="true"] {
1028
+ color: var(--text);
1029
+ border-color: var(--border);
1030
+ }
1031
+
1032
+ #requests-toggle:focus-visible {
1033
+ outline: 1px solid var(--text-muted);
1034
+ outline-offset: 1px;
1035
+ }
1036
+
1037
+ .rq-count {
1038
+ background: var(--surface-2);
1039
+ color: var(--text);
1040
+ border-radius: 1rem;
1041
+ padding: 0 0.5rem;
1042
+ font-family: var(--font-mono);
1043
+ font-size: 0.8125rem;
1044
+ font-weight: 400;
1045
+ }
1046
+
1047
+ #requests-toggle[data-inflight="true"] .rq-count {
1048
+ font-weight: 600;
1049
+ }
1050
+
1051
+ .rq-count[hidden] {
1052
+ display: none;
1053
+ }
1054
+
1055
+ #top-bar[data-fit="4"] #requests-toggle {
1056
+ padding: 0 0.5rem;
1057
+ }
1058
+
1059
+ /* The panel under the bar's right end: an overlay that pushes nothing, under the Ask box and the popovers (1000) and
1060
+ the return banner (900), level with the Session drawer (800), which it never shares the screen with. One row per
1061
+ request, newest first; it scrolls inside itself. */
1062
+ #requests-panel {
1063
+ position: absolute;
1064
+ top: 100%;
1065
+ right: 1rem;
1066
+ margin-top: 0.5rem;
1067
+ z-index: 800;
1068
+ box-sizing: border-box;
1069
+ width: 26rem;
1070
+ max-width: calc(100vw - 2rem);
1071
+ max-height: 20rem;
1072
+ overflow-y: auto;
1073
+ padding: 0.5rem;
1074
+ display: flex;
1075
+ flex-direction: column;
1076
+ gap: 0.25rem;
1077
+ background: var(--secondary);
1078
+ border: 1px solid var(--text-muted);
1079
+ box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.45);
1080
+ font-size: 0.8125rem;
1081
+ line-height: 1.5;
1082
+ }
1083
+
1084
+ #requests-panel[hidden] {
1085
+ display: none;
1086
+ }
1087
+
1088
+ #requests-panel:focus {
1089
+ outline: none;
1090
+ }
1091
+
1092
+ #requests-panel:focus-visible {
1093
+ outline: 1px solid var(--text-muted);
1094
+ outline-offset: -1px;
1095
+ }
1096
+
1097
+ .rq-list,
1098
+ .rq-recent-body {
1099
+ margin: 0;
1100
+ padding: 0;
1101
+ list-style: none;
1102
+ display: flex;
1103
+ flex-direction: column;
1104
+ gap: 0.25rem;
1105
+ }
1106
+
1107
+ .rq-recent-body[hidden] {
1108
+ display: none;
1109
+ }
1110
+
1111
+ .rq-recent[hidden] {
1112
+ display: none;
1113
+ }
1114
+
1115
+ /* A row is two lines: the start of the request and its time, then a shape and the words of the state. The hover and
1116
+ the open state change the border, never the fill, so muted text is never placed on the lighter surface. */
1117
+ .rq-row {
1118
+ display: grid;
1119
+ grid-template-columns: 0.5rem 1fr auto;
1120
+ column-gap: 0.5rem;
1121
+ align-items: center;
1122
+ width: 100%;
1123
+ min-width: 1.75rem;
1124
+ min-height: 3rem;
1125
+ box-sizing: border-box;
1126
+ padding: 0.5rem 1rem;
1127
+ background: transparent;
1128
+ border: 1px solid transparent;
1129
+ color: var(--text);
1130
+ font-family: inherit;
1131
+ font-size: 0.8125rem;
1132
+ font-weight: 400;
1133
+ line-height: 1.5;
1134
+ text-align: left;
1135
+ cursor: pointer;
1136
+ }
1137
+
1138
+ .rq-row:hover {
1139
+ border-color: var(--border);
1140
+ }
1141
+
1142
+ .rq-row[aria-expanded="true"] {
1143
+ border-color: var(--border);
1144
+ }
1145
+
1146
+ .rq-row:focus-visible,
1147
+ .rq-recent-head:focus-visible {
1148
+ outline: 1px solid var(--text-muted);
1149
+ outline-offset: -1px;
1150
+ }
1151
+
1152
+ .rq-text {
1153
+ grid-column: 1 / 3;
1154
+ min-width: 0;
1155
+ overflow: hidden;
1156
+ text-overflow: ellipsis;
1157
+ white-space: nowrap;
1158
+ }
1159
+
1160
+ .rq-time {
1161
+ grid-column: 3;
1162
+ font-family: var(--font-mono);
1163
+ color: var(--text-muted);
1164
+ white-space: nowrap;
1165
+ }
1166
+
1167
+ /* The shape backs the words up and nothing more: a ring while it goes on, a disc when it is done, a dashed ring when
1168
+ it ended without a change, a square when it did not work. Text colors only. */
1169
+ .rq-mark {
1170
+ grid-column: 1;
1171
+ grid-row: 2;
1172
+ width: 0.5rem;
1173
+ height: 0.5rem;
1174
+ box-sizing: border-box;
1175
+ border-radius: 50%;
1176
+ border: 1px solid var(--text-muted);
1177
+ }
1178
+
1179
+ .rq-mark[data-shape="done"] {
1180
+ background: var(--text);
1181
+ border-color: var(--text);
1182
+ }
1183
+
1184
+ .rq-mark[data-shape="ended"] {
1185
+ border-style: dashed;
1186
+ }
1187
+
1188
+ .rq-mark[data-shape="bad"] {
1189
+ background: var(--text);
1190
+ border-color: var(--text);
1191
+ border-radius: 0;
1192
+ }
1193
+
1194
+ .rq-state {
1195
+ grid-column: 2 / 4;
1196
+ grid-row: 2;
1197
+ min-width: 0;
1198
+ overflow: hidden;
1199
+ text-overflow: ellipsis;
1200
+ white-space: nowrap;
1201
+ }
1202
+
1203
+ .rq-item[data-finished="true"] .rq-text {
1204
+ color: var(--text-muted);
1205
+ }
1206
+
1207
+ .rq-detail {
1208
+ box-sizing: border-box;
1209
+ margin: 0.25rem 0 0;
1210
+ padding: 0.5rem;
1211
+ max-height: 8rem;
1212
+ overflow-y: auto;
1213
+ background: var(--dominant);
1214
+ border: 1px solid var(--border);
1215
+ overflow-wrap: anywhere;
1216
+ }
1217
+
1218
+ .rq-detail[hidden] {
1219
+ display: none;
1220
+ }
1221
+
1222
+ .rq-detail p {
1223
+ margin: 0 0 0.5rem;
1224
+ }
1225
+
1226
+ .rq-detail p:last-child {
1227
+ margin-bottom: 0;
1228
+ }
1229
+
1230
+ .rq-detail .rq-about,
1231
+ .rq-detail .rq-basis {
1232
+ color: var(--text-muted);
1233
+ }
1234
+
1235
+ .rq-detail .rq-reason {
1236
+ font-family: var(--font-mono);
1237
+ white-space: pre-wrap;
1238
+ }
1239
+
1240
+ .rq-recent-head {
1241
+ display: flex;
1242
+ align-items: center;
1243
+ gap: 0.5rem;
1244
+ width: 100%;
1245
+ min-width: 2rem;
1246
+ min-height: 2rem;
1247
+ box-sizing: border-box;
1248
+ padding: 0 1rem;
1249
+ background: transparent;
1250
+ border: 1px solid transparent;
1251
+ color: var(--text-muted);
1252
+ font-family: inherit;
1253
+ font-size: 0.8125rem;
1254
+ font-weight: 400;
1255
+ line-height: 1.5;
1256
+ text-align: left;
1257
+ cursor: pointer;
1258
+ }
1259
+
1260
+ .rq-recent-head:hover {
1261
+ color: var(--text);
1262
+ border-color: var(--border);
1263
+ }
1264
+
1265
+ .rq-recent .rq-item .rq-state {
1266
+ color: var(--text-muted);
1267
+ }
1268
+
1269
+ /* The spoken line of the Requests list: visually hidden, kept in the accessibility tree, always present and never
1270
+ [hidden] (the recipe of #change-status). */
1271
+ #requests-live {
1272
+ position: absolute;
1273
+ width: 1px;
1274
+ height: 1px;
1275
+ overflow: hidden;
1276
+ clip-path: inset(50%);
1277
+ white-space: nowrap;
1278
+ font-size: 0.8125rem;
1279
+ line-height: 1.5;
1280
+ }
1281
+
1012
1282
  /* The strip: one muted line under the bar, only while the drawer is closed and a condition holds. */
1013
1283
  #session-strip {
1014
1284
  display: flex;
@@ -1645,7 +1915,9 @@ body {
1645
1915
  grid-row: 1;
1646
1916
  }
1647
1917
 
1648
- .lane[data-drum-rows] .lane-events {
1918
+ /* 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
1919
+ cell) sit on the notes whatever height the lane's name column gives the lane. */
1920
+ .lane-events {
1649
1921
  align-self: stretch;
1650
1922
  width: 100%;
1651
1923
  height: 100%;
@@ -1692,17 +1964,245 @@ body {
1692
1964
  .lane[data-drum-rows="7"] { min-height: 8.75rem; }
1693
1965
  .lane[data-drum-rows="8"] { min-height: 10rem; }
1694
1966
 
1695
- /* The arrange() sections above the lanes — same grid columns as .lane, so
1696
- a section span lines up with the ruler/lane track column beneath it. No accent is used here
1697
- (accent stays reserved for "live now"/"structural" states, never a static
1698
- section label). */
1699
- .section-row {
1700
- display: grid;
1701
- grid-template-columns: 8.5em minmax(0, 1fr);
1702
- height: 1.5em;
1703
- }
1704
-
1705
- .section-row-gutter {
1967
+ /* The editable Timeline: the panel under the lanes, and the marks on a lane. The panel is a quiet strip that is always the
1968
+ same height, so the lanes do not move under the pointer when a note is picked; its two text lines keep their room even
1969
+ when empty. A selected note gets a solid ring, a preview a dashed one, the other places of the same token a dotted one
1970
+ and a range of cycles a wash with two edges: outlines and words only, never the accent colors, which keep their uses
1971
+ (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
1972
+ pointer and are hidden from screen readers. Nothing here moves, fades or animates. */
1973
+ #timeline-note {
1974
+ flex: 0 0 auto;
1975
+ box-sizing: border-box;
1976
+ display: flex;
1977
+ flex-wrap: wrap;
1978
+ align-items: center;
1979
+ gap: 0.25rem 1rem;
1980
+ padding: 0.5rem 1rem;
1981
+ background: var(--secondary);
1982
+ border-top: 1px solid var(--border);
1983
+ font-size: 0.8125rem;
1984
+ font-weight: 400;
1985
+ line-height: 1.5;
1986
+ }
1987
+
1988
+ #timeline-note[hidden] {
1989
+ display: none;
1990
+ }
1991
+
1992
+ .tl-note-where {
1993
+ min-width: 0;
1994
+ min-height: 2rem;
1995
+ display: flex;
1996
+ align-items: center;
1997
+ color: var(--text-muted);
1998
+ overflow-wrap: anywhere;
1999
+ }
2000
+
2001
+ /* The spaces at the start of the position text are kept when the lane name and the position sit side by side. */
2002
+ .tl-note-where > span {
2003
+ white-space: pre-wrap;
2004
+ }
2005
+
2006
+ .tl-lane-name {
2007
+ font-weight: 600;
2008
+ color: var(--text);
2009
+ }
2010
+
2011
+ .tl-note-state {
2012
+ color: var(--text-muted);
2013
+ }
2014
+
2015
+ .tl-note-controls {
2016
+ display: flex;
2017
+ flex-wrap: wrap;
2018
+ align-items: center;
2019
+ gap: 0.5rem 1rem;
2020
+ }
2021
+
2022
+ .tl-note-controls[hidden] {
2023
+ display: none;
2024
+ }
2025
+
2026
+ .tl-group {
2027
+ display: flex;
2028
+ align-items: center;
2029
+ gap: 0.25rem;
2030
+ }
2031
+
2032
+ /* A range offers only Ask Claude: the groups and the Make a rest button are hidden, and their display must not undo that. */
2033
+ .tl-group[hidden],
2034
+ .tl-btn[hidden] {
2035
+ display: none;
2036
+ }
2037
+
2038
+ .tl-label {
2039
+ color: var(--text-muted);
2040
+ margin-right: 0.25rem;
2041
+ }
2042
+
2043
+ .tl-value {
2044
+ font-family: var(--font-mono);
2045
+ color: var(--text);
2046
+ margin-right: 0.25rem;
2047
+ }
2048
+
2049
+ .tl-fixed {
2050
+ color: var(--text-muted);
2051
+ margin-right: 0.25rem;
2052
+ }
2053
+
2054
+ .tl-btn {
2055
+ box-sizing: border-box;
2056
+ min-width: 2rem;
2057
+ height: 2rem;
2058
+ padding: 0 0.5rem;
2059
+ background: var(--surface-2);
2060
+ border: 1px solid var(--border);
2061
+ color: var(--text);
2062
+ font-family: inherit;
2063
+ font-size: 0.8125rem;
2064
+ font-weight: 400;
2065
+ line-height: 1.5;
2066
+ cursor: pointer;
2067
+ }
2068
+
2069
+ .tl-btn:hover:not([aria-disabled="true"]) {
2070
+ border-color: var(--text-muted);
2071
+ }
2072
+
2073
+ .tl-btn[aria-disabled="true"] {
2074
+ border-style: dashed;
2075
+ cursor: default;
2076
+ }
2077
+
2078
+ .tl-btn:focus-visible {
2079
+ outline: 1px solid var(--text-muted);
2080
+ outline-offset: 1px;
2081
+ }
2082
+
2083
+ #timeline-note-line,
2084
+ .tl-note-hint {
2085
+ flex: 1 0 100%;
2086
+ margin: 0;
2087
+ min-height: 1.5rem;
2088
+ color: var(--text-muted);
2089
+ overflow-wrap: anywhere;
2090
+ }
2091
+
2092
+ #timeline-note-line[data-kind="refused"],
2093
+ #timeline-note-line[data-kind="result"],
2094
+ #timeline-note-line[data-kind="notice"],
2095
+ #timeline-note-line[data-kind="preview"] {
2096
+ color: var(--text);
2097
+ }
2098
+
2099
+ #workspace[data-layout="b"] #timeline-note-line,
2100
+ #workspace[data-layout="b"] .tl-note-hint {
2101
+ min-height: 4rem;
2102
+ }
2103
+
2104
+ /* In the side panel the buttons wrap into rows when a note is selected; the panel reserves their room, so it keeps one height
2105
+ whether nothing, a note or a drag is shown and the lanes do not move when a note is picked. */
2106
+ #workspace[data-layout="b"] #timeline-note {
2107
+ min-height: 19rem;
2108
+ }
2109
+
2110
+ #timeline-lanes:focus-visible {
2111
+ outline: 1px solid var(--text-muted);
2112
+ outline-offset: -1px;
2113
+ }
2114
+
2115
+ #timeline-lanes[data-dragging] {
2116
+ user-select: none;
2117
+ }
2118
+
2119
+ /* 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
2120
+ editable note is dragged, the plain arrow while a note the page cannot edit is dragged, a cross for a drag across the lanes.
2121
+ The lanes hold the pointer during a drag, so they carry the cursor themselves while one lasts. */
2122
+ #timeline-lanes .lane-events {
2123
+ cursor: crosshair;
2124
+ }
2125
+
2126
+ #timeline-lanes[data-hover="note"] .lane-events {
2127
+ cursor: pointer;
2128
+ }
2129
+
2130
+ #timeline-lanes[data-dragging="pitch"],
2131
+ #timeline-lanes[data-dragging="pitch"] .lane-events {
2132
+ cursor: ns-resize;
2133
+ }
2134
+
2135
+ #timeline-lanes[data-dragging="refused"],
2136
+ #timeline-lanes[data-dragging="refused"] .lane-events {
2137
+ cursor: default;
2138
+ }
2139
+
2140
+ #timeline-lanes[data-dragging="range"],
2141
+ #timeline-lanes[data-dragging="range"] .lane-events {
2142
+ cursor: crosshair;
2143
+ }
2144
+
2145
+ .lane-marks {
2146
+ grid-column: 2;
2147
+ grid-row: 1;
2148
+ position: relative;
2149
+ pointer-events: none;
2150
+ min-width: 0;
2151
+ }
2152
+
2153
+ .tl-ring,
2154
+ .tl-ghost,
2155
+ .tl-same {
2156
+ position: absolute;
2157
+ box-sizing: border-box;
2158
+ min-width: 0.5rem;
2159
+ outline-offset: 0.25rem;
2160
+ }
2161
+
2162
+ .tl-ring {
2163
+ outline: 1px solid var(--text);
2164
+ }
2165
+
2166
+ .tl-ghost {
2167
+ outline: 1px dashed var(--text);
2168
+ }
2169
+
2170
+ .tl-same {
2171
+ outline: 1px dotted var(--text-muted);
2172
+ }
2173
+
2174
+ /* The element the lanes name with aria-activedescendant: it carries the name of the active note or lane for screen readers and
2175
+ paints nothing. It is not hidden from them, so it is clipped to nothing instead of taking display: none. */
2176
+ .tl-active {
2177
+ position: absolute;
2178
+ width: 1px;
2179
+ height: 1px;
2180
+ overflow: hidden;
2181
+ clip-path: inset(50%);
2182
+ pointer-events: none;
2183
+ }
2184
+
2185
+ .tl-range {
2186
+ position: absolute;
2187
+ top: 0;
2188
+ bottom: 0;
2189
+ box-sizing: border-box;
2190
+ background: color-mix(in srgb, var(--text) 14%, transparent);
2191
+ border-left: 1px solid var(--text-muted);
2192
+ border-right: 1px solid var(--text-muted);
2193
+ }
2194
+
2195
+ /* The arrange() sections above the lanes — same grid columns as .lane, so
2196
+ a section span lines up with the ruler/lane track column beneath it. No accent is used here
2197
+ (accent stays reserved for "live now"/"structural" states, never a static
2198
+ section label). */
2199
+ .section-row {
2200
+ display: grid;
2201
+ grid-template-columns: 8.5em minmax(0, 1fr);
2202
+ height: 1.5em;
2203
+ }
2204
+
2205
+ .section-row-gutter {
1706
2206
  display: flex;
1707
2207
  align-items: center;
1708
2208
  min-width: 0;
@@ -2289,6 +2789,7 @@ body {
2289
2789
  #parts-view {
2290
2790
  flex: 1;
2291
2791
  min-height: 0;
2792
+ min-width: 0;
2292
2793
  display: flex;
2293
2794
  flex-direction: column;
2294
2795
  padding: 1rem;
@@ -2414,29 +2915,384 @@ body {
2414
2915
  border-color: var(--accent);
2415
2916
  }
2416
2917
 
2417
- .part-card[data-state="queued"] {
2418
- border-color: var(--accent-2);
2918
+ .part-card[data-state="queued"] {
2919
+ border-color: var(--accent-2);
2920
+ }
2921
+
2922
+ .part-card[data-state="queued"] .part-state {
2923
+ color: var(--accent-2);
2924
+ }
2925
+
2926
+ .part-progress {
2927
+ display: block;
2928
+ height: 0.25rem;
2929
+ background: var(--border);
2930
+ overflow: hidden;
2931
+ }
2932
+
2933
+ .part-progress-fill {
2934
+ display: block;
2935
+ height: 100%;
2936
+ width: 0;
2937
+ }
2938
+
2939
+ .part-card[data-state="playing"] .part-progress-fill {
2940
+ background: var(--accent);
2941
+ }
2942
+
2943
+ /* The lines above the Song grid in the Parts view. #song-notes sits between the message and the grid so its lines do not
2944
+ scroll away with a long grid. No accent, no motion. */
2945
+ #song-notes {
2946
+ display: flex;
2947
+ flex-direction: column;
2948
+ gap: 0.5rem;
2949
+ padding-bottom: 0.5rem;
2950
+ border-bottom: 1px solid var(--border);
2951
+ }
2952
+
2953
+ #song-notes[hidden] {
2954
+ display: none;
2955
+ }
2956
+
2957
+ .song-note {
2958
+ margin: 0;
2959
+ font-size: 1rem;
2960
+ font-weight: 400;
2961
+ line-height: 1.5;
2962
+ color: var(--text);
2963
+ overflow-wrap: anywhere;
2964
+ }
2965
+
2966
+ .song-note[data-kind="check-failed"] {
2967
+ color: var(--text-muted);
2968
+ }
2969
+
2970
+ /* The toolbar above the Song grid: what a launched section waits for, and where the music is. #song-launch shares the
2971
+ segmented group look of the top bar (the selector lists near #view-buttons). No accent, no motion. */
2972
+ #song-bar {
2973
+ display: flex;
2974
+ flex-wrap: wrap;
2975
+ align-items: center;
2976
+ column-gap: 1rem;
2977
+ row-gap: 0.5rem;
2978
+ }
2979
+
2980
+ #song-bar[hidden] {
2981
+ display: none;
2982
+ }
2983
+
2984
+ #song-launch-label,
2985
+ #song-now {
2986
+ margin: 0;
2987
+ font-size: 0.8125rem;
2988
+ font-weight: 400;
2989
+ line-height: 1.5;
2990
+ color: var(--text-muted);
2991
+ }
2992
+
2993
+ #song-now[hidden] {
2994
+ display: none;
2995
+ }
2996
+
2997
+ /* The one sentence under the toolbar: why the grid is off, what waits, or why a gesture changed nothing. It is the only
2998
+ live region of the view and changes only on a gesture or a state change. A queued launch and one that needs no code
2999
+ change are information, so they are muted; the rest read in the plain text color. No accent, no motion. */
3000
+ #song-line {
3001
+ margin: 0;
3002
+ font-size: 1rem;
3003
+ font-weight: 400;
3004
+ line-height: 1.5;
3005
+ color: var(--text);
3006
+ overflow-wrap: anywhere;
3007
+ }
3008
+
3009
+ #song-line[data-kind="queued"],
3010
+ #song-line[data-kind="already"] {
3011
+ color: var(--text-muted);
3012
+ }
3013
+
3014
+ #song-line[hidden] {
3015
+ display: none;
3016
+ }
3017
+
3018
+ /* The Song grid: sections as columns, groups and tracks as rows. Only .song-grid-scroll scrolls, in both directions; the
3019
+ name column and the header row stay in place (sticky) and are opaque so scrolled cells slide under them. The section
3020
+ header is the Parts card (.part-card), so the playing and queued colors come from the rules above. Nothing here uses an
3021
+ accent, a destructive or a Jam color, and nothing moves: no transition and no animation. */
3022
+ #parts-rows[data-view="song"] {
3023
+ flex: 1;
3024
+ min-height: 0;
3025
+ min-width: 0;
3026
+ overflow: hidden;
3027
+ }
3028
+
3029
+ .song-grid-scroll {
3030
+ flex: 1;
3031
+ min-width: 0;
3032
+ min-height: 0;
3033
+ overflow: auto;
3034
+ scroll-padding-left: 10rem;
3035
+ scroll-padding-top: 7rem;
3036
+ }
3037
+
3038
+ .song-grid {
3039
+ border-collapse: separate;
3040
+ border-spacing: 0;
3041
+ table-layout: fixed;
3042
+ width: 100%;
3043
+ min-width: calc(10rem + var(--song-columns) * 10rem);
3044
+ background: var(--dominant);
3045
+ }
3046
+
3047
+ .song-col-name {
3048
+ width: 10rem;
3049
+ }
3050
+
3051
+ .song-grid col[data-playing="true"] {
3052
+ background: var(--secondary);
3053
+ }
3054
+
3055
+ .song-grid tr[hidden] {
3056
+ display: none;
3057
+ }
3058
+
3059
+ .song-grid th,
3060
+ .song-grid td {
3061
+ padding: 0;
3062
+ min-width: 0;
3063
+ text-align: left;
3064
+ vertical-align: top;
3065
+ font-size: 0.8125rem;
3066
+ font-weight: 400;
3067
+ line-height: 1.5;
3068
+ color: var(--text);
3069
+ border-right: 1px solid var(--border);
3070
+ border-bottom: 1px solid var(--border);
3071
+ }
3072
+
3073
+ .song-grid thead th {
3074
+ position: sticky;
3075
+ top: 0;
3076
+ z-index: 2;
3077
+ background: var(--dominant);
3078
+ }
3079
+
3080
+ .song-grid .song-colhead {
3081
+ padding: 0.25rem;
3082
+ }
3083
+
3084
+ .song-grid .song-corner {
3085
+ position: sticky;
3086
+ left: 0;
3087
+ z-index: 3;
3088
+ padding: 0.5rem;
3089
+ color: var(--text-muted);
3090
+ }
3091
+
3092
+ .song-grid .song-rowhead {
3093
+ position: sticky;
3094
+ left: 0;
3095
+ z-index: 1;
3096
+ background: var(--dominant);
3097
+ padding: 0.25rem 0.5rem;
3098
+ padding-inline-start: calc(0.5rem + var(--depth, 0) * 1rem);
3099
+ overflow-wrap: anywhere;
3100
+ vertical-align: middle;
3101
+ }
3102
+
3103
+ .song-grid .song-rowhead[data-deep="true"] {
3104
+ padding-inline-start: 4.5rem;
3105
+ }
3106
+
3107
+ .song-group-row .song-rowhead,
3108
+ .song-group-row td {
3109
+ background: var(--secondary);
3110
+ }
3111
+
3112
+ .song-head {
3113
+ width: 100%;
3114
+ box-sizing: border-box;
3115
+ margin: 0;
3116
+ }
3117
+
3118
+ .song-head .part-name {
3119
+ font-weight: 600;
3120
+ }
3121
+
3122
+ .song-head-line {
3123
+ display: flex;
3124
+ flex-wrap: wrap;
3125
+ align-items: baseline;
3126
+ column-gap: 0.5rem;
3127
+ }
3128
+
3129
+ .song-head .song-head-action,
3130
+ .song-head .song-used {
3131
+ font-family: inherit;
3132
+ font-size: 0.8125rem;
3133
+ font-weight: 400;
3134
+ line-height: 1.5;
3135
+ color: var(--text-muted);
3136
+ }
3137
+
3138
+ .song-head .song-harmony {
3139
+ max-height: 3em;
3140
+ overflow: hidden;
3141
+ overflow-wrap: anywhere;
3142
+ font-size: 0.8125rem;
3143
+ font-weight: 400;
3144
+ line-height: 1.5;
3145
+ color: var(--text-muted);
3146
+ }
3147
+
3148
+ .song-fold {
3149
+ display: inline-flex;
3150
+ align-items: baseline;
3151
+ gap: 0.5rem;
3152
+ max-width: 100%;
3153
+ margin: 0;
3154
+ padding: 0;
3155
+ background: transparent;
3156
+ border: none;
3157
+ color: var(--text);
3158
+ font-family: inherit;
3159
+ font-size: 0.8125rem;
3160
+ font-weight: 600;
3161
+ line-height: 1.5;
3162
+ text-align: left;
3163
+ cursor: pointer;
3164
+ overflow-wrap: anywhere;
3165
+ }
3166
+
3167
+ .song-fold:focus-visible {
3168
+ outline: 1px solid var(--text-muted);
3169
+ outline-offset: 0.25rem;
3170
+ }
3171
+
3172
+ .song-fold-name {
3173
+ min-width: 0;
3174
+ }
3175
+
3176
+ .song-count {
3177
+ font-weight: 400;
3178
+ color: var(--text-muted);
3179
+ }
3180
+
3181
+ .song-cell {
3182
+ display: flex;
3183
+ flex-direction: column;
3184
+ justify-content: center;
3185
+ width: 100%;
3186
+ min-height: 3rem;
3187
+ box-sizing: border-box;
3188
+ margin: 0;
3189
+ padding: 0.25rem 0.5rem;
3190
+ background: transparent;
3191
+ border: 1px solid transparent;
3192
+ color: var(--text);
3193
+ font-family: inherit;
3194
+ font-size: 0.8125rem;
3195
+ font-weight: 400;
3196
+ line-height: 1.5;
3197
+ text-align: left;
3198
+ cursor: pointer;
3199
+ }
3200
+
3201
+ .song-cell:hover:not([aria-disabled="true"]) {
3202
+ border-color: var(--text-muted);
3203
+ }
3204
+
3205
+ .song-cell:focus-visible {
3206
+ outline: 1px solid var(--text-muted);
3207
+ outline-offset: -1px;
3208
+ }
3209
+
3210
+ /* A control the line switched off stays focusable and keeps its text; only the pointer says it does nothing. */
3211
+ .song-cell[aria-disabled="true"],
3212
+ .song-head[aria-disabled="true"] {
3213
+ cursor: default;
3214
+ }
3215
+
3216
+ .song-variation {
3217
+ white-space: nowrap;
3218
+ overflow: hidden;
3219
+ text-overflow: ellipsis;
3220
+ }
3221
+
3222
+ .song-cell[data-kind="rest"] .song-variation,
3223
+ .song-wait,
3224
+ .song-fit {
3225
+ color: var(--text-muted);
3226
+ }
3227
+
3228
+ /* The second line of a cell: whether a fixed variation fits its section's harmony, or that the cell's change waits for its
3229
+ line. Words in the muted colour, two lines at most; the whole text is in the cell's title. */
3230
+ .song-wait,
3231
+ .song-fit {
3232
+ max-height: 3em;
3233
+ overflow: hidden;
3234
+ overflow-wrap: anywhere;
3235
+ }
3236
+
3237
+ .song-cell[aria-expanded="true"] {
3238
+ border-color: var(--text-muted);
3239
+ }
3240
+
3241
+ /* The list that opens on a cell. It is fixed to the window and lives outside the grid's scroller, so the scroller never
3242
+ clips it; the page puts it below the cell or above it and keeps it inside the window. */
3243
+ #song-cell-list {
3244
+ position: fixed;
3245
+ z-index: 5;
3246
+ box-sizing: border-box;
3247
+ min-width: 10rem;
3248
+ max-width: 20rem;
3249
+ max-height: 16rem;
3250
+ overflow-y: auto;
3251
+ margin: 0;
3252
+ padding: 0.25rem;
3253
+ background: var(--secondary);
3254
+ border: 1px solid var(--text-muted);
3255
+ }
3256
+
3257
+ #song-cell-list[hidden] {
3258
+ display: none;
3259
+ }
3260
+
3261
+ .song-option {
3262
+ display: flex;
3263
+ flex-wrap: wrap;
3264
+ align-items: baseline;
3265
+ column-gap: 0.5rem;
3266
+ padding: 0.25rem 0.5rem;
3267
+ font-size: 0.8125rem;
3268
+ line-height: 1.5;
3269
+ color: var(--text);
3270
+ cursor: pointer;
3271
+ overflow-wrap: anywhere;
3272
+ }
3273
+
3274
+ .song-option[data-active="true"] {
3275
+ box-shadow: inset 0 0 0 1px var(--text-muted);
2419
3276
  }
2420
3277
 
2421
- .part-card[data-state="queued"] .part-state {
2422
- color: var(--accent-2);
3278
+ .song-option-note {
3279
+ color: var(--text-muted);
2423
3280
  }
2424
3281
 
2425
- .part-progress {
2426
- display: block;
2427
- height: 0.25rem;
2428
- background: var(--border);
2429
- overflow: hidden;
3282
+ .song-list-note {
3283
+ padding: 0.25rem 0.5rem;
3284
+ font-size: 0.8125rem;
3285
+ line-height: 1.5;
3286
+ overflow-wrap: anywhere;
2430
3287
  }
2431
3288
 
2432
- .part-progress-fill {
2433
- display: block;
2434
- height: 100%;
2435
- width: 0;
3289
+ .song-option-rest {
3290
+ border-top: 1px solid var(--border);
2436
3291
  }
2437
3292
 
2438
- .part-card[data-state="playing"] .part-progress-fill {
2439
- background: var(--accent);
3293
+ .song-grid .song-outside-note {
3294
+ padding: 0.25rem 0.5rem;
3295
+ color: var(--text-muted);
2440
3296
  }
2441
3297
 
2442
3298
  /* Strudel inserts the CodeMirror container as the NEXT SIBLING of <strudel-editor>,
@@ -2603,6 +3459,196 @@ body {
2603
3459
  .mx-strip[data-color="7"] { --strip-color: var(--track-7); }
2604
3460
  .mx-strip[data-color="8"] { --strip-color: var(--track-8); }
2605
3461
 
3462
+ /* 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
3463
+ stays at the left of the strips area while a wide group scrolls; from the fifth level the frame drops its padding, so a
3464
+ deep tree keeps room for names. A folded group shows its head only. */
3465
+ .mx-group {
3466
+ flex: 0 0 auto;
3467
+ align-self: flex-start;
3468
+ min-height: 100%;
3469
+ min-width: 0;
3470
+ box-sizing: border-box;
3471
+ display: flex;
3472
+ flex-direction: column;
3473
+ gap: 0.5rem;
3474
+ padding: 0.5rem;
3475
+ border: 1px solid var(--border);
3476
+ border-top: 0.25rem solid var(--border);
3477
+ }
3478
+
3479
+ .mx-group[data-deep="true"] {
3480
+ padding: 0;
3481
+ gap: 0;
3482
+ }
3483
+
3484
+ .mx-group-head {
3485
+ position: sticky;
3486
+ left: 1rem;
3487
+ align-self: flex-start;
3488
+ box-sizing: border-box;
3489
+ width: 11rem;
3490
+ display: flex;
3491
+ flex-direction: column;
3492
+ gap: 0.25rem;
3493
+ background: var(--secondary);
3494
+ }
3495
+
3496
+ .mx-group-title {
3497
+ display: flex;
3498
+ align-items: center;
3499
+ gap: 0.5rem;
3500
+ min-width: 0;
3501
+ min-height: 1.5rem;
3502
+ }
3503
+
3504
+ .mx-fold {
3505
+ flex: 1 1 auto;
3506
+ min-width: 0;
3507
+ display: flex;
3508
+ align-items: baseline;
3509
+ gap: 0.5rem;
3510
+ min-height: 1.5rem;
3511
+ margin: 0;
3512
+ padding: 0;
3513
+ background: none;
3514
+ border: 0;
3515
+ color: var(--text);
3516
+ font-family: inherit;
3517
+ font-size: 0.8125rem;
3518
+ font-weight: 600;
3519
+ line-height: 1.25;
3520
+ text-align: left;
3521
+ cursor: pointer;
3522
+ }
3523
+
3524
+ .mx-fold-chevron {
3525
+ flex: 0 0 auto;
3526
+ font-weight: 400;
3527
+ color: var(--text-muted);
3528
+ }
3529
+
3530
+ .mx-fold:hover .mx-fold-chevron {
3531
+ color: var(--text);
3532
+ }
3533
+
3534
+ .mx-fold-name {
3535
+ min-width: 0;
3536
+ overflow: hidden;
3537
+ text-overflow: ellipsis;
3538
+ white-space: nowrap;
3539
+ }
3540
+
3541
+ .mx-fold:focus-visible {
3542
+ outline: 1px solid var(--text-muted);
3543
+ outline-offset: -1px;
3544
+ }
3545
+
3546
+ .mx-group-line {
3547
+ display: flex;
3548
+ align-items: center;
3549
+ gap: 0.25rem;
3550
+ }
3551
+
3552
+ .mx-group-count {
3553
+ flex: 1 1 auto;
3554
+ min-width: 0;
3555
+ overflow: hidden;
3556
+ text-overflow: ellipsis;
3557
+ white-space: nowrap;
3558
+ font-size: 0.8125rem;
3559
+ font-weight: 400;
3560
+ line-height: 1.25;
3561
+ color: var(--text-muted);
3562
+ }
3563
+
3564
+ /* Some of the group's tracks are muted or soloed: a dashed outline in the text colour (the count line says how many). */
3565
+ .mx-group-head .mx-mute[aria-pressed="mixed"],
3566
+ .mx-group-head .mx-solo[aria-pressed="mixed"] {
3567
+ border-style: dashed;
3568
+ border-color: var(--text);
3569
+ color: var(--text);
3570
+ }
3571
+
3572
+ .mx-group-head [data-writes="true"]:disabled {
3573
+ cursor: default;
3574
+ }
3575
+
3576
+ .mx-group-note {
3577
+ margin: 0;
3578
+ font-size: 0.8125rem;
3579
+ font-weight: 400;
3580
+ line-height: 1.25;
3581
+ color: var(--text-muted);
3582
+ overflow-wrap: anywhere;
3583
+ }
3584
+
3585
+ .mx-group-note[hidden] {
3586
+ display: none;
3587
+ }
3588
+
3589
+ .mx-group-body {
3590
+ flex: 1 1 auto;
3591
+ min-width: 0;
3592
+ display: flex;
3593
+ align-items: stretch;
3594
+ gap: 0.5rem;
3595
+ padding-top: 0.5rem;
3596
+ border-top: 1px solid var(--border);
3597
+ }
3598
+
3599
+ .mx-group-body[hidden] {
3600
+ display: none;
3601
+ }
3602
+
3603
+ .mx-group-body > .mx-strip,
3604
+ .mx-group-body > .mx-group {
3605
+ align-self: stretch;
3606
+ min-height: 0;
3607
+ }
3608
+
3609
+ /* A track strip's name row: the light, then the name button. The light's box never changes size, so a blink moves nothing.
3610
+ The loop in the page writes the fade as the fill's opacity; nothing here animates. */
3611
+ .mx-name-row {
3612
+ display: flex;
3613
+ align-items: center;
3614
+ gap: 0.5rem;
3615
+ min-height: 1.5rem;
3616
+ min-width: 0;
3617
+ }
3618
+
3619
+ .mx-name-row > button.mx-name {
3620
+ flex: 1 1 auto;
3621
+ width: auto;
3622
+ min-width: 0;
3623
+ }
3624
+
3625
+ .mx-light {
3626
+ flex: 0 0 auto;
3627
+ position: relative;
3628
+ box-sizing: border-box;
3629
+ width: 0.75rem;
3630
+ height: 0.75rem;
3631
+ border: 1px solid var(--text-muted);
3632
+ border-radius: 50%;
3633
+ }
3634
+
3635
+ .mx-light[hidden] {
3636
+ display: none;
3637
+ }
3638
+
3639
+ .mx-light-fill {
3640
+ position: absolute;
3641
+ inset: 0;
3642
+ border-radius: 50%;
3643
+ background: var(--text);
3644
+ opacity: 0;
3645
+ }
3646
+
3647
+ /* The steady state of motion off only: lit while the track played within the last cycle. */
3648
+ .mx-light[data-lit="true"] .mx-light-fill {
3649
+ opacity: 1;
3650
+ }
3651
+
2606
3652
  .mx-name {
2607
3653
  font-size: 0.8125rem;
2608
3654
  font-weight: 600;
@@ -3751,6 +4797,100 @@ button.mx-name:focus-visible {
3751
4797
  text-overflow: ellipsis;
3752
4798
  }
3753
4799
 
4800
+ /* The line that says why a scale is in use when several fit: a bold lead word and one plain sentence under the scale
4801
+ name. It is no warning, so it wears the text colour only, and it is hidden for a scale that was simply heard. */
4802
+ #jam-reason {
4803
+ margin: 0;
4804
+ font-size: 1rem;
4805
+ font-weight: 400;
4806
+ line-height: 1.5;
4807
+ color: var(--text);
4808
+ overflow-wrap: anywhere;
4809
+ }
4810
+
4811
+ #jam-reason[hidden] {
4812
+ display: none;
4813
+ }
4814
+
4815
+ #jam-reason-lead {
4816
+ font-weight: 600;
4817
+ }
4818
+
4819
+ /* The scales that fit, under the reason line: a quiet caption, then one item per scale. While one can be chosen each item is a
4820
+ button and the one in use is pressed (a lighter fill and an underline, no weight change, so nothing shifts); when none can
4821
+ be chosen the items are plain text with no hover and no pointer. Never cut: an item keeps its whole name and the row wraps
4822
+ between items. Text colours only, no Jam hue: a list of scales is no warning. */
4823
+ #jam-candidates {
4824
+ display: flex;
4825
+ flex-direction: column;
4826
+ gap: 0.25rem;
4827
+ }
4828
+
4829
+ #jam-candidates[hidden] {
4830
+ display: none;
4831
+ }
4832
+
4833
+ #jam-candidates-label,
4834
+ #jam-candidates-more {
4835
+ font-size: 0.8125rem;
4836
+ font-weight: 400;
4837
+ line-height: 1.5;
4838
+ color: var(--text-muted);
4839
+ }
4840
+
4841
+ #jam-candidates-more[hidden] {
4842
+ display: none;
4843
+ }
4844
+
4845
+ #jam-candidate-list {
4846
+ display: flex;
4847
+ flex-direction: row;
4848
+ flex-wrap: wrap;
4849
+ gap: 0.5rem;
4850
+ }
4851
+
4852
+ .jam-candidate {
4853
+ box-sizing: border-box;
4854
+ min-height: 1.75rem;
4855
+ padding: 0 0.5rem;
4856
+ display: inline-flex;
4857
+ align-items: center;
4858
+ border: 1px solid var(--border);
4859
+ border-radius: 0;
4860
+ background: transparent;
4861
+ color: var(--text-muted);
4862
+ font: inherit;
4863
+ font-size: 0.8125rem;
4864
+ font-weight: 400;
4865
+ line-height: 1.5;
4866
+ white-space: nowrap;
4867
+ }
4868
+
4869
+ button.jam-candidate {
4870
+ cursor: pointer;
4871
+ }
4872
+
4873
+ button.jam-candidate:hover {
4874
+ color: var(--text);
4875
+ border-color: var(--text-muted);
4876
+ }
4877
+
4878
+ button.jam-candidate[aria-pressed="true"] {
4879
+ color: var(--text);
4880
+ background: var(--surface-2);
4881
+ border-color: var(--text-muted);
4882
+ box-shadow: inset 0 -0.125rem 0 var(--text);
4883
+ }
4884
+
4885
+ button.jam-candidate:focus-visible {
4886
+ outline: 0.125rem solid var(--text);
4887
+ outline-offset: 0.25rem;
4888
+ }
4889
+
4890
+ span.jam-candidate {
4891
+ color: var(--text);
4892
+ }
4893
+
3754
4894
  /* The declared harmony: a quiet line above the scale block and the label before the scale name.
3755
4895
  Both are hidden without a declaration. Two plain facts: no accent, no mark of agreement or difference. */
3756
4896
  #jam-declared {
@@ -3794,6 +4934,19 @@ button.mx-name:focus-visible {
3794
4934
  color: var(--text-muted);
3795
4935
  }
3796
4936
 
4937
+ /* The section whose harmony the line shows, after the value: shown only for a piece with sections. */
4938
+ #jam-declared-section {
4939
+ flex: none;
4940
+ font-size: 0.8125rem;
4941
+ font-weight: 400;
4942
+ line-height: 1.5;
4943
+ color: var(--text-muted);
4944
+ }
4945
+
4946
+ #jam-declared-section[hidden] {
4947
+ display: none;
4948
+ }
4949
+
3797
4950
  #jam-degrees {
3798
4951
  display: flex;
3799
4952
  flex-direction: row;