moontide-mcp 2.1.0 → 2.4.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 +54 -331
  2. package/dist/ask.js +59 -12
  3. package/dist/catalog.js +118 -9
  4. package/dist/instruments.js +83 -9
  5. package/dist/mcp.js +146 -20
  6. package/dist/midi-state.js +43 -1
  7. package/dist/protocol.js +123 -5
  8. package/dist/reference.js +12 -0
  9. package/dist/request-status.js +221 -0
  10. package/dist/session.js +203 -7
  11. package/dist/vocabulary.js +420 -0
  12. package/package.json +3 -2
  13. package/page/ask-view.js +118 -50
  14. package/page/ask.js +139 -169
  15. package/page/bar-model.js +44 -2
  16. package/page/change-timing.js +35 -0
  17. package/page/index.html +6 -2
  18. package/page/instruments-model.js +50 -3
  19. package/page/instruments-view.js +14 -2
  20. package/page/jam-analysis.js +68 -23
  21. package/page/jam-model.js +185 -29
  22. package/page/jam-resolve.js +271 -0
  23. package/page/jam-view.js +165 -10
  24. package/page/lights.js +29 -7
  25. package/page/main.js +807 -66
  26. package/page/midi-clock.js +22 -3
  27. package/page/midi-in.js +407 -0
  28. package/page/midi-out.js +557 -48
  29. package/page/midi-plan.js +42 -6
  30. package/page/midi-rig.js +286 -13
  31. package/page/midi-settings.js +507 -0
  32. package/page/mixer-view.js +7 -1
  33. package/page/note-panel.js +29 -8
  34. package/page/note-tag.js +19 -4
  35. package/page/page-rewrite.js +1 -1
  36. package/page/played-buffer.js +208 -0
  37. package/page/query-check.js +21 -2
  38. package/page/request-status-view.js +419 -0
  39. package/page/request-status.js +686 -0
  40. package/page/scratch-gesture.js +239 -0
  41. package/page/scratch-listen.js +260 -0
  42. package/page/scratch-model.js +1696 -0
  43. package/page/scratch-view.js +2176 -0
  44. package/page/song-grid-model.js +15 -0
  45. package/page/song-grid-view.js +1 -1
  46. package/page/structure.js +2 -1
  47. package/page/style.css +1243 -43
  48. package/page/tempo-glide.js +330 -0
  49. package/page/tempo-guard.js +62 -10
  50. package/page/timeline-edit-view.js +1 -1
  51. package/page/timeline.js +2 -1
  52. package/page/top-bar-view.js +18 -13
  53. package/page/views.js +20 -7
  54. package/reference/midi.md +2 -0
  55. package/reference/motif.md +83 -0
  56. package/reference/settings.md +161 -0
  57. package/reference/transitions.md +13 -1
package/page/style.css CHANGED
@@ -456,34 +456,6 @@ body {
456
456
  font-size: 0.8125rem;
457
457
  }
458
458
 
459
- /* The status of the latest request to Claude. No display rule: #top-bar > span styles it and the browser's [hidden]
460
- works unaided, so nothing to say takes no width. It sits before the Session button in the markup, so it is right of
461
- the Layout group. It holds the long sentence and a short form: the short one is for fit step 3 and is aria-hidden. */
462
- #ask-status {
463
- flex: none;
464
- }
465
-
466
- #ask-status .ask-short {
467
- display: none;
468
- }
469
-
470
- /* Fit step 3 and 4: the short form shows, and the long sentence is visually hidden but kept in the accessibility tree
471
- (never display: none), the recipe of #change-status. */
472
- #top-bar[data-fit="3"] #ask-status .ask-short,
473
- #top-bar[data-fit="4"] #ask-status .ask-short {
474
- display: inline;
475
- }
476
-
477
- #top-bar[data-fit="3"] #ask-status .ask-long,
478
- #top-bar[data-fit="4"] #ask-status .ask-long {
479
- position: absolute;
480
- width: 1px;
481
- height: 1px;
482
- overflow: hidden;
483
- clip-path: inset(50%);
484
- white-space: nowrap;
485
- }
486
-
487
459
  /* The cycle and beat beside the tempo, quiet decoration (Label size, mono, muted). The hidden attribute
488
460
  hides it while nothing plays, so it has no display rule. Fit step 1 drops it with the wordmark (.bar-deco). */
489
461
  #top-bar #cycle-beat {
@@ -494,9 +466,7 @@ body {
494
466
  }
495
467
 
496
468
  /* Whether page requests reach Claude live. No display rule: #top-bar > span
497
- styles it and the browser's [hidden] works unaided. After #ask-status in the markup, same order,
498
- so it sits right of the request status and left of the motion switch, and the request status
499
- appearing never moves it. */
469
+ styles it and the browser's [hidden] works unaided. */
500
470
  #page-requests {
501
471
  flex-shrink: 0;
502
472
  }
@@ -1015,6 +985,300 @@ body {
1015
985
  font-weight: 400;
1016
986
  }
1017
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
+
1018
1282
  /* The strip: one muted line under the bar, only while the drawer is closed and a condition holds. */
1019
1283
  #session-strip {
1020
1284
  display: flex;
@@ -4533,19 +4797,113 @@ button.mx-name:focus-visible {
4533
4797
  text-overflow: ellipsis;
4534
4798
  }
4535
4799
 
4536
- /* The declared harmony: a quiet line above the scale block and the label before the scale name.
4537
- Both are hidden without a declaration. Two plain facts: no accent, no mark of agreement or difference. */
4538
- #jam-declared {
4539
- display: flex;
4540
- flex-direction: row;
4541
- flex-wrap: wrap;
4542
- align-items: baseline;
4543
- column-gap: 0.5rem;
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 {
4544
4803
  margin: 0;
4545
- }
4546
-
4547
- #jam-declared[hidden] {
4548
- display: none;
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
+
4894
+ /* The declared harmony: a quiet line above the scale block and the label before the scale name.
4895
+ Both are hidden without a declaration. Two plain facts: no accent, no mark of agreement or difference. */
4896
+ #jam-declared {
4897
+ display: flex;
4898
+ flex-direction: row;
4899
+ flex-wrap: wrap;
4900
+ align-items: baseline;
4901
+ column-gap: 0.5rem;
4902
+ margin: 0;
4903
+ }
4904
+
4905
+ #jam-declared[hidden] {
4906
+ display: none;
4549
4907
  }
4550
4908
 
4551
4909
  #jam-declared-label,
@@ -5942,6 +6300,848 @@ button.mx-name:focus-visible {
5942
6300
  white-space: nowrap;
5943
6301
  }
5944
6302
 
6303
+ /* The Scratchpad view. #scratch-view fills the panel like the Jam view and scrolls on its own. The roll, its ruler and the
6304
+ piano names share one box that scrolls sideways, so their columns stay aligned. The lines are painted with gradients (cycle,
6305
+ beat, grid step and an octave line above each C row), the notes and the cursor are absolutely placed elements that read their
6306
+ place from the custom properties the view sets. Neutral colors only: the accent is for "live now" and red is for the failed-change
6307
+ banner. Sizes in rem, 1px hairlines only, no motion. */
6308
+ #scratch-view {
6309
+ /* Before the view is first sized, and when it has no size to read: the smallest beat and a middle row. The view's sizing sets both
6310
+ again on #scratch-scroll, which the ruler and the roll inherit. */
6311
+ --sp-beat-w: 4rem;
6312
+ --sp-row-h: 0.875rem;
6313
+ /* The rows the roll shows: the view sets the real count on #scratch-roll from the notes, a whole number of octaves (24 to 108). */
6314
+ --sp-rows: 24;
6315
+ --sp-line-cycle: color-mix(in srgb, var(--text-muted) 70%, var(--dominant));
6316
+ --sp-line-beat: color-mix(in srgb, var(--text-muted) 35%, var(--border));
6317
+ --sp-line-step: color-mix(in srgb, var(--border) 60%, transparent);
6318
+ --sp-line-octave: color-mix(in srgb, var(--text-muted) 50%, var(--dominant));
6319
+ --sp-row-natural: color-mix(in srgb, var(--surface-2) 55%, var(--dominant));
6320
+ --sp-row-black: var(--dominant);
6321
+ /* The height of the listening strip: one line, two in layout b. The information line above it is pinned just over it. */
6322
+ --sp-listen-h: 2rem;
6323
+ flex: 1;
6324
+ min-height: 0;
6325
+ overflow: auto;
6326
+ display: flex;
6327
+ flex-direction: column;
6328
+ padding: 0.5rem 1rem;
6329
+ gap: 0.5rem;
6330
+ }
6331
+
6332
+ #scratch-view[hidden] {
6333
+ display: none;
6334
+ }
6335
+
6336
+ #workspace[data-layout="b"] #scratch-view {
6337
+ --sp-listen-h: 4rem;
6338
+ padding: 1rem;
6339
+ }
6340
+
6341
+ #workspace[data-layout="c"] #scratch-view {
6342
+ padding: 1.5rem;
6343
+ }
6344
+
6345
+ #workspace[data-layout="b"] #scratch-scroll,
6346
+ #workspace[data-layout="c"] #scratch-scroll {
6347
+ min-height: 23.5rem;
6348
+ }
6349
+
6350
+ /* The toolbar: the grid, the length of the motif and the pitch window. The buttons are the page's .tl-btn; the grid group takes the
6351
+ segmented look of the Jam instrument switch (a frame, the pressed segment on the raised fill, no weight change). A button that has
6352
+ reached its end keeps focus and shows it with a muted word and a dashed frame. */
6353
+ #scratch-controls {
6354
+ flex: 0 0 auto;
6355
+ display: flex;
6356
+ flex-wrap: wrap;
6357
+ align-items: center;
6358
+ gap: 0.5rem 1rem;
6359
+ }
6360
+
6361
+ #scratch-grids,
6362
+ #scratch-length,
6363
+ #scratch-pitch,
6364
+ #scratch-mode,
6365
+ #scratch-edit,
6366
+ #scratch-history,
6367
+ #scratch-record-group {
6368
+ display: flex;
6369
+ align-items: center;
6370
+ gap: 0.25rem;
6371
+ }
6372
+
6373
+ /* The Early experimental label: the last item of the toolbar, plain text at its right end, no control and no tab stop. */
6374
+ #scratch-early {
6375
+ margin-left: auto;
6376
+ padding: 0.125rem 0.5rem;
6377
+ border: 1px solid var(--border);
6378
+ color: var(--text-muted);
6379
+ font-size: 0.8125rem;
6380
+ line-height: 1.25rem;
6381
+ white-space: nowrap;
6382
+ }
6383
+
6384
+ #scratch-grids {
6385
+ gap: 0;
6386
+ border: 1px solid var(--border);
6387
+ }
6388
+
6389
+ #scratch-grids .tl-btn {
6390
+ background: transparent;
6391
+ border: none;
6392
+ color: var(--text-muted);
6393
+ }
6394
+
6395
+ #scratch-grids .tl-btn:hover:not([aria-disabled="true"]) {
6396
+ color: var(--text);
6397
+ }
6398
+
6399
+ #scratch-grids .tl-btn[aria-pressed="true"] {
6400
+ color: var(--text);
6401
+ background: var(--surface-2);
6402
+ }
6403
+
6404
+ #scratch-grids .tl-btn:focus-visible {
6405
+ outline-offset: -1px;
6406
+ }
6407
+
6408
+ #scratch-controls .tl-btn[aria-disabled="true"] {
6409
+ color: var(--text-muted);
6410
+ }
6411
+
6412
+ /* The Draw and Tap rhythm toggles read pressed by the text color on their frame (the frame of the other buttons is the faint border). */
6413
+ #scratch-draw[aria-pressed="true"],
6414
+ #scratch-tap[aria-pressed="true"] {
6415
+ border-color: var(--text);
6416
+ }
6417
+
6418
+ .sp-readout {
6419
+ box-sizing: border-box;
6420
+ min-width: 4.5rem;
6421
+ padding: 0 0.5rem;
6422
+ color: var(--text);
6423
+ font-family: var(--font-mono);
6424
+ font-size: 0.8125rem;
6425
+ font-weight: 400;
6426
+ line-height: 1.5;
6427
+ text-align: center;
6428
+ white-space: nowrap;
6429
+ }
6430
+
6431
+ /* The scroll box takes the room the view has (its height is never taken from its content, so the rows can follow it) and never less
6432
+ than 22rem. It scrolls both ways: sideways when the beats at their smallest do not fit, and up and down when the rows at their
6433
+ smallest height do not fit (a motif of more than three octaves in a short strip). The ruler is held at the top of the box, the piano
6434
+ names at its left and the velocity lane at its bottom, so only the roll moves. A shorter strip scrolls the view. */
6435
+ #scratch-scroll {
6436
+ flex: 1 1 0%;
6437
+ min-height: 22rem;
6438
+ box-sizing: border-box;
6439
+ border: 1px solid var(--border);
6440
+ background: var(--dominant);
6441
+ /* No padding at the left, the top or the bottom: the piano names, the ruler and the lane stick to those edges of the box, and a padding
6442
+ there would show a sliver of the scrolled roll beside them. The ruler and the lane carry the air themselves. */
6443
+ padding: 0 0.25rem 0 0;
6444
+ overflow: auto;
6445
+ /* The room for a scrollbar is kept whether or not the rows need one, so the beat width worked out from the box does not change when
6446
+ the rows start to scroll. */
6447
+ scrollbar-gutter: stable;
6448
+ /* The ruler and the lane hold a z-index of their own over the roll. Kept inside the box, it cannot rise over the information line and the
6449
+ strip that layout a pins over the bottom of the view (they sit lower), which cut the top of the line's text. */
6450
+ isolation: isolate;
6451
+ }
6452
+
6453
+ /* Held at the top of the box while the roll scrolls up and down; the bar over the names and the grid hides what passes under it. */
6454
+ #scratch-ruler {
6455
+ position: sticky;
6456
+ top: 0;
6457
+ z-index: 3;
6458
+ box-sizing: border-box;
6459
+ display: flex;
6460
+ width: max-content;
6461
+ min-width: 100%;
6462
+ height: 1.5rem;
6463
+ padding-top: 0.25rem;
6464
+ background: var(--dominant);
6465
+ user-select: none;
6466
+ }
6467
+
6468
+ /* The width of the piano names stays at the left while the ruler scrolls, so a label never slides over the names (the column is as wide as
6469
+ the one below, which the fit counts). */
6470
+ #scratch-ruler::before {
6471
+ content: "";
6472
+ position: sticky;
6473
+ left: 0;
6474
+ z-index: 2;
6475
+ flex: none;
6476
+ width: 4.5rem;
6477
+ background: var(--dominant);
6478
+ }
6479
+
6480
+ .sp-beat {
6481
+ flex: none;
6482
+ box-sizing: border-box;
6483
+ width: var(--sp-beat-w);
6484
+ padding-left: 0.25rem;
6485
+ color: var(--text-muted);
6486
+ font-family: var(--font-mono);
6487
+ font-size: 0.8125rem;
6488
+ font-weight: 400;
6489
+ line-height: 1.25rem;
6490
+ white-space: nowrap;
6491
+ }
6492
+
6493
+ .sp-beat-first {
6494
+ color: var(--text);
6495
+ }
6496
+
6497
+ #scratch-roll {
6498
+ display: flex;
6499
+ width: max-content;
6500
+ user-select: none;
6501
+ }
6502
+
6503
+ /* The ring sits just outside the roll: the names and the grid are positioned and would paint over a ring drawn inside it. */
6504
+ #scratch-roll:focus-visible {
6505
+ outline: 1px solid var(--text-muted);
6506
+ outline-offset: 1px;
6507
+ }
6508
+
6509
+ #scratch-gutter {
6510
+ position: sticky;
6511
+ left: 0;
6512
+ z-index: 2;
6513
+ flex: none;
6514
+ width: 4.5rem;
6515
+ background: var(--dominant);
6516
+ }
6517
+
6518
+ .sp-key {
6519
+ box-sizing: border-box;
6520
+ height: var(--sp-row-h);
6521
+ display: flex;
6522
+ align-items: center;
6523
+ justify-content: flex-end;
6524
+ padding-right: 0.25rem;
6525
+ overflow: hidden;
6526
+ background: var(--surface-2);
6527
+ color: var(--text-muted);
6528
+ font-family: var(--font-mono);
6529
+ font-size: 0.8125rem;
6530
+ font-weight: 400;
6531
+ line-height: 1;
6532
+ white-space: nowrap;
6533
+ }
6534
+
6535
+ .sp-key[data-black="true"] {
6536
+ background: var(--dominant);
6537
+ }
6538
+
6539
+ .sp-key[data-cursor="true"] {
6540
+ color: var(--text);
6541
+ }
6542
+
6543
+ /* A key held now: a bar of the accent at the left and its name in the text color, heavier. */
6544
+ .sp-key[data-held="true"] {
6545
+ border-left: 0.25rem solid var(--accent);
6546
+ color: var(--text);
6547
+ font-weight: 600;
6548
+ }
6549
+
6550
+ #scratch-grid {
6551
+ position: relative;
6552
+ flex: none;
6553
+ width: calc(var(--sp-cycles) * 4 * var(--sp-beat-w));
6554
+ height: calc(var(--sp-rows) * var(--sp-row-h));
6555
+ overflow: hidden;
6556
+ user-select: none;
6557
+ touch-action: none;
6558
+ cursor: default;
6559
+ background-color: var(--sp-row-black);
6560
+ background-image:
6561
+ repeating-linear-gradient(to right, var(--sp-line-cycle) 0, var(--sp-line-cycle) 1px, transparent 1px, transparent calc(var(--sp-beat-w) * 4)),
6562
+ repeating-linear-gradient(to right, var(--sp-line-beat) 0, var(--sp-line-beat) 1px, transparent 1px, transparent var(--sp-beat-w)),
6563
+ repeating-linear-gradient(to right, var(--sp-line-step) 0, var(--sp-line-step) 1px, transparent 1px, transparent calc(var(--sp-beat-w) * 4 / var(--sp-steps))),
6564
+ linear-gradient(to bottom, var(--sp-line-octave) 0, var(--sp-line-octave) 1px, transparent 1px),
6565
+ linear-gradient(
6566
+ to bottom,
6567
+ var(--sp-row-natural) 0,
6568
+ var(--sp-row-natural) var(--sp-row-h),
6569
+ var(--sp-row-black) var(--sp-row-h),
6570
+ var(--sp-row-black) calc(var(--sp-row-h) * 2),
6571
+ var(--sp-row-natural) calc(var(--sp-row-h) * 2),
6572
+ var(--sp-row-natural) calc(var(--sp-row-h) * 3),
6573
+ var(--sp-row-black) calc(var(--sp-row-h) * 3),
6574
+ var(--sp-row-black) calc(var(--sp-row-h) * 4),
6575
+ var(--sp-row-natural) calc(var(--sp-row-h) * 4),
6576
+ var(--sp-row-natural) calc(var(--sp-row-h) * 5),
6577
+ var(--sp-row-black) calc(var(--sp-row-h) * 5),
6578
+ var(--sp-row-black) calc(var(--sp-row-h) * 6),
6579
+ var(--sp-row-natural) calc(var(--sp-row-h) * 6),
6580
+ var(--sp-row-natural) calc(var(--sp-row-h) * 8),
6581
+ var(--sp-row-black) calc(var(--sp-row-h) * 8),
6582
+ var(--sp-row-black) calc(var(--sp-row-h) * 9),
6583
+ var(--sp-row-natural) calc(var(--sp-row-h) * 9),
6584
+ var(--sp-row-natural) calc(var(--sp-row-h) * 10),
6585
+ var(--sp-row-black) calc(var(--sp-row-h) * 10),
6586
+ var(--sp-row-black) calc(var(--sp-row-h) * 11),
6587
+ var(--sp-row-natural) calc(var(--sp-row-h) * 11),
6588
+ var(--sp-row-natural) calc(var(--sp-row-h) * 12)
6589
+ );
6590
+ background-size: auto, auto, auto, 100% calc(var(--sp-row-h) * 12), 100% calc(var(--sp-row-h) * 12);
6591
+ background-position: 0 0, 0 0, 0 0, 0 calc(var(--sp-row-h) * -1), 0 0;
6592
+ }
6593
+
6594
+ /* The pointer is a crosshair on empty cells only while Draw is on: a drag then writes a note, else it draws a selection box. */
6595
+ #scratch-view[data-draw="on"] #scratch-grid {
6596
+ cursor: crosshair;
6597
+ }
6598
+
6599
+ #scratch-notes {
6600
+ position: absolute;
6601
+ inset: 0;
6602
+ }
6603
+
6604
+ .sp-note,
6605
+ .sp-live,
6606
+ #scratch-cursor,
6607
+ #scratch-preview,
6608
+ #scratch-hover-cell {
6609
+ position: absolute;
6610
+ box-sizing: border-box;
6611
+ left: calc(var(--sp-start) / var(--sp-units) * 100% + 0.125rem);
6612
+ width: calc(var(--sp-len) / var(--sp-units) * 100% - 0.25rem);
6613
+ top: calc(var(--sp-row) * var(--sp-row-h) + 0.125rem);
6614
+ height: calc(var(--sp-row-h) - 0.25rem);
6615
+ }
6616
+
6617
+ .sp-note {
6618
+ background: color-mix(in srgb, var(--text) calc(30% + 60% * var(--v)), var(--dominant));
6619
+ border: 1px solid var(--text-muted);
6620
+ }
6621
+
6622
+ .sp-note {
6623
+ cursor: grab;
6624
+ }
6625
+
6626
+ /* The live layer: the keyboard buffer's notes, drawn over the motif in the accent ("live now", as the Timeline playhead is), translucent so a
6627
+ motif note under them stays visible. A note still held has no right border: its right edge is open and follows the playhead. The layer
6628
+ and its notes take no pointer event, so the motif underneath stays editable. */
6629
+ #scratch-live {
6630
+ position: absolute;
6631
+ inset: 0;
6632
+ pointer-events: none;
6633
+ }
6634
+
6635
+ .sp-live {
6636
+ width: max(0.25rem, calc(var(--sp-len) / var(--sp-units) * 100% - 0.25rem));
6637
+ background: color-mix(in srgb, var(--accent) calc(15% + 25% * var(--v)), transparent);
6638
+ border: 1px solid var(--accent);
6639
+ pointer-events: none;
6640
+ }
6641
+
6642
+ .sp-live[data-held="true"] {
6643
+ border-right-width: 0;
6644
+ }
6645
+
6646
+ /* The handle at a note's right side: half a rem wide, at most 40 percent of a short note. It only changes the cursor; the press is
6647
+ told apart from the numbers of the pointer in page/scratch-gesture.js. */
6648
+ .sp-edge {
6649
+ position: absolute;
6650
+ top: 0;
6651
+ right: 0;
6652
+ bottom: 0;
6653
+ width: min(0.5rem, 40%);
6654
+ cursor: ew-resize;
6655
+ }
6656
+
6657
+ .sp-hover {
6658
+ outline: 1px solid var(--text);
6659
+ }
6660
+
6661
+ .sp-selected {
6662
+ outline: 0.125rem solid var(--text);
6663
+ }
6664
+
6665
+ /* A drag of several selected notes draws each of them where the drop would put it, in the dashed ring of a preview. */
6666
+ .sp-draft {
6667
+ outline-style: dashed;
6668
+ }
6669
+
6670
+ /* A draw, move or resize in progress: the dashed ring of the cursor, over the notes. The hovered empty cell is a thin outline, so the
6671
+ two are never confused. Neither takes a pointer event: the grid does. */
6672
+ #scratch-preview {
6673
+ border: 0.125rem dashed var(--text);
6674
+ pointer-events: none;
6675
+ }
6676
+
6677
+ #scratch-preview[hidden] {
6678
+ display: none;
6679
+ }
6680
+
6681
+ #scratch-hover-cell {
6682
+ outline: 1px solid var(--text-muted);
6683
+ pointer-events: none;
6684
+ }
6685
+
6686
+ #scratch-hover-cell[hidden] {
6687
+ display: none;
6688
+ }
6689
+
6690
+ /* The selection box of a drag in Select mode: a thin dashed outline and a faint fill, placed by the fractions of units and rows the
6691
+ drag spans. It takes no pointer event: the grid does. */
6692
+ #scratch-select-rect {
6693
+ position: absolute;
6694
+ box-sizing: border-box;
6695
+ left: calc(var(--sp-start) / var(--sp-units) * 100%);
6696
+ width: calc(var(--sp-len) / var(--sp-units) * 100%);
6697
+ top: calc(var(--sp-row) * var(--sp-row-h));
6698
+ height: calc(var(--sp-rows-span) * var(--sp-row-h));
6699
+ border: 1px dashed var(--text);
6700
+ background: color-mix(in srgb, var(--text) 8%, transparent);
6701
+ pointer-events: none;
6702
+ }
6703
+
6704
+ #scratch-select-rect[hidden] {
6705
+ display: none;
6706
+ }
6707
+
6708
+ #scratch-cursor {
6709
+ border: 0.125rem dashed var(--text);
6710
+ pointer-events: none;
6711
+ }
6712
+
6713
+ #scratch-cursor[hidden] {
6714
+ display: none;
6715
+ }
6716
+
6717
+ /* Tap's now-marker: a 1px line of the text color where the music is in the motif. It moves because the music moves, so it moves in
6718
+ both motion modes and has no transition. It takes no pointer event. */
6719
+ #scratch-now {
6720
+ position: absolute;
6721
+ top: 0;
6722
+ bottom: 0;
6723
+ left: calc(var(--sp-now) / var(--sp-units) * 100%);
6724
+ width: 1px;
6725
+ background: var(--text);
6726
+ pointer-events: none;
6727
+ }
6728
+
6729
+ #scratch-now[hidden] {
6730
+ display: none;
6731
+ }
6732
+
6733
+ /* The live playhead: where the cycle axis is on the ring of 8 cycles. Like the marker above it is a position readout, so it has no
6734
+ transition and moves in both motion modes. */
6735
+ #scratch-playhead {
6736
+ position: absolute;
6737
+ top: 0;
6738
+ bottom: 0;
6739
+ left: calc(var(--sp-playhead) / var(--sp-units) * 100%);
6740
+ width: 0.125rem;
6741
+ background: var(--accent);
6742
+ pointer-events: none;
6743
+ }
6744
+
6745
+ #scratch-playhead[hidden] {
6746
+ display: none;
6747
+ }
6748
+
6749
+ /* The velocity lane: one bar per note under its start column, sharing the roll's columns inside the same scroll box, so a sideways
6750
+ scroll moves both. Notes that start together sit side by side in their column, each bar at least a quarter rem wide. The bars take
6751
+ no pointer event: the lane's bars area hears the press and finds the column from the pointer's place, so the whole column answers.
6752
+ The lane is held at the bottom of the box while the roll scrolls up and down, with a fill so nothing shows through. */
6753
+ #scratch-lane {
6754
+ position: sticky;
6755
+ bottom: 0;
6756
+ z-index: 3;
6757
+ display: flex;
6758
+ width: max-content;
6759
+ height: 3.5rem;
6760
+ padding-bottom: 0.25rem;
6761
+ background: var(--dominant);
6762
+ user-select: none;
6763
+ }
6764
+
6765
+ #workspace[data-layout="b"] #scratch-lane,
6766
+ #workspace[data-layout="c"] #scratch-lane {
6767
+ height: 5rem;
6768
+ }
6769
+
6770
+ #scratch-lane:focus-visible {
6771
+ outline: 1px solid var(--text-muted);
6772
+ outline-offset: 1px;
6773
+ }
6774
+
6775
+ #scratch-lane-label {
6776
+ position: sticky;
6777
+ left: 0;
6778
+ z-index: 2;
6779
+ flex: none;
6780
+ box-sizing: border-box;
6781
+ width: 4.5rem;
6782
+ padding-right: 0.25rem;
6783
+ background: var(--dominant);
6784
+ color: var(--text-muted);
6785
+ font-family: var(--font-mono);
6786
+ font-size: 0.8125rem;
6787
+ font-weight: 400;
6788
+ line-height: 1.5;
6789
+ text-align: right;
6790
+ }
6791
+
6792
+ #scratch-bars {
6793
+ position: relative;
6794
+ flex: none;
6795
+ box-sizing: border-box;
6796
+ width: calc(var(--sp-cycles) * 4 * var(--sp-beat-w));
6797
+ height: 100%;
6798
+ border-top: 1px solid var(--border);
6799
+ touch-action: none;
6800
+ cursor: ns-resize;
6801
+ background-image:
6802
+ repeating-linear-gradient(to right, var(--sp-line-cycle) 0, var(--sp-line-cycle) 1px, transparent 1px, transparent calc(var(--sp-beat-w) * 4)),
6803
+ repeating-linear-gradient(to right, var(--sp-line-beat) 0, var(--sp-line-beat) 1px, transparent 1px, transparent var(--sp-beat-w));
6804
+ }
6805
+
6806
+ .sp-bar {
6807
+ --sp-slot-w: max(0.375rem, calc(var(--sp-step) / var(--sp-slots) / var(--sp-units) * 100%));
6808
+ position: absolute;
6809
+ bottom: 0;
6810
+ box-sizing: border-box;
6811
+ left: calc(var(--sp-start) / var(--sp-units) * 100% + var(--sp-slot) * var(--sp-slot-w) + 0.0625rem);
6812
+ width: max(0.25rem, calc(var(--sp-slot-w) - 0.125rem));
6813
+ height: calc(var(--v) * 100%);
6814
+ min-height: 1px;
6815
+ background: var(--text-muted);
6816
+ pointer-events: none;
6817
+ }
6818
+
6819
+ .sp-bar.sp-selected {
6820
+ outline: none;
6821
+ background: var(--text);
6822
+ }
6823
+
6824
+ #scratch-note {
6825
+ position: relative;
6826
+ flex: 0 0 auto;
6827
+ box-sizing: border-box;
6828
+ display: flex;
6829
+ flex-wrap: wrap;
6830
+ align-items: center;
6831
+ gap: 0.25rem 1rem;
6832
+ min-height: 2rem;
6833
+ font-size: 0.8125rem;
6834
+ font-weight: 400;
6835
+ line-height: 1.5;
6836
+ }
6837
+
6838
+ #workspace[data-layout="b"] #scratch-note {
6839
+ min-height: 4rem;
6840
+ }
6841
+
6842
+ /* In layout a the panel is a short band under the editor: the roll keeps its minimum height and the view scrolls. The information line and the
6843
+ listening strip stay at the bottom edge of the view, in sight, because the strip is the one place that says why nothing is heard. */
6844
+ #workspace[data-layout="a"] #scratch-note,
6845
+ #workspace[data-layout="a"] #scratch-listen {
6846
+ position: sticky;
6847
+ z-index: 1;
6848
+ background: var(--secondary);
6849
+ }
6850
+
6851
+ #workspace[data-layout="a"] #scratch-note {
6852
+ bottom: calc(var(--sp-listen-h) + 0.5rem);
6853
+ }
6854
+
6855
+ /* Scrolling a control into view with "nearest" (a focused or revealed lane, a key) must stop above the two pinned lines, not under them: the
6856
+ padding is the listening strip, the information line at its smallest height and the gaps between them. */
6857
+ #workspace[data-layout="a"] #scratch-view {
6858
+ scroll-padding-bottom: calc(var(--sp-listen-h) + 3rem);
6859
+ }
6860
+
6861
+ #workspace[data-layout="a"] #scratch-listen {
6862
+ bottom: 0;
6863
+ }
6864
+
6865
+ #scratch-note-line {
6866
+ flex: 1 1 auto;
6867
+ min-width: 0;
6868
+ margin: 0;
6869
+ color: var(--text-muted);
6870
+ overflow-wrap: anywhere;
6871
+ }
6872
+
6873
+ #scratch-note-name {
6874
+ font-weight: 600;
6875
+ color: var(--text);
6876
+ }
6877
+
6878
+ #scratch-note-rest {
6879
+ font-weight: 400;
6880
+ color: var(--text-muted);
6881
+ font-variant-numeric: tabular-nums;
6882
+ }
6883
+
6884
+ #scratch-note-line[data-kind="notice"] #scratch-note-rest {
6885
+ color: var(--text);
6886
+ }
6887
+
6888
+ /* The line of a hovered note or of a draw in progress: it takes the place of the shown line and is never read aloud (the words for a
6889
+ screen reader stay in #scratch-note-spoken, which a hover does not touch). */
6890
+ #scratch-note[data-hover="true"] #scratch-note-shown {
6891
+ visibility: hidden;
6892
+ }
6893
+
6894
+ #scratch-note-hover {
6895
+ position: absolute;
6896
+ left: 0;
6897
+ top: 0;
6898
+ bottom: 0;
6899
+ display: flex;
6900
+ align-items: center;
6901
+ margin: 0;
6902
+ max-width: 100%;
6903
+ color: var(--text-muted);
6904
+ overflow-wrap: anywhere;
6905
+ font-variant-numeric: tabular-nums;
6906
+ }
6907
+
6908
+ #scratch-note-hover[hidden] {
6909
+ display: none;
6910
+ }
6911
+
6912
+ .sp-hover-name {
6913
+ font-weight: 600;
6914
+ color: var(--text);
6915
+ }
6916
+
6917
+ /* Visually hidden, kept in the accessibility tree: the live region reads this full-words form of the line. */
6918
+ #scratch-note-spoken {
6919
+ position: absolute;
6920
+ width: 1px;
6921
+ height: 1px;
6922
+ overflow: hidden;
6923
+ clip-path: inset(50%);
6924
+ white-space: nowrap;
6925
+ }
6926
+
6927
+ #scratch-hint {
6928
+ flex: 0 1 auto;
6929
+ margin: 0;
6930
+ color: var(--text-muted);
6931
+ }
6932
+
6933
+ /* The listening strip under the information line: whether a MIDI keyboard is listened to, and the last note it played. It keeps a fixed
6934
+ height like the information line (one line, two in layout b); a longer sentence ends in an ellipsis and carries its whole text in its
6935
+ title. Only the state sentence is a live region, so a jam never floods a screen reader note by note. */
6936
+ #scratch-listen {
6937
+ flex: 0 0 auto;
6938
+ box-sizing: border-box;
6939
+ display: flex;
6940
+ align-items: center;
6941
+ gap: 0.25rem 1rem;
6942
+ height: var(--sp-listen-h);
6943
+ overflow: hidden;
6944
+ font-size: 0.8125rem;
6945
+ font-weight: 400;
6946
+ line-height: 1.5;
6947
+ }
6948
+
6949
+ #scratch-listen-state,
6950
+ #scratch-heard {
6951
+ margin: 0;
6952
+ min-width: 0;
6953
+ overflow: hidden;
6954
+ text-overflow: ellipsis;
6955
+ white-space: nowrap;
6956
+ }
6957
+
6958
+ #workspace[data-layout="b"] #scratch-listen-state,
6959
+ #workspace[data-layout="b"] #scratch-heard {
6960
+ display: -webkit-box;
6961
+ -webkit-box-orient: vertical;
6962
+ -webkit-line-clamp: 2;
6963
+ white-space: normal;
6964
+ overflow-wrap: anywhere;
6965
+ }
6966
+
6967
+ #scratch-listen-state {
6968
+ flex: 1 1 auto;
6969
+ color: var(--text-muted);
6970
+ }
6971
+
6972
+ /* The Allow MIDI to listen button sits after the state sentence; it shows only while asking can help, and its own rule keeps it hidden. */
6973
+ #scratch-allow {
6974
+ flex: 0 0 auto;
6975
+ }
6976
+
6977
+ #scratch-allow[hidden] {
6978
+ display: none;
6979
+ }
6980
+
6981
+ #scratch-heard {
6982
+ flex: 0 1 auto;
6983
+ max-width: 50%;
6984
+ color: var(--text);
6985
+ font-variant-numeric: tabular-nums;
6986
+ }
6987
+
6988
+ /* The hear strip under the keyboard strip: the menu of instruments and channels, the Start choice, Loop, Listen and Stop on one row that wraps, and
6989
+ under them the strip's one live region, the sentence that says what became of a play. Like the send strip it scrolls with the view. The menu has
6990
+ the look of the words box of the send strip, the Start choice the segmented look of the grid group, and a long name is cut inside the menu. */
6991
+ #scratch-play {
6992
+ flex: 0 0 auto;
6993
+ box-sizing: border-box;
6994
+ display: flex;
6995
+ flex-wrap: wrap;
6996
+ align-items: center;
6997
+ gap: 0.25rem 0.5rem;
6998
+ font-size: 0.8125rem;
6999
+ font-weight: 400;
7000
+ line-height: 1.5;
7001
+ }
7002
+
7003
+ #scratch-play-output {
7004
+ flex: 0 1 16rem;
7005
+ min-width: 0;
7006
+ max-width: 100%;
7007
+ box-sizing: border-box;
7008
+ height: 2rem;
7009
+ padding: 0 0.5rem;
7010
+ background: var(--dominant);
7011
+ color: var(--text);
7012
+ border: 1px solid var(--border);
7013
+ font-family: inherit;
7014
+ font-size: 0.8125rem;
7015
+ line-height: 1.5;
7016
+ overflow: hidden;
7017
+ text-overflow: ellipsis;
7018
+ white-space: nowrap;
7019
+ }
7020
+
7021
+ #scratch-play-output:focus-visible {
7022
+ outline: 1px solid var(--text-muted);
7023
+ outline-offset: 0;
7024
+ }
7025
+
7026
+ #scratch-play-start {
7027
+ display: flex;
7028
+ align-items: center;
7029
+ gap: 0;
7030
+ border: 1px solid var(--border);
7031
+ }
7032
+
7033
+ #scratch-play-start .tl-btn {
7034
+ background: transparent;
7035
+ border: none;
7036
+ color: var(--text-muted);
7037
+ }
7038
+
7039
+ #scratch-play-start .tl-btn:hover:not([aria-disabled="true"]) {
7040
+ color: var(--text);
7041
+ }
7042
+
7043
+ #scratch-play-start .tl-btn[aria-pressed="true"] {
7044
+ color: var(--text);
7045
+ background: var(--surface-2);
7046
+ }
7047
+
7048
+ #scratch-play-start .tl-btn:focus-visible {
7049
+ outline-offset: -1px;
7050
+ }
7051
+
7052
+ #scratch-play .tl-btn[aria-disabled="true"] {
7053
+ color: var(--text-muted);
7054
+ }
7055
+
7056
+ #scratch-play-loop[aria-pressed="true"] {
7057
+ border-color: var(--text);
7058
+ }
7059
+
7060
+ #scratch-play-note {
7061
+ flex: 1 0 100%;
7062
+ margin: 0;
7063
+ min-height: 1.25rem;
7064
+ color: var(--text);
7065
+ overflow-wrap: anywhere;
7066
+ font-variant-numeric: tabular-nums;
7067
+ }
7068
+
7069
+ .sp-hear-id {
7070
+ font-weight: 600;
7071
+ }
7072
+
7073
+ /* The send strip, the last block of the Scratchpad: the phrase of the motif (the very text a send carries, in a box of its own height with its own
7074
+ scroll, wrapping at any character), the words for Claude with the Send to Claude button beside them, and the status line, the strip's one live
7075
+ region. It scrolls with the view; the phrase can be thousands of characters and is never a live region. */
7076
+ #scratch-send {
7077
+ flex: 0 0 auto;
7078
+ box-sizing: border-box;
7079
+ display: flex;
7080
+ flex-direction: column;
7081
+ gap: 0.25rem;
7082
+ font-size: 0.8125rem;
7083
+ font-weight: 400;
7084
+ line-height: 1.5;
7085
+ }
7086
+
7087
+ #scratch-phrase {
7088
+ display: block;
7089
+ box-sizing: border-box;
7090
+ max-height: 4.5rem;
7091
+ overflow: auto;
7092
+ padding: 0.25rem 0.5rem;
7093
+ background: var(--dominant);
7094
+ color: var(--text);
7095
+ border: 1px solid var(--border);
7096
+ font-family: var(--font-mono);
7097
+ font-size: 0.75rem;
7098
+ line-height: 1.5;
7099
+ overflow-wrap: anywhere;
7100
+ }
7101
+
7102
+ #scratch-phrase[data-empty="true"] {
7103
+ color: var(--text-muted);
7104
+ font-family: inherit;
7105
+ font-size: 0.8125rem;
7106
+ }
7107
+
7108
+ #scratch-phrase:focus-visible,
7109
+ #scratch-words:focus-visible {
7110
+ outline: 1px solid var(--text-muted);
7111
+ outline-offset: 0;
7112
+ }
7113
+
7114
+ #scratch-send-row {
7115
+ display: flex;
7116
+ align-items: center;
7117
+ gap: 0.5rem;
7118
+ }
7119
+
7120
+ #scratch-words {
7121
+ flex: 1 1 auto;
7122
+ min-width: 0;
7123
+ box-sizing: border-box;
7124
+ height: 2rem;
7125
+ padding: 0 0.5rem;
7126
+ background: var(--dominant);
7127
+ color: var(--text);
7128
+ border: 1px solid var(--border);
7129
+ font-family: inherit;
7130
+ font-size: 0.8125rem;
7131
+ line-height: 1.5;
7132
+ }
7133
+
7134
+ #scratch-send-button {
7135
+ flex: 0 0 auto;
7136
+ }
7137
+
7138
+ #scratch-send-status {
7139
+ margin: 0;
7140
+ min-height: 1.25rem;
7141
+ color: var(--text);
7142
+ overflow-wrap: anywhere;
7143
+ }
7144
+
5945
7145
  /* The load failure notice, for a page whose Strudel or acorn script did not load. It stands alone: it needs nothing
5946
7146
  from the rest of the page, which may never start. The base rule is the strip of the partial case (acorn missing,
5947
7147
  everything else works): in flow above the bar, the look of a kind-notice failed-change banner, one level above the