Coconuttttt Claude Opus 4.6 commited on
Commit
5d26e1b
·
1 Parent(s): a0f1a9a

UI redesign Phase 0-6 + functional fixes

Browse files

- tokens.css v3: chrome-only design tokens (slate + amber)
- corrector.css: full retoken, no pink in chrome, TL note 56px
- corrector.html: mode-panel, multi-edit panel, proxy scripts
- Phase 3: status bar restyle, shortcut-bar hidden, feedback collapsed
- Phase 4: left mode-panel with proxied mode/display/offset controls
- Phase 5: barline toolbar coral context-bar + marker dimming
- Phase 6: multi-edit panel for batch operations on multi-select
- Fix: staff drag now updates omrY so markers don't snap back
- Fix: free glyph click navigates timeline to that measure
- Fix: auto-fit zoom on image load (width-fit)
- Fix: acc-label size 46px, closer to note (+8,+5)
- Add: pitch preview sound on raise/lower/accidental/drag
- Update: glyph shapes with correct unicode symbols

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files changed (4) hide show
  1. static/corrector.css +202 -112
  2. static/corrector.html +120 -13
  3. static/corrector.js +92 -12
  4. static/tokens.css +79 -132
static/corrector.css CHANGED
@@ -1,92 +1,144 @@
1
  * { box-sizing: border-box; margin: 0; padding: 0; }
2
- body { font-family: 'Segoe UI', sans-serif; font-size: 13px; background: #1a1a2e; color: #e0e0e0; display: flex; flex-direction: column; height: 100vh; }
3
 
4
  /* Feedback Banner */
5
- #feedback-bar {
6
- display: flex; justify-content: center; padding: 6px 12px;
7
- background: #1a0a2e; border-bottom: 1px solid #444;
8
- }
9
  #feedback-btn {
10
  display: inline-block; padding: 8px 40px;
11
- background: #e94560; color: #fff; font-size: 22px; font-weight: bold;
12
  text-decoration: none; border-radius: 6px; letter-spacing: 1px;
13
  }
14
- #feedback-btn:hover { background: #ff6b6b; }
15
 
16
  /* Upload Bar */
17
  #upload-bar {
18
  display: flex; align-items: center; gap: 15px; padding: 8px 12px;
19
- background: #16213e; border-bottom: 1px solid #333;
20
- font-size: 24px; flex-wrap: wrap;
21
- }
22
- #upload-bar label { display: flex; align-items: center; gap: 6px; font-size: 24px; }
23
- #upload-bar input[type="file"] { width: 340px; font-size: 21px; }
24
- #upload-bar input[type="number"] { width: 75px; background: #0f3460; border: 1px solid #555; color: #fff; padding: 4px 5px; border-radius: 4px; font-size: 24px; }
25
- #load-btn { padding: 8px 22px; background: #e94560; color: #fff; border: none; border-radius: 5px; cursor: pointer; font-weight: bold; font-size: 24px; }
26
- #load-btn:hover { background: #ff6b6b; }
27
- #load-status { color: #aaa; font-size: 19px; }
28
  #page-nav { display: inline-flex; align-items: center; gap: 6px; }
29
- #page-nav button { padding: 4px 12px; background: #0f3460; color: #e0e0e0; border: 1px solid #555; border-radius: 4px; cursor: pointer; font-size: 24px; }
30
- #page-nav button:hover { background: #1a1a5e; }
31
- #page-indicator { font-size: 22px; color: #e0e0e0; min-width: 60px; text-align: center; }
32
 
33
  /* Toolbar */
34
  #toolbar {
35
  display: flex; align-items: center; gap: 8px; padding: 6px 12px;
36
- background: #0f3460; border-bottom: 1px solid #333; flex-wrap: wrap;
37
- font-size: 24px;
38
  }
39
- .tool-group { display: flex; align-items: center; gap: 4px; padding: 0 6px; border-right: 1px solid #333; }
40
  .tool-group:last-child { border-right: none; }
41
  .tool-group.right { margin-left: auto; }
42
  #toolbar button {
43
- padding: 6px 11px; background: #16213e; color: #e0e0e0; border: 1px solid #555;
44
- border-radius: 3px; cursor: pointer; font-size: 24px;
45
- }
46
- #toolbar button:hover { background: #1a1a5e; border-color: #888; }
47
- #toolbar button:active { background: #e94560; }
48
- #btn-show-free-glyphs { padding: 10px 18px; font-size: 26px; font-weight: bold; background: #4a2080; border-color: #8a4fd0; }
49
- #btn-show-free-glyphs:hover { background: #6a30b0; border-color: #a060e0; }
50
- #btn-show-free-glyphs.active { background: #7b2ff7; border-color: #a060e0; color: #fff; }
51
- #btn-download { background: #1b6b1b; border-color: #2a2; }
52
- #btn-download:hover { background: #2a8a2a; }
53
- #toolbar label { font-size: 24px; }
54
- #toolbar input[type="number"] { font-size: 24px; width: 68px; }
 
 
 
 
 
 
55
  #toolbar input[type="range"] { width: 112px; height: 12px; }
56
 
57
  #zoom-slider { width: 112px; vertical-align: middle; }
58
- #zoom-label { font-size: 21px; color: #aaa; min-width: 38px; display: inline-block; }
59
 
60
  /* Shortcut Reference Bar */
61
- #shortcut-bar {
62
- display: flex; flex-wrap: wrap; gap: 6px 18px; padding: 6px 12px;
63
- background: #12122a; border-bottom: 1px solid #2a2a4a;
64
- font-size: 20px; color: #888;
 
 
 
 
 
 
 
 
 
65
  }
66
- #shortcut-bar b { color: #bbb; }
67
 
68
  /* Progress Bar */
69
  #progress-bar-container {
70
- position: relative; height: 38px; background: #0a0a1a; border-bottom: 1px solid #333;
71
  cursor: pointer; user-select: none;
72
  }
73
  #progress-bar-fill {
74
- height: 100%; width: 0%; background: linear-gradient(90deg, #e94560, #ff6b6b);
75
  transition: none; pointer-events: none;
76
  }
77
  #progress-time {
78
  position: absolute; top: 0; right: 8px; line-height: 38px;
79
- font-size: 15px; color: #aaa; pointer-events: none;
80
  }
81
 
82
- /* Main Area: flex row for score + timeline */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  #main-area {
84
  flex: 1; display: flex; overflow: hidden; min-height: 0;
85
  }
86
 
87
  /* Main Canvas */
88
  #canvas-wrapper {
89
- flex: 1; overflow: auto; background: #111; position: relative; min-width: 0;
90
  }
91
  #canvas-container {
92
  position: relative; display: inline-block;
@@ -99,6 +151,10 @@ body { font-family: 'Segoe UI', sans-serif; font-size: 13px; background: #1a1a2e
99
  position: absolute; top: 0; left: 0; pointer-events: none;
100
  }
101
 
 
 
 
 
102
  /* Markers */
103
  .marker {
104
  pointer-events: all; cursor: pointer;
@@ -123,12 +179,11 @@ body { font-family: 'Segoe UI', sans-serif; font-size: 13px; background: #1a1a2e
123
  .marker.grade-mid { fill: rgba(255, 165, 0, 0.8); stroke: #ffa500; stroke-width: 1.5; }
124
  .marker.grade-ok { fill: rgba(255, 220, 50, 0.7); stroke: #ddcc33; stroke-width: 1; }
125
 
126
- /* Accidental label next to marker */
127
  .acc-label {
128
- pointer-events: none; font-size: 66px; fill: #ffd700; font-weight: bold;
129
- stroke: #000; stroke-width: 5px; paint-order: stroke fill;
130
  }
131
- /* Ghost marker for add mode */
132
  .ghost-marker {
133
  fill: rgba(255, 50, 50, 0.5); stroke: #ff3333; stroke-width: 2.5;
134
  stroke-dasharray: 4 2;
@@ -137,24 +192,28 @@ body { font-family: 'Segoe UI', sans-serif; font-size: 13px; background: #1a1a2e
137
  fill: #ff3333; font-size: 14px; font-weight: bold; opacity: 0.9;
138
  }
139
 
140
- /* Chord Popup */
 
 
 
 
141
  #chord-popup {
142
- position: absolute; background: #16213e; border: 1px solid #e94560;
143
  border-radius: 6px; padding: 6px 0; z-index: 100; min-width: 140px;
144
- box-shadow: 0 4px 12px rgba(0,0,0,0.5);
145
  }
146
  #chord-popup.hidden { display: none; }
147
- #chord-popup-title { padding: 4px 12px; font-size: 11px; color: #aaa; border-bottom: 1px solid #333; }
148
  #chord-popup-list { list-style: none; }
149
  #chord-popup-list li {
150
  padding: 6px 12px; cursor: pointer; font-size: 13px;
151
  }
152
- #chord-popup-list li:hover { background: #0f3460; }
153
  #chord-popup-list li .voice-dot {
154
  display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px;
155
  }
156
 
157
- /* Barline overlays */
158
  .barline-overlay {
159
  pointer-events: none;
160
  transition: stroke-width 0.1s, opacity 0.1s;
@@ -167,7 +226,7 @@ body { font-family: 'Segoe UI', sans-serif; font-size: 13px; background: #1a1a2e
167
  .barline-overlay.bl-selected { stroke: #0066ff; stroke-width: 3; opacity: 1.0; }
168
  .barline-overlay.bl-hover { stroke-width: 2.5; opacity: 0.8; }
169
 
170
- /* Free glyph overlays */
171
  .free-glyph-box {
172
  fill: rgba(255, 153, 0, 0.15);
173
  stroke: #ff9900;
@@ -186,51 +245,63 @@ body { font-family: 'Segoe UI', sans-serif; font-size: 13px; background: #1a1a2e
186
  fill: rgba(0, 204, 68, 0.15);
187
  }
188
 
189
- /* Glyph shape popup */
190
  #glyph-popup {
191
- position: fixed; background: #16213e; border: 2px solid #ff9900;
192
  border-radius: 8px; padding: 10px 0; z-index: 10000; min-width: 280px;
193
  max-height: 70vh; overflow-y: auto;
194
- box-shadow: 0 6px 20px rgba(0,0,0,0.6);
195
  }
196
  #glyph-popup.hidden { display: none; }
197
- #glyph-popup-title { padding: 8px 20px; font-size: 20px; color: #ccc; border-bottom: 1px solid #444; font-weight: bold; }
198
  #glyph-popup-list { list-style: none; }
199
  #glyph-popup-list li {
200
  padding: 10px 20px; cursor: pointer; font-size: 22px;
201
  }
202
- #glyph-popup-list li:hover { background: #0f3460; }
203
  #glyph-popup-list .glyph-cat {
204
- padding: 6px 20px; font-size: 18px; color: #ff9900; pointer-events: none; font-weight: bold; border-top: 1px solid #333;
205
  }
206
 
207
- /* Ghost barline for barline edit mode */
208
  .ghost-barline {
209
  stroke: #ff3333; stroke-width: 1.5; opacity: 0.5;
210
  stroke-dasharray: 6 3; pointer-events: none;
211
  }
212
-
213
- /* Barline measure label */
214
  .barline-label {
215
  fill: #00cc44; font-size: 9px; pointer-events: none; opacity: 0.7;
216
  }
217
 
218
- /* Barline toolbar */
219
  #barline-toolbar {
220
- display: none; padding: 5px 12px;
221
- background: #1a0a2e; border-bottom: 1px solid #555;
222
- font-size: 20px; color: #ccc; align-items: center; gap: 12px;
 
 
223
  }
224
  #barline-toolbar.active { display: flex; }
 
 
 
 
 
 
 
225
  #barline-toolbar button {
226
- padding: 4px 12px; background: #16213e; color: #e0e0e0;
227
- border: 1px solid #555; border-radius: 3px; cursor: pointer; font-size: 20px;
 
228
  }
229
- #barline-toolbar button:hover { background: #1a1a5e; }
230
- #barline-toolbar .bl-mode-label { color: #ff6644; font-weight: bold; }
231
- #barline-toolbar .bl-hint { color: #888; font-size: 18px; }
 
 
 
 
232
 
233
- /* Rhythm validation warnings */
234
  .rhythm-warning {
235
  pointer-events: none;
236
  animation: rhythm-pulse 2s ease-in-out infinite;
@@ -240,38 +311,37 @@ body { font-family: 'Segoe UI', sans-serif; font-size: 13px; background: #1a1a2e
240
  50% { fill: rgba(255, 40, 40, 0.25); }
241
  }
242
 
243
- /* Timeline Panel (Piano Roll) — right side */
244
  #timeline-panel {
245
  flex: 1; min-width: 0;
246
- background: #0d0d1a; border-left: 2px solid #e94560;
247
  display: flex; flex-direction: column; overflow: hidden;
248
  }
249
  #timeline-header {
250
  display: flex; align-items: center; padding: 10px 14px;
251
- background: linear-gradient(180deg, #1a0a2e, #12122a);
252
- border-bottom: 2px solid #e94560; gap: 10px; flex-shrink: 0;
253
  }
254
  #timeline-title {
255
- font-weight: bold; color: #e94560; font-size: 28px;
256
- text-shadow: 0 0 8px rgba(233,69,96,0.4);
257
  white-space: nowrap;
258
  }
259
  #timeline-controls { display: flex; align-items: center; gap: 12px; margin-left: auto; }
260
  #timeline-snap-label {
261
- color: #bbb; font-size: 24px; display: flex; align-items: center; gap: 6px;
262
  cursor: pointer;
263
  }
264
  #timeline-snap-label input[type="checkbox"] { width: 20px; height: 20px; }
265
- #timeline-snap-label:hover { color: #fff; }
266
  #timeline-grid-select {
267
- background: #1a1a3e; color: #ddd; border: 1px solid #444;
268
- border-radius: 4px; font-size: 24px; padding: 6px 10px;
269
  }
270
  #timeline-close {
271
- background: none; border: none; color: #666;
272
- font-size: 36px; cursor: pointer; padding: 0 8px; line-height: 1;
273
  }
274
- #timeline-close:hover { color: #e94560; }
275
  #timeline-body {
276
  flex: 1; overflow-y: auto; overflow-x: hidden; padding: 8px;
277
  }
@@ -279,34 +349,34 @@ body { font-family: 'Segoe UI', sans-serif; font-size: 13px; background: #1a1a2e
279
  /* Staff lane */
280
  .tl-lane {
281
  margin-bottom: 6px; position: relative;
282
- background: #0a0a16; border: 1px solid #1a1a3e; border-radius: 4px;
283
  overflow: hidden;
284
  }
285
  .tl-lane-label {
286
  position: absolute; left: 0; top: 0; bottom: 0; width: 80px;
287
- background: #111128; border-right: 1px solid #2a2a4a;
288
- font-size: 20px; color: #8888bb; padding: 4px 6px;
289
  pointer-events: none; z-index: 2; line-height: 1.3;
290
  display: flex; flex-direction: column; justify-content: center;
291
  }
292
- .tl-lane-label .staff-id { font-size: 22px; color: #aaa; font-weight: bold; }
293
  .tl-grid-line {
294
  position: absolute; top: 0; bottom: 0; width: 1px;
295
- background: #1a1a2e; pointer-events: none; z-index: 0;
296
  }
297
- .tl-grid-line.beat { background: #2a2a4a; }
298
- .tl-grid-line.bar-start { background: #555; width: 2px; }
299
  .tl-grid-label {
300
- position: absolute; bottom: 1px; font-size: 8px; color: #555;
301
  pointer-events: none; transform: translateX(-50%); z-index: 0;
302
  }
303
 
304
- /* Note block */
305
  .tl-note {
306
- position: absolute; height: 96px;
307
  border-radius: 4px; cursor: grab; z-index: 1;
308
- font-size: 22px; font-weight: 600; line-height: 96px;
309
- text-align: center; padding: 0 3px;
310
  color: #fff; white-space: nowrap; overflow: hidden;
311
  text-overflow: ellipsis;
312
  user-select: none; min-width: 18px;
@@ -316,17 +386,17 @@ body { font-family: 'Segoe UI', sans-serif; font-size: 13px; background: #1a1a2e
316
  }
317
  .tl-note:hover {
318
  box-shadow: 0 0 8px rgba(255,255,255,0.25), inset 0 0 12px rgba(255,255,255,0.08);
319
- z-index: 3; transform: scaleY(1.08);
320
  }
321
  .tl-note.dragging {
322
  cursor: grabbing; opacity: 0.9; z-index: 4;
323
- box-shadow: 0 0 14px rgba(233,69,96,0.7), 0 2px 8px rgba(0,0,0,0.5);
324
- transform: scaleY(1.12);
325
  }
326
  .tl-note.selected {
327
  border: 2px solid #ffd700;
328
- box-shadow: 0 0 12px rgba(255,215,0,0.6), 0 0 24px rgba(255,215,0,0.3);
329
- transform: scaleY(1.1);
330
  }
331
  .tl-note.rest {
332
  background: #222233 !important; color: #777; border-style: dashed;
@@ -341,7 +411,7 @@ body { font-family: 'Segoe UI', sans-serif; font-size: 13px; background: #1a1a2e
341
  .tl-note.voice3 { background: linear-gradient(135deg, #228833, #33aa44); }
342
  .tl-note.voice4 { background: linear-gradient(135deg, #cc8800, #eea500); }
343
 
344
- /* Rubber-band selection */
345
  .tl-rubber {
346
  border: 2px dashed #ffd700; background: rgba(255,215,0,0.1);
347
  pointer-events: none; z-index: 100;
@@ -361,13 +431,33 @@ body { font-family: 'Segoe UI', sans-serif; font-size: 13px; background: #1a1a2e
361
  /* Measure navigation arrows */
362
  .tl-nav { display: flex; gap: 8px; margin-bottom: 8px; }
363
  .tl-nav button {
364
- padding: 6px 16px; background: #16213e; color: #aaa; border: 1px solid #333;
365
- border-radius: 4px; cursor: pointer; font-size: 22px;
366
  }
367
- .tl-nav button:hover { background: #1a1a5e; color: #fff; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
 
369
  /* Status Bar */
370
  #status-bar {
371
- display: flex; justify-content: space-between; padding: 5px 12px;
372
- background: #16213e; border-top: 1px solid #333; font-size: 19px; color: #aaa;
373
- }
 
 
 
 
 
 
 
1
  * { box-sizing: border-box; margin: 0; padding: 0; }
2
+ body { font-family: var(--font-sans); font-size: var(--fs-body); background: var(--bg-1); color: var(--ink-0); display: flex; flex-direction: column; height: 100vh; }
3
 
4
  /* Feedback Banner */
5
+ #feedback-bar { display: none; }
 
 
 
6
  #feedback-btn {
7
  display: inline-block; padding: 8px 40px;
8
+ background: var(--accent-dim); color: #fff; font-size: var(--fs-body); font-weight: bold;
9
  text-decoration: none; border-radius: 6px; letter-spacing: 1px;
10
  }
11
+ #feedback-btn:hover { background: #f7b06d; }
12
 
13
  /* Upload Bar */
14
  #upload-bar {
15
  display: flex; align-items: center; gap: 15px; padding: 8px 12px;
16
+ background: var(--bg-2); border-bottom: 1px solid var(--line);
17
+ font-size: var(--fs-body); flex-wrap: wrap;
18
+ }
19
+ #upload-bar label { display: flex; align-items: center; gap: 6px; font-size: var(--fs-label); }
20
+ #upload-bar input[type="file"] { width: 340px; font-size: var(--fs-label); }
21
+ #upload-bar input[type="number"] { width: 75px; background: var(--bg-3); border: 1px solid var(--line); color: var(--ink-0); padding: 4px 5px; border-radius: 4px; font-size: var(--fs-label); }
22
+ #load-btn { padding: 8px 22px; background: linear-gradient(180deg, #f7b06d, #e08840); color: #1c1004; border: none; border-radius: 5px; cursor: pointer; font-weight: 600; font-size: var(--fs-label); }
23
+ #load-btn:hover { filter: brightness(1.06); }
24
+ #load-status { color: var(--ink-2); font-size: var(--fs-meta); }
25
  #page-nav { display: inline-flex; align-items: center; gap: 6px; }
26
+ #page-nav button { padding: 4px 12px; background: var(--bg-3); color: var(--ink-0); border: 1px solid var(--line); border-radius: 4px; cursor: pointer; font-size: var(--fs-label); }
27
+ #page-nav button:hover { background: var(--bg-4); }
28
+ #page-indicator { font-size: var(--fs-label); font-family: var(--font-mono); color: var(--ink-0); min-width: 60px; text-align: center; }
29
 
30
  /* Toolbar */
31
  #toolbar {
32
  display: flex; align-items: center; gap: 8px; padding: 6px 12px;
33
+ background: var(--bg-3); border-bottom: 1px solid var(--line); flex-wrap: wrap;
34
+ font-size: var(--fs-body);
35
  }
36
+ .tool-group { display: flex; align-items: center; gap: 4px; padding: 0 6px; border-right: 1px solid var(--line); }
37
  .tool-group:last-child { border-right: none; }
38
  .tool-group.right { margin-left: auto; }
39
  #toolbar button {
40
+ padding: 6px 11px; background: var(--bg-2); color: var(--ink-0); border: 1px solid var(--line-strong);
41
+ border-radius: 3px; cursor: pointer; font-size: var(--fs-label);
42
+ }
43
+ #toolbar button:hover { background: var(--bg-4); border-color: var(--line-strong); }
44
+ #toolbar button:active { background: var(--accent-soft); color: var(--accent); }
45
+ #btn-show-free-glyphs { padding: 10px 18px; font-size: var(--fs-label); font-weight: bold; background: rgba(255,153,0,.12); border-color: rgba(255,153,0,.4); color: #ffcc88; }
46
+ #btn-show-free-glyphs:hover { background: rgba(255,153,0,.2); }
47
+ #btn-show-free-glyphs.active { background: rgba(255,153,0,.25); border-color: #ff9900; color: #ffd9a3; }
48
+ #btn-download { background: rgba(124,212,84,.12); border-color: rgba(124,212,84,.4); color: #b6e4a4; }
49
+ #btn-download:hover { background: rgba(124,212,84,.2); }
50
+ #feedback-link {
51
+ display: inline-flex; width: 30px; height: 30px;
52
+ align-items: center; justify-content: center;
53
+ color: var(--ink-2); text-decoration: none; border-radius: var(--r-md);
54
+ }
55
+ #feedback-link:hover { background: var(--bg-3); color: var(--ink-0); }
56
+ #toolbar label { font-size: var(--fs-label); }
57
+ #toolbar input[type="number"] { font-size: var(--fs-label); width: 68px; }
58
  #toolbar input[type="range"] { width: 112px; height: 12px; }
59
 
60
  #zoom-slider { width: 112px; vertical-align: middle; }
61
+ #zoom-label { font-size: var(--fs-meta); color: var(--ink-2); min-width: 38px; display: inline-block; }
62
 
63
  /* Shortcut Reference Bar */
64
+ #shortcut-bar { display: none; }
65
+ #shortcut-bar b { color: var(--ink-1); }
66
+
67
+ /* OMR Apply Button (moved to status-bar in Phase 3) */
68
+ #omr-apply-btn {
69
+ margin-left: 8px; padding: 4px 10px;
70
+ background: rgba(124,212,84,.12); border: 1px solid rgba(124,212,84,.4); color: #b6e4a4;
71
+ border-radius: 5px; cursor: pointer; font-size: var(--fs-meta);
72
+ }
73
+ #omr-apply-btn:hover { background: rgba(124,212,84,.2); }
74
+ #omr-apply-badge {
75
+ background: var(--danger); color: #fff; border-radius: 8px;
76
+ padding: 0 5px; margin-left: 4px; font-size: 10px;
77
  }
 
78
 
79
  /* Progress Bar */
80
  #progress-bar-container {
81
+ position: relative; height: 38px; background: var(--bg-0); border-bottom: 1px solid var(--line);
82
  cursor: pointer; user-select: none;
83
  }
84
  #progress-bar-fill {
85
+ height: 100%; width: 0%; background: linear-gradient(90deg, var(--accent), #f7b06d);
86
  transition: none; pointer-events: none;
87
  }
88
  #progress-time {
89
  position: absolute; top: 0; right: 8px; line-height: 38px;
90
+ font-size: var(--fs-meta); color: var(--ink-2); pointer-events: none;
91
  }
92
 
93
+ /* Mode Panel (left aside) */
94
+ #mode-panel {
95
+ width: 248px; flex-shrink: 0;
96
+ background: var(--bg-2); border-right: 1px solid var(--line);
97
+ display: flex; flex-direction: column;
98
+ overflow-y: auto;
99
+ }
100
+ #mode-panel .mode-list { padding: 8px; display: flex; flex-direction: column; gap: 4px; }
101
+ .mode-row {
102
+ display: flex; align-items: center; gap: 10px;
103
+ padding: 8px 10px; border-radius: 6px;
104
+ background: transparent; border: 1px solid transparent;
105
+ color: var(--ink-0); cursor: pointer;
106
+ font-size: var(--fs-label); text-align: left;
107
+ }
108
+ .mode-row .ic {
109
+ width: 24px; height: 24px; display: inline-flex; align-items: center; justify-content: center;
110
+ background: var(--bg-3); border-radius: 5px; color: var(--ink-1); font-size: 13px;
111
+ }
112
+ .mode-row .kbd-chip { margin-left: auto; }
113
+ .mode-row.is-active {
114
+ background: var(--accent-soft); border-color: var(--accent-dim); color: var(--accent);
115
+ }
116
+ .mode-row.is-active .ic { background: rgba(243,162,91,.2); color: var(--accent); }
117
+
118
+ #mode-panel .display-toggles { padding: 4px 14px 12px; display: flex; flex-direction: column; gap: 4px; }
119
+ .tg { display: flex; align-items: center; gap: 10px; font-size: var(--fs-label); color: var(--ink-1); cursor: pointer; padding: 4px 0; }
120
+ .tg input { accent-color: var(--accent); }
121
+
122
+ #mode-panel .offset-grid {
123
+ padding: 6px 14px 12px;
124
+ display: grid; grid-template-columns: 1fr 1fr; gap: 8px 10px;
125
+ }
126
+ .offset-grid label { display: flex; flex-direction: column; gap: 4px; font-size: var(--fs-meta); color: var(--ink-2); }
127
+ .offset-grid input[type="number"] {
128
+ background: #0a0d14; border: 1px solid var(--line); border-radius: 5px;
129
+ color: var(--ink-0); padding: 4px 6px; font-family: var(--font-mono); font-size: 11.5px;
130
+ }
131
+
132
+ #warning-list { padding: 6px 14px 12px; display: flex; flex-direction: column; gap: 6px; font-size: var(--fs-meta); }
133
+
134
+ /* Main Area: flex row for mode-panel + score + timeline */
135
  #main-area {
136
  flex: 1; display: flex; overflow: hidden; min-height: 0;
137
  }
138
 
139
  /* Main Canvas */
140
  #canvas-wrapper {
141
+ flex: 1; overflow: auto; background: var(--bg-0); position: relative; min-width: 0;
142
  }
143
  #canvas-container {
144
  position: relative; display: inline-block;
 
151
  position: absolute; top: 0; left: 0; pointer-events: none;
152
  }
153
 
154
+ /* ═══════════════════════════════════════════════════════════════
155
+ PRESERVED — do not change. See PRESERVED_VALUES.md §1
156
+ ═══════════════════════════════════════════════════════════════ */
157
+
158
  /* Markers */
159
  .marker {
160
  pointer-events: all; cursor: pointer;
 
179
  .marker.grade-mid { fill: rgba(255, 165, 0, 0.8); stroke: #ffa500; stroke-width: 1.5; }
180
  .marker.grade-ok { fill: rgba(255, 220, 50, 0.7); stroke: #ddcc33; stroke-width: 1; }
181
 
182
+ /* PRESERVED §2 Accidental & ghost labels */
183
  .acc-label {
184
+ pointer-events: none; font-size: 46px; fill: #ffd700; font-weight: bold;
185
+ stroke: #000; stroke-width: 4px; paint-order: stroke fill;
186
  }
 
187
  .ghost-marker {
188
  fill: rgba(255, 50, 50, 0.5); stroke: #ff3333; stroke-width: 2.5;
189
  stroke-dasharray: 4 2;
 
192
  fill: #ff3333; font-size: 14px; font-weight: bold; opacity: 0.9;
193
  }
194
 
195
+ /* ═══════════════════════════════════════════════════════════════
196
+ CHROME — retokened
197
+ ═══════════════════════════════════════════════════════════════ */
198
+
199
+ /* Chord Popup — chrome retokened, position PRESERVED §6 */
200
  #chord-popup {
201
+ position: absolute; background: var(--bg-2); border: 1px solid var(--line-strong);
202
  border-radius: 6px; padding: 6px 0; z-index: 100; min-width: 140px;
203
+ box-shadow: var(--shadow-md);
204
  }
205
  #chord-popup.hidden { display: none; }
206
+ #chord-popup-title { padding: 4px 12px; font-size: 11px; color: var(--ink-2); border-bottom: 1px solid var(--line); }
207
  #chord-popup-list { list-style: none; }
208
  #chord-popup-list li {
209
  padding: 6px 12px; cursor: pointer; font-size: 13px;
210
  }
211
+ #chord-popup-list li:hover { background: var(--bg-3); }
212
  #chord-popup-list li .voice-dot {
213
  display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px;
214
  }
215
 
216
+ /* PRESERVED §4 — Barline overlays */
217
  .barline-overlay {
218
  pointer-events: none;
219
  transition: stroke-width 0.1s, opacity 0.1s;
 
226
  .barline-overlay.bl-selected { stroke: #0066ff; stroke-width: 3; opacity: 1.0; }
227
  .barline-overlay.bl-hover { stroke-width: 2.5; opacity: 0.8; }
228
 
229
+ /* PRESERVED §5 — Free glyph overlays */
230
  .free-glyph-box {
231
  fill: rgba(255, 153, 0, 0.15);
232
  stroke: #ff9900;
 
245
  fill: rgba(0, 204, 68, 0.15);
246
  }
247
 
248
+ /* PRESERVED §6 — Glyph shape popup (sizes/position locked, chrome retokened) */
249
  #glyph-popup {
250
+ position: fixed; background: var(--bg-2); border: 2px solid #ff9900;
251
  border-radius: 8px; padding: 10px 0; z-index: 10000; min-width: 280px;
252
  max-height: 70vh; overflow-y: auto;
253
+ box-shadow: var(--shadow-lg);
254
  }
255
  #glyph-popup.hidden { display: none; }
256
+ #glyph-popup-title { padding: 8px 20px; font-size: 20px; color: var(--ink-1); border-bottom: 1px solid var(--line); font-weight: bold; }
257
  #glyph-popup-list { list-style: none; }
258
  #glyph-popup-list li {
259
  padding: 10px 20px; cursor: pointer; font-size: 22px;
260
  }
261
+ #glyph-popup-list li:hover { background: var(--bg-3); }
262
  #glyph-popup-list .glyph-cat {
263
+ padding: 6px 20px; font-size: 18px; color: #ff9900; pointer-events: none; font-weight: bold; border-top: 1px solid var(--line);
264
  }
265
 
266
+ /* PRESERVED §4 — Ghost barline & barline label */
267
  .ghost-barline {
268
  stroke: #ff3333; stroke-width: 1.5; opacity: 0.5;
269
  stroke-dasharray: 6 3; pointer-events: none;
270
  }
 
 
271
  .barline-label {
272
  fill: #00cc44; font-size: 9px; pointer-events: none; opacity: 0.7;
273
  }
274
 
275
+ /* Barline toolbar — chrome retokened */
276
  #barline-toolbar {
277
+ display: none;
278
+ height: 44px; padding: 0 16px; align-items: center; gap: 12px;
279
+ background: linear-gradient(180deg, rgba(255,108,122,.16), rgba(255,108,122,.05));
280
+ border-bottom: 1px solid rgba(255,108,122,.35);
281
+ font-size: var(--fs-label); color: var(--ink-1);
282
  }
283
  #barline-toolbar.active { display: flex; }
284
+ #barline-toolbar .bl-mode-label {
285
+ padding: 4px 12px; border-radius: 999px;
286
+ font-size: 11px; font-weight: 700; letter-spacing: .06em;
287
+ background: rgba(255,108,122,.25); color: #ffd0d4; border: 1px solid rgba(255,108,122,.4);
288
+ }
289
+ #barline-toolbar #bl-count { font-family: var(--font-mono); color: var(--ink-1); }
290
+ #barline-toolbar .bl-hint { color: var(--ink-2); font-size: var(--fs-meta); margin-left: auto; }
291
  #barline-toolbar button {
292
+ height: 28px; padding: 0 12px; border-radius: var(--r-md);
293
+ background: var(--bg-3); border: 1px solid var(--line-strong); color: var(--ink-0);
294
+ font-size: var(--fs-label); cursor: pointer;
295
  }
296
+ #barline-toolbar button:hover { background: var(--bg-4); }
297
+ #barline-toolbar #bl-btn-accept {
298
+ background: linear-gradient(180deg, #f7b06d, #e08840); border-color: #bf6c2f; color: #1c1004; font-weight: 600;
299
+ }
300
+
301
+ /* Dim markers during barline edit mode */
302
+ body.mode-barline #marker-svg .marker { opacity: 0.3; transition: opacity .15s; }
303
 
304
+ /* PRESERVED §8 — Rhythm validation warnings */
305
  .rhythm-warning {
306
  pointer-events: none;
307
  animation: rhythm-pulse 2s ease-in-out infinite;
 
311
  50% { fill: rgba(255, 40, 40, 0.25); }
312
  }
313
 
314
+ /* Timeline Panel (Piano Roll) — chrome retokened */
315
  #timeline-panel {
316
  flex: 1; min-width: 0;
317
+ background: var(--bg-1); border-left: 1px solid var(--line);
318
  display: flex; flex-direction: column; overflow: hidden;
319
  }
320
  #timeline-header {
321
  display: flex; align-items: center; padding: 10px 14px;
322
+ background: linear-gradient(180deg, var(--bg-2), var(--bg-1));
323
+ border-bottom: 1px solid var(--line); gap: 10px; flex-shrink: 0;
324
  }
325
  #timeline-title {
326
+ font-weight: bold; color: var(--ink-0); font-size: var(--fs-display);
 
327
  white-space: nowrap;
328
  }
329
  #timeline-controls { display: flex; align-items: center; gap: 12px; margin-left: auto; }
330
  #timeline-snap-label {
331
+ color: var(--ink-1); font-size: var(--fs-label); display: flex; align-items: center; gap: 6px;
332
  cursor: pointer;
333
  }
334
  #timeline-snap-label input[type="checkbox"] { width: 20px; height: 20px; }
335
+ #timeline-snap-label:hover { color: var(--ink-0); }
336
  #timeline-grid-select {
337
+ background: var(--bg-3); color: var(--ink-0); border: 1px solid var(--line);
338
+ border-radius: 4px; font-size: var(--fs-label); padding: 6px 10px;
339
  }
340
  #timeline-close {
341
+ background: none; border: none; color: var(--ink-3);
342
+ font-size: 20px; cursor: pointer; padding: 0 8px; line-height: 1;
343
  }
344
+ #timeline-close:hover { color: var(--accent); }
345
  #timeline-body {
346
  flex: 1; overflow-y: auto; overflow-x: hidden; padding: 8px;
347
  }
 
349
  /* Staff lane */
350
  .tl-lane {
351
  margin-bottom: 6px; position: relative;
352
+ background: #0a0a16; border: 1px solid var(--line); border-radius: 4px;
353
  overflow: hidden;
354
  }
355
  .tl-lane-label {
356
  position: absolute; left: 0; top: 0; bottom: 0; width: 80px;
357
+ background: var(--bg-2); border-right: 1px solid var(--line);
358
+ font-size: var(--fs-meta); color: var(--ink-2); padding: 4px 6px;
359
  pointer-events: none; z-index: 2; line-height: 1.3;
360
  display: flex; flex-direction: column; justify-content: center;
361
  }
362
+ .tl-lane-label .staff-id { font-size: var(--fs-label); color: var(--ink-1); font-weight: 700; }
363
  .tl-grid-line {
364
  position: absolute; top: 0; bottom: 0; width: 1px;
365
+ background: var(--bg-1); pointer-events: none; z-index: 0;
366
  }
367
+ .tl-grid-line.beat { background: var(--line); }
368
+ .tl-grid-line.bar-start { background: var(--line-strong); width: 2px; }
369
  .tl-grid-label {
370
+ position: absolute; bottom: 1px; font-size: 8px; color: var(--ink-3);
371
  pointer-events: none; transform: translateX(-50%); z-index: 0;
372
  }
373
 
374
+ /* PRESERVED §7 — Note block (height/font-size changeable, voice gradients locked) */
375
  .tl-note {
376
+ position: absolute; height: 56px;
377
  border-radius: 4px; cursor: grab; z-index: 1;
378
+ font-size: var(--fs-label); font-weight: 600; line-height: 52px;
379
+ text-align: center; padding: 0 3px; margin: 7px 0;
380
  color: #fff; white-space: nowrap; overflow: hidden;
381
  text-overflow: ellipsis;
382
  user-select: none; min-width: 18px;
 
386
  }
387
  .tl-note:hover {
388
  box-shadow: 0 0 8px rgba(255,255,255,0.25), inset 0 0 12px rgba(255,255,255,0.08);
389
+ z-index: 3; transform: scaleY(1.15);
390
  }
391
  .tl-note.dragging {
392
  cursor: grabbing; opacity: 0.9; z-index: 4;
393
+ box-shadow: 0 0 14px rgba(243,162,91,0.7), 0 2px 8px rgba(0,0,0,0.5);
394
+ transform: scaleY(1.2);
395
  }
396
  .tl-note.selected {
397
  border: 2px solid #ffd700;
398
+ box-shadow: 0 0 8px rgba(255,215,0,0.7), 0 0 16px rgba(255,215,0,0.35);
399
+ transform: scaleY(1.18);
400
  }
401
  .tl-note.rest {
402
  background: #222233 !important; color: #777; border-style: dashed;
 
411
  .tl-note.voice3 { background: linear-gradient(135deg, #228833, #33aa44); }
412
  .tl-note.voice4 { background: linear-gradient(135deg, #cc8800, #eea500); }
413
 
414
+ /* PRESERVED §3 — Rubber-band selection */
415
  .tl-rubber {
416
  border: 2px dashed #ffd700; background: rgba(255,215,0,0.1);
417
  pointer-events: none; z-index: 100;
 
431
  /* Measure navigation arrows */
432
  .tl-nav { display: flex; gap: 8px; margin-bottom: 8px; }
433
  .tl-nav button {
434
+ padding: 6px 16px; background: var(--bg-2); color: var(--ink-2); border: 1px solid var(--line);
435
+ border-radius: 4px; cursor: pointer; font-size: var(--fs-label);
436
  }
437
+ .tl-nav button:hover { background: var(--bg-4); color: var(--ink-0); }
438
+
439
+ /* Multi-Edit Panel */
440
+ #multi-edit-panel {
441
+ width: 280px; flex-shrink: 0;
442
+ background: var(--bg-2); border-left: 1px solid var(--line);
443
+ display: flex; flex-direction: column;
444
+ padding-bottom: 16px; overflow-y: auto;
445
+ }
446
+ #multi-edit-panel[hidden] { display: none; }
447
+ .me-block { padding: 12px 14px; display: flex; flex-direction: column; gap: 8px; border-bottom: 1px solid var(--line); }
448
+ .me-block:last-of-type { border-bottom: 0; flex-direction: row; gap: 8px; }
449
+ .me-label { font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-2); }
450
+ .me-row { display: flex; gap: 6px; flex-wrap: wrap; }
451
+ .me-row .btn { flex: 1; min-width: 0; }
452
 
453
  /* Status Bar */
454
  #status-bar {
455
+ display: flex; align-items: center; gap: 18px;
456
+ padding: 0 14px; height: 30px;
457
+ background: var(--bg-2); border-top: 1px solid var(--line);
458
+ font-size: var(--fs-meta); color: var(--ink-2);
459
+ flex-shrink: 0;
460
+ }
461
+ #status-mode { color: var(--accent); font-weight: 600; }
462
+ #status-selection { color: var(--ink-1); }
463
+ #status-total { margin-left: auto; }
static/corrector.html CHANGED
@@ -73,15 +73,8 @@
73
  <div class="tool-group">
74
  <label><span data-i18n="zoom">확대</span>: <input type="range" id="zoom-slider" min="25" max="200" value="50" step="5"><span id="zoom-label">50%</span></label>
75
  </div>
76
- <div class="tool-group">
77
- <label><span data-i18n="x_off">X 오프셋</span>: <input type="number" id="offset-x" value="0" step="1" style="width:84px"></label>
78
- <label><span data-i18n="y_off">Y 오프셋</span>: <input type="number" id="offset-y" value="0" step="1" style="width:84px"></label>
79
- </div>
80
- <div class="tool-group">
81
- <label><span data-i18n="staff_dist">보표 간격</span>: <input type="number" id="staff-dist-input" value="65" step="1" style="width:84px"></label>
82
- <label><span data-i18n="sys_dist">단 간격 보정</span>: <input type="number" id="sys-dist-adj" value="0" step="1" style="width:84px"></label>
83
- </div>
84
- <div class="tool-group">
85
  <button id="btn-debug-lines" data-i18n="staff_lines" title="오선 표시/숨김">오선 표시</button>
86
  <button id="btn-adjust-staves" data-i18n="adjust_staves" title="오선 위치 수동 조정">오선 조정</button>
87
  <button id="btn-show-barlines" data-i18n="show_barlines" title="감지된 마디선 표시/숨김">마디선</button>
@@ -92,6 +85,7 @@
92
  <button id="btn-lang" title="한국어/English">EN</button>
93
  </div>
94
  <div class="tool-group right">
 
95
  <button id="btn-download" data-i18n="download">XML 다운로드</button>
96
  </div>
97
  </div>
@@ -124,9 +118,6 @@
124
  <span><b>Shift+K:</b> <span data-i18n="sc_keysig">조표</span></span>
125
  <span><b>Shift+C:</b> <span data-i18n="sc_clef">음자리표</span></span>
126
  <span><b>t:</b> Timeline</span>
127
- <button id="omr-apply-btn" style="margin-left:12px;padding:2px 10px;background:#2a6;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:12px;display:none" onclick="applyOmrEdits()">
128
- Apply to OMR <span id="omr-apply-badge" style="background:#e44;color:#fff;border-radius:8px;padding:0 5px;margin-left:4px;font-size:10px;display:none"></span>
129
- </button>
130
  </div>
131
 
132
  <!-- Barline Edit Toolbar -->
@@ -141,6 +132,34 @@
141
 
142
  <!-- Main Area: Score + Timeline side by side -->
143
  <div id="main-area">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  <div id="canvas-wrapper">
145
  <div id="canvas-container">
146
  <img id="score-image" alt="Score image">
@@ -165,6 +184,45 @@
165
  </div>
166
  <div id="timeline-body"></div>
167
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  </div>
169
 
170
  <!-- Chord Popup -->
@@ -181,11 +239,60 @@
181
 
182
  <!-- Status Bar -->
183
  <div id="status-bar">
184
- <span id="status-mode" style="color:#ffff66;font-weight:bold;"></span>
185
  <span id="status-selection" data-i18n="no_sel">선택 없음</span>
186
  <span id="status-total"></span>
 
 
 
187
  </div>
188
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  <script src="/static/corrector.js"></script>
190
  </body>
191
  </html>
 
73
  <div class="tool-group">
74
  <label><span data-i18n="zoom">확대</span>: <input type="range" id="zoom-slider" min="25" max="200" value="50" step="5"><span id="zoom-label">50%</span></label>
75
  </div>
76
+ <!-- Hidden buttons: IDs required by corrector.js, proxied from #mode-panel -->
77
+ <div class="tool-group" style="display:none">
 
 
 
 
 
 
 
78
  <button id="btn-debug-lines" data-i18n="staff_lines" title="오선 표시/숨김">오선 표시</button>
79
  <button id="btn-adjust-staves" data-i18n="adjust_staves" title="오선 위치 수동 조정">오선 조정</button>
80
  <button id="btn-show-barlines" data-i18n="show_barlines" title="감지된 마디선 표시/숨김">마디선</button>
 
85
  <button id="btn-lang" title="한국어/English">EN</button>
86
  </div>
87
  <div class="tool-group right">
88
+ <a id="feedback-link" href="https://docs.google.com/forms/d/e/1FAIpQLScDoM53RMjDLlftORYHXmZ5kmkN4TTZOIyFIRuVsZhp4RGjEA/viewform" target="_blank" rel="noopener" title="피드백">&#9993;</a>
89
  <button id="btn-download" data-i18n="download">XML 다운로드</button>
90
  </div>
91
  </div>
 
118
  <span><b>Shift+K:</b> <span data-i18n="sc_keysig">조표</span></span>
119
  <span><b>Shift+C:</b> <span data-i18n="sc_clef">음자리표</span></span>
120
  <span><b>t:</b> Timeline</span>
 
 
 
121
  </div>
122
 
123
  <!-- Barline Edit Toolbar -->
 
132
 
133
  <!-- Main Area: Score + Timeline side by side -->
134
  <div id="main-area">
135
+ <aside id="mode-panel">
136
+ <section class="panel-head"><h4>모드</h4></section>
137
+ <div class="mode-list">
138
+ <button class="mode-row is-active" data-proxy=""><span class="ic">✎</span> 음표 편집 <span class="kbd-chip">esc</span></button>
139
+ <button class="mode-row" data-proxy="btn-barline-mode"><span class="ic">┊</span> 마디선 편집 <span class="kbd-chip">B</span></button>
140
+ <button class="mode-row" data-proxy="btn-show-free-glyphs"><span class="ic">◇</span> 후보 기호 <span class="kbd-chip" id="glyph-count"></span></button>
141
+ <button class="mode-row" data-proxy="btn-adjust-staves"><span class="ic">≣</span> 오선 위치 조정</button>
142
+ </div>
143
+
144
+ <section class="panel-head"><h4>표시</h4></section>
145
+ <div class="display-toggles">
146
+ <label class="tg"><input type="checkbox" data-proxy="btn-debug-lines"> 오선 표시</label>
147
+ <label class="tg"><input type="checkbox" data-proxy="btn-show-barlines"> 마디선 오버레이</label>
148
+ </div>
149
+
150
+ <section class="panel-head"><h4>오프셋 / 간격</h4></section>
151
+ <div class="offset-grid">
152
+ <label>X<input type="number" id="offset-x" value="0" step="1"></label>
153
+ <label>Y<input type="number" id="offset-y" value="0" step="1"></label>
154
+ <label>보표 간격<input type="number" id="staff-dist-input" value="65" step="1"></label>
155
+ <label>단 간격 보정<input type="number" id="sys-dist-adj" value="0" step="1"></label>
156
+ </div>
157
+
158
+ <div style="flex:1"></div>
159
+ <section class="panel-head"><h4>경고</h4></section>
160
+ <div id="warning-list"></div>
161
+ </aside>
162
+
163
  <div id="canvas-wrapper">
164
  <div id="canvas-container">
165
  <img id="score-image" alt="Score image">
 
184
  </div>
185
  <div id="timeline-body"></div>
186
  </div>
187
+
188
+ <!-- Multi-Edit Panel (appears on multi-select) -->
189
+ <aside id="multi-edit-panel" hidden>
190
+ <section class="panel-head"><h4>다중 편집 <span id="multi-edit-count" class="mono"></span></h4></section>
191
+ <div class="me-block">
192
+ <div class="me-label">음높이</div>
193
+ <div class="me-row">
194
+ <button class="btn" data-proxy="btn-down" data-proxy-shift>&#9660; 옥타브</button>
195
+ <button class="btn" data-proxy="btn-down">&#9660; 반음</button>
196
+ <button class="btn" data-proxy="btn-up">반음 &#9650;</button>
197
+ <button class="btn" data-proxy="btn-up" data-proxy-shift>옥타브 &#9650;</button>
198
+ </div>
199
+ </div>
200
+ <div class="me-block">
201
+ <div class="me-label">길이</div>
202
+ <div class="me-row">
203
+ <button class="btn" data-proxy="btn-dur-whole">&#119133;</button>
204
+ <button class="btn" data-proxy="btn-dur-half">&#119134;</button>
205
+ <button class="btn" data-proxy="btn-dur-quarter">&#119135;</button>
206
+ <button class="btn" data-proxy="btn-dur-eighth">&#119136;</button>
207
+ <button class="btn" data-proxy="btn-dur-16th">&#119137;</button>
208
+ <button class="btn" data-proxy="btn-dur-32nd">&#119138;</button>
209
+ </div>
210
+ </div>
211
+ <div class="me-block">
212
+ <div class="me-label">임시표</div>
213
+ <div class="me-row">
214
+ <button class="btn" data-proxy="btn-dblsharp">&#119082;</button>
215
+ <button class="btn" data-proxy="btn-sharp">&#9839;</button>
216
+ <button class="btn" data-proxy="btn-natural">&#9838;</button>
217
+ <button class="btn" data-proxy="btn-flat">&#9837;</button>
218
+ <button class="btn" data-proxy="btn-dblflat">&#119083;</button>
219
+ </div>
220
+ </div>
221
+ <div class="me-block">
222
+ <button class="btn ghost" data-proxy="btn-rest-toggle" style="flex:1">음표 &#8596; 쉼표</button>
223
+ <button class="btn danger" data-proxy="btn-delete" style="flex:1">&#128465; 일괄 삭제</button>
224
+ </div>
225
+ </aside>
226
  </div>
227
 
228
  <!-- Chord Popup -->
 
239
 
240
  <!-- Status Bar -->
241
  <div id="status-bar">
242
+ <span id="status-mode"></span>
243
  <span id="status-selection" data-i18n="no_sel">선택 없음</span>
244
  <span id="status-total"></span>
245
+ <button id="omr-apply-btn" onclick="applyOmrEdits()" style="display:none">
246
+ Apply to OMR <span id="omr-apply-badge" style="display:none"></span>
247
+ </button>
248
  </div>
249
 
250
+ <script>
251
+ // Mode panel proxies — clicks the underlying button so corrector.js stays unaware.
252
+ document.addEventListener('DOMContentLoaded', () => {
253
+ document.querySelectorAll('[data-proxy]').forEach(el => {
254
+ const targetId = el.dataset.proxy;
255
+ if (!targetId) return;
256
+ const target = document.getElementById(targetId);
257
+ if (!target) return;
258
+ if (el.tagName === 'INPUT') {
259
+ el.addEventListener('change', () => { target.click(); });
260
+ } else {
261
+ el.addEventListener('click', () => { target.click(); });
262
+ }
263
+ });
264
+
265
+ // Barline mode: mirror .active onto body for marker dimming
266
+ const _btb = document.getElementById('barline-toolbar');
267
+ if (_btb) {
268
+ new MutationObserver(() => {
269
+ document.body.classList.toggle('mode-barline', _btb.classList.contains('active'));
270
+ }).observe(_btb, { attributes: true, attributeFilter: ['class'] });
271
+ }
272
+
273
+ // Multi-edit panel: show/hide based on multi-select count
274
+ const _mep = document.getElementById('multi-edit-panel');
275
+ const _mec = document.getElementById('multi-edit-count');
276
+ if (_mep) {
277
+ new MutationObserver(() => {
278
+ const n = document.querySelectorAll('#marker-svg .marker.multi-selected').length;
279
+ const show = n > 1;
280
+ _mep.toggleAttribute('hidden', !show);
281
+ if (show) _mec.textContent = '(' + n + ')';
282
+ }).observe(document.getElementById('marker-svg'), { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] });
283
+ }
284
+
285
+ // Shift-proxy for octave step buttons in multi-edit panel
286
+ document.querySelectorAll('[data-proxy][data-proxy-shift]').forEach(el => {
287
+ el.addEventListener('click', e => {
288
+ e.preventDefault(); e.stopPropagation();
289
+ const target = document.getElementById(el.dataset.proxy);
290
+ if (!target) return;
291
+ target.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, shiftKey: true }));
292
+ }, true);
293
+ });
294
+ });
295
+ </script>
296
  <script src="/static/corrector.js"></script>
297
  </body>
298
  </html>
static/corrector.js CHANGED
@@ -1510,21 +1510,36 @@ function matchOmrGrades(notes, omrData) {
1510
 
1511
  const _GLYPH_SHAPES = [
1512
  { cat: "Notes", cat_ko: "음표", items: [
1513
- { shape: "NOTEHEAD_BLACK", label: " Black", label_ko: " 검은 음표" },
1514
- { shape: "NOTEHEAD_VOID", label: " Void (half)", label_ko: " 빈 음표 (2분)" },
1515
- { shape: "WHOLE_NOTE", label: "𝅝 Whole", label_ko: "𝅝 온음표" },
1516
  ]},
1517
  { cat: "Rests", cat_ko: "쉼표", items: [
1518
  { shape: "QUARTER_REST", label: "𝄾 Quarter", label_ko: "𝄾 4분쉼표" },
1519
- { shape: "EIGHTH_REST", label: "𝄾 Eighth", label_ko: "𝄾 8분쉼표" },
1520
- { shape: "HALF_REST", label: "𝄻 Half", label_ko: "𝄻 2분쉼표" },
1521
- { shape: "WHOLE_REST", label: "𝄻 Whole", label_ko: "𝄻 온쉼표" },
1522
  ]},
1523
  { cat: "Accidentals", cat_ko: "임시표", items: [
1524
  { shape: "SHARP", label: "♯ Sharp", label_ko: "♯ 올림표" },
1525
  { shape: "FLAT", label: "♭ Flat", label_ko: "♭ 내림표" },
1526
  { shape: "NATURAL", label: "♮ Natural", label_ko: "♮ 제자리표" },
1527
  ]},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1528
  ];
1529
 
1530
  function toggleFreeGlyphs() {
@@ -1576,6 +1591,19 @@ function openGlyphAssignPopup(fgIdx, event) {
1576
 
1577
  const g = freeGlyphData[fgIdx];
1578
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1579
  const isKo = currentLang === "ko";
1580
  for (const cat of _GLYPH_SHAPES) {
1581
  const catLi = document.createElement("li");
@@ -3153,8 +3181,8 @@ function renderMarkers(notes) {
3153
  // Accidental label
3154
  if (n.alter !== 0) {
3155
  const txt = document.createElementNS(SVG_NS, "text");
3156
- txt.setAttribute("x", n.px + 22);
3157
- txt.setAttribute("y", n.py + 20);
3158
  txt.classList.add("acc-label");
3159
  txt.dataset.idx = idx;
3160
  txt.textContent = alterStr(n.alter);
@@ -3450,8 +3478,8 @@ function updateSingleMarker(idx) {
3450
  accLabel.dataset.idx = idx;
3451
  markerSvg.appendChild(accLabel);
3452
  }
3453
- accLabel.setAttribute("x", n.px + 12);
3454
- accLabel.setAttribute("y", n.py + 7);
3455
  accLabel.textContent = accStr;
3456
  } else if (accLabel) {
3457
  accLabel.remove();
@@ -3897,6 +3925,7 @@ function raiseNote(skipUndo) {
3897
  newPitch: stepOctaveToOmrPitch(n.step, n.octave, n.clef.sign) });
3898
  }
3899
  recomputeAndUpdate(selectedIdx);
 
3900
  }
3901
 
3902
  function lowerNote(skipUndo) {
@@ -3930,6 +3959,7 @@ function lowerNote(skipUndo) {
3930
  newPitch: stepOctaveToOmrPitch(n.step, n.octave, n.clef.sign) });
3931
  }
3932
  recomputeAndUpdate(selectedIdx);
 
3933
  }
3934
 
3935
  function setAccidental(alterValue, skipUndo) {
@@ -3965,6 +3995,7 @@ function setAccidental(alterValue, skipUndo) {
3965
  }
3966
  }
3967
  recomputeAndUpdate(selectedIdx);
 
3968
  }
3969
 
3970
  // ── Note Deletion ────────────────────────────────────────────
@@ -6424,6 +6455,16 @@ async function loadPage(pageIdx) {
6424
  markerSvg.setAttribute("width", scoreImage.naturalWidth);
6425
  markerSvg.setAttribute("height", scoreImage.naturalHeight);
6426
 
 
 
 
 
 
 
 
 
 
 
6427
  renderMarkers(noteInfos);
6428
  if (selectedIdx >= 0 && selectedIdx < noteInfos.length) selectNote(selectedIdx);
6429
  else { selectedIdx = -1; statusSel.textContent = t("no_sel"); }
@@ -6473,6 +6514,16 @@ async function loadPage(pageIdx) {
6473
  markerSvg.setAttribute("width", scoreImage.naturalWidth);
6474
  markerSvg.setAttribute("height", scoreImage.naturalHeight);
6475
 
 
 
 
 
 
 
 
 
 
 
6476
  // Parse XML — from pre-loaded text or from file
6477
  let xmlText;
6478
  if (pg.xmlText) {
@@ -7186,6 +7237,13 @@ let isPlaying = false;
7186
  let playbackStartOffset = 0; // seconds offset for seek
7187
  let cursorSeekTime = 0; // time in seconds where the red cursor is parked
7188
 
 
 
 
 
 
 
 
7189
  function noteToFreq(step, octave, alter) {
7190
  const midi = (octave + 1) * 12 + [0,2,4,5,7,9,11][STEP_INDEX[step]] + alter;
7191
  return 440 * Math.pow(2, (midi - 69) / 12);
@@ -8416,6 +8474,7 @@ document.addEventListener("mouseup", () => {
8416
  recordOmrEdit({ type: "change_pitch", headId: n.omrHeadId,
8417
  newPitch: stepOctaveToOmrPitch(n.step, n.octave, n.clef.sign) });
8418
  }
 
8419
  }
8420
  if (dragIsXMode) {
8421
  document.body.style.cursor = "";
@@ -8448,7 +8507,7 @@ function recomputeWithAnchorsRealtime() {
8448
  }
8449
  const accLabel = markerSvg.querySelector(`text.acc-label[data-idx="${idx}"]`);
8450
  if (accLabel) {
8451
- accLabel.setAttribute("x", n.px + 12);
8452
  }
8453
  });
8454
  }
@@ -8786,6 +8845,27 @@ document.addEventListener("mousemove", (e) => {
8786
 
8787
  document.addEventListener("mouseup", (e) => {
8788
  if (staffDragIdx < 0 || !staffAdjustMode) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8789
  staffDragIdx = -1;
8790
  staffDragOrigData = null;
8791
  staffDragDebugLines = [];
@@ -9682,7 +9762,7 @@ document.addEventListener("mousemove", (e) => {
9682
  const circle = markerSvg.querySelector(`circle.marker[data-idx="${idx}"]`);
9683
  if (circle) circle.setAttribute("cx", n.px);
9684
  const accLabel = markerSvg.querySelector(`text.acc-label[data-idx="${idx}"]`);
9685
- if (accLabel) accLabel.setAttribute("x", n.px + 12);
9686
  });
9687
  return;
9688
  }
 
1510
 
1511
  const _GLYPH_SHAPES = [
1512
  { cat: "Notes", cat_ko: "음표", items: [
1513
+ { shape: "NOTEHEAD_BLACK", label: " Black (filled)", label_ko: " 검은 음표머리" },
1514
+ { shape: "NOTEHEAD_VOID", label: " Void (half)", label_ko: " 빈 음표머리 (2분)" },
1515
+ { shape: "WHOLE_NOTE", label: " Whole", label_ko: " 온음표" },
1516
  ]},
1517
  { cat: "Rests", cat_ko: "쉼표", items: [
1518
  { shape: "QUARTER_REST", label: "𝄾 Quarter", label_ko: "𝄾 4분쉼표" },
1519
+ { shape: "EIGHTH_REST", label: "𝄿 Eighth", label_ko: "𝄿 8분쉼표" },
1520
+ { shape: "HALF_REST", label: " Half", label_ko: " 2분쉼표" },
1521
+ { shape: "WHOLE_REST", label: " Whole", label_ko: " 온쉼표" },
1522
  ]},
1523
  { cat: "Accidentals", cat_ko: "임시표", items: [
1524
  { shape: "SHARP", label: "♯ Sharp", label_ko: "♯ 올림표" },
1525
  { shape: "FLAT", label: "♭ Flat", label_ko: "♭ 내림표" },
1526
  { shape: "NATURAL", label: "♮ Natural", label_ko: "♮ 제자리표" },
1527
  ]},
1528
+ // { cat: "Clefs", cat_ko: "음자리표", items: [
1529
+ // { shape: "G_CLEF", label: "𝄞 Treble", label_ko: "𝄞 높은음자리표" },
1530
+ // { shape: "F_CLEF", label: "𝄢 Bass", label_ko: "𝄢 낮은음자리표" },
1531
+ // { shape: "C_CLEF", label: "𝄡 Alto/Tenor", label_ko: "𝄡 가온음자리표" },
1532
+ // ]},
1533
+ // { cat: "Dynamics", cat_ko: "셈여림", items: [
1534
+ // { shape: "DYNAMICS_F", label: "f Forte", label_ko: "f 포르테" },
1535
+ // { shape: "DYNAMICS_P", label: "p Piano", label_ko: "p 피아노" },
1536
+ // ]},
1537
+ // { cat: "Other", cat_ko: "기타", items: [
1538
+ // { shape: "FERMATA", label: "𝄐 Fermata", label_ko: "𝄐 늘임표" },
1539
+ // { shape: "FLAG_8TH", label: "⚑ 8th flag", label_ko: "⚑ 8분 꼬리" },
1540
+ // { shape: "FLAG_16TH", label: "⚑⚑ 16th flag", label_ko: "⚑⚑ 16분 꼬리" },
1541
+ // { shape: "DOT", label: "• Dot", label_ko: "• 점" },
1542
+ // ]},
1543
  ];
1544
 
1545
  function toggleFreeGlyphs() {
 
1591
 
1592
  const g = freeGlyphData[fgIdx];
1593
 
1594
+ // Navigate timeline to the measure containing this glyph
1595
+ const glyphCx = g.x + g.w / 2;
1596
+ let closestIdx = -1, closestDist = Infinity;
1597
+ noteInfos.forEach((n, i) => {
1598
+ if (n.systemIdx !== g.systemIdx) return;
1599
+ const d = Math.abs(n.px - glyphCx);
1600
+ if (d < closestDist) { closestDist = d; closestIdx = i; }
1601
+ });
1602
+ if (closestIdx >= 0) {
1603
+ const n = noteInfos[closestIdx];
1604
+ renderTimelinePanel(n.measureNum, n.systemIdx);
1605
+ }
1606
+
1607
  const isKo = currentLang === "ko";
1608
  for (const cat of _GLYPH_SHAPES) {
1609
  const catLi = document.createElement("li");
 
3181
  // Accidental label
3182
  if (n.alter !== 0) {
3183
  const txt = document.createElementNS(SVG_NS, "text");
3184
+ txt.setAttribute("x", n.px + 8);
3185
+ txt.setAttribute("y", n.py + 5);
3186
  txt.classList.add("acc-label");
3187
  txt.dataset.idx = idx;
3188
  txt.textContent = alterStr(n.alter);
 
3478
  accLabel.dataset.idx = idx;
3479
  markerSvg.appendChild(accLabel);
3480
  }
3481
+ accLabel.setAttribute("x", n.px + 8);
3482
+ accLabel.setAttribute("y", n.py + 5);
3483
  accLabel.textContent = accStr;
3484
  } else if (accLabel) {
3485
  accLabel.remove();
 
3925
  newPitch: stepOctaveToOmrPitch(n.step, n.octave, n.clef.sign) });
3926
  }
3927
  recomputeAndUpdate(selectedIdx);
3928
+ _previewNote(n);
3929
  }
3930
 
3931
  function lowerNote(skipUndo) {
 
3959
  newPitch: stepOctaveToOmrPitch(n.step, n.octave, n.clef.sign) });
3960
  }
3961
  recomputeAndUpdate(selectedIdx);
3962
+ _previewNote(n);
3963
  }
3964
 
3965
  function setAccidental(alterValue, skipUndo) {
 
3995
  }
3996
  }
3997
  recomputeAndUpdate(selectedIdx);
3998
+ _previewNote(n);
3999
  }
4000
 
4001
  // ── Note Deletion ────────────────────────────────────────────
 
6455
  markerSvg.setAttribute("width", scoreImage.naturalWidth);
6456
  markerSvg.setAttribute("height", scoreImage.naturalHeight);
6457
 
6458
+ // Auto-fit zoom to canvas width on first load
6459
+ if (!pg._zoomApplied) {
6460
+ const wrapper = document.getElementById("canvas-wrapper");
6461
+ const fitZoom = Math.floor((wrapper.clientWidth / scoreImage.naturalWidth) * 100);
6462
+ const clampedZoom = Math.max(25, Math.min(200, fitZoom));
6463
+ zoomSlider.value = clampedZoom;
6464
+ applyZoom(clampedZoom);
6465
+ pg._zoomApplied = true;
6466
+ }
6467
+
6468
  renderMarkers(noteInfos);
6469
  if (selectedIdx >= 0 && selectedIdx < noteInfos.length) selectNote(selectedIdx);
6470
  else { selectedIdx = -1; statusSel.textContent = t("no_sel"); }
 
6514
  markerSvg.setAttribute("width", scoreImage.naturalWidth);
6515
  markerSvg.setAttribute("height", scoreImage.naturalHeight);
6516
 
6517
+ // Auto-fit zoom to canvas width on first load
6518
+ if (!pg._zoomApplied) {
6519
+ const wrapper = document.getElementById("canvas-wrapper");
6520
+ const fitZoom = Math.floor((wrapper.clientWidth / scoreImage.naturalWidth) * 100);
6521
+ const clampedZoom = Math.max(25, Math.min(200, fitZoom));
6522
+ zoomSlider.value = clampedZoom;
6523
+ applyZoom(clampedZoom);
6524
+ pg._zoomApplied = true;
6525
+ }
6526
+
6527
  // Parse XML — from pre-loaded text or from file
6528
  let xmlText;
6529
  if (pg.xmlText) {
 
7237
  let playbackStartOffset = 0; // seconds offset for seek
7238
  let cursorSeekTime = 0; // time in seconds where the red cursor is parked
7239
 
7240
+ function _previewNote(n) {
7241
+ if (!n || n.isRest) return;
7242
+ if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
7243
+ const freq = noteToFreq(n.step, n.octave, n.alter);
7244
+ playNoteSound(freq, 0.3, 0, n.step, n.octave, n.alter);
7245
+ }
7246
+
7247
  function noteToFreq(step, octave, alter) {
7248
  const midi = (octave + 1) * 12 + [0,2,4,5,7,9,11][STEP_INDEX[step]] + alter;
7249
  return 440 * Math.pow(2, (midi - 69) / 12);
 
8474
  recordOmrEdit({ type: "change_pitch", headId: n.omrHeadId,
8475
  newPitch: stepOctaveToOmrPitch(n.step, n.octave, n.clef.sign) });
8476
  }
8477
+ _previewNote(n);
8478
  }
8479
  if (dragIsXMode) {
8480
  document.body.style.cursor = "";
 
8507
  }
8508
  const accLabel = markerSvg.querySelector(`text.acc-label[data-idx="${idx}"]`);
8509
  if (accLabel) {
8510
+ accLabel.setAttribute("x", n.px + 8);
8511
  }
8512
  });
8513
  }
 
8845
 
8846
  document.addEventListener("mouseup", (e) => {
8847
  if (staffDragIdx < 0 || !staffAdjustMode) return;
8848
+
8849
+ // Compute delta before clearing state — needed to update omrY/omrX
8850
+ const dy = (e.clientY - staffDragStartY) / currentZoom;
8851
+ const dx = (e.clientX - staffDragStartX) / currentZoom;
8852
+ const wasShift = staffDragShift;
8853
+ const draggedIdx = staffDragIdx;
8854
+ const numSPS = (systemsData.length > 0) ? systemsData[0].numStaves : 1;
8855
+ const dragSysIdx = Math.floor(draggedIdx / numSPS);
8856
+ const dragStaffInSys = (draggedIdx % numSPS) + 1;
8857
+
8858
+ // Update omrY/omrX for notes on the dragged staff so they don't snap back
8859
+ noteInfos.forEach(n => {
8860
+ if (n.systemIdx === dragSysIdx && n.staff === dragStaffInSys) {
8861
+ if (wasShift) {
8862
+ if (n.omrX != null) n.omrX += dx;
8863
+ } else {
8864
+ if (n.omrY != null) n.omrY += dy;
8865
+ }
8866
+ }
8867
+ });
8868
+
8869
  staffDragIdx = -1;
8870
  staffDragOrigData = null;
8871
  staffDragDebugLines = [];
 
9762
  const circle = markerSvg.querySelector(`circle.marker[data-idx="${idx}"]`);
9763
  if (circle) circle.setAttribute("cx", n.px);
9764
  const accLabel = markerSvg.querySelector(`text.acc-label[data-idx="${idx}"]`);
9765
+ if (accLabel) accLabel.setAttribute("x", n.px + 8);
9766
  });
9767
  return;
9768
  }
static/tokens.css CHANGED
@@ -1,75 +1,76 @@
1
  /* ============================================================
2
- Score → MML · Design Tokens (drop-in)
3
  ------------------------------------------------------------
4
- Apply ONCE at the top of static/index.html and static/corrector.html
5
- (or extract to /static/tokens.css and link it before the file's own CSS).
6
-
7
- These tokens replace the existing dark-navy + hot-pink palette
8
- (#1a1a2e / #16213e / #0f3460 / #e94560) with a refined
9
- slate + amber system. Voice colors are tuned to be friendlier
10
- beside the grade-confidence colors (which previously fought
11
- the #e94560 pink).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  ============================================================ */
13
 
14
- /* Fonts ------------------------------------------------------ */
15
  @import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css");
16
  @import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap");
17
 
18
  :root{
19
- /* Surfaces (dark, neutral slate — replaces #1a1a2e/#16213e/#0f3460) */
20
- --bg-0: #0a0c10; /* canvas surround / behind score paper */
21
- --bg-1: #0e1118; /* app shell */
22
- --bg-2: #141822; /* panels, cards */
23
- --bg-3: #1b2030; /* raised: toolbar buttons */
24
- --bg-4: #232a3c; /* button hover */
25
  --line: #262c3a;
26
  --line-strong: #323a4f;
27
 
28
- /* Ink (text) */
29
- --ink-0: #eef1f7; /* primary */
30
- --ink-1: #b9c0cf; /* secondary */
31
- --ink-2: #7f8898; /* tertiary / labels */
32
- --ink-3: #535b6c; /* muted / disabled */
33
 
34
- /* SINGLE accent — actions only. Replaces #e94560 everywhere. */
35
- --accent: #f3a25b; /* warm amber */
36
  --accent-dim: #a87246;
37
  --accent-soft: rgba(243,162,91,.14);
38
 
39
- /* Selection (two-tier: single = gold, multi = red) */
40
- --sel-single: #ffd86b; /* was #ffd700 — slightly warmer */
41
- --sel-multi: #ff5d5d; /* was #e02020 — same intent, less saturated */
42
-
43
- /* Grade / confidence — kept semantically distinct from accent */
44
- --grade-low: #ff5252; /* matches sel-multi family */
45
- --grade-mid: #ffae3a;
46
- --grade-ok: #7cd454;
47
-
48
- /* Voice palette — tuned for parity in saturation/luminance */
49
- --v1: #6aa3ff; /* was #4488ff — softer */
50
- --v2: #ff6c7a; /* was #ff4444 — distinct from grade-low */
51
- --v3: #6dd17e; /* was #44aa44 — distinct from grade-ok */
52
- --v4: #ffc24a; /* was #ffaa00 — distinct from grade-mid */
53
-
54
- /* Barline edit overlays */
55
- --bl-high: #7cd454;
56
- --bl-medium: #ffae3a;
57
- --bl-low: #d9882e;
58
- --bl-manual: #80b7ff;
59
- --bl-implicit: #cc66cc;
60
- --bl-selected: #5a99ff;
61
 
62
  /* Type */
63
  --font-sans: "Pretendard Variable", Pretendard, ui-sans-serif, sans-serif;
64
  --font-mono: "JetBrains Mono", ui-monospace, monospace;
65
 
66
- /* Type scale — replaces the existing 24px-everywhere problem */
67
- --fs-display: 22px; /* page headers */
68
- --fs-title: 16px; /* section titles */
69
- --fs-body: 13px; /* default UI */
70
- --fs-label: 12px; /* form labels, toolbar buttons */
71
- --fs-meta: 11.5px; /* status bar, helper text */
72
- --fs-kbd: 10.5px; /* keyboard hints */
73
 
74
  /* Spacing */
75
  --space-1: 4px;
@@ -92,7 +93,7 @@
92
  }
93
 
94
  /* ============================================================
95
- GLOBAL DEFAULTS (applies once tokens.css is linked)
96
  ============================================================ */
97
 
98
  html, body {
@@ -104,17 +105,15 @@ html, body {
104
  text-rendering: geometricPrecision;
105
  }
106
 
107
- /* Tabular numerics on every mono span */
108
  .mono, code, kbd, .kbd-chip {
109
  font-family: var(--font-mono);
110
  font-variant-numeric: tabular-nums;
111
  }
112
 
113
  /* ============================================================
114
- COMPONENT PRIMITIVES — copy these once and reuse
115
  ============================================================ */
116
 
117
- /* Button */
118
  .btn{
119
  display:inline-flex; align-items:center; gap:6px;
120
  height:30px; padding:0 12px; border-radius: var(--r-md);
@@ -137,13 +136,19 @@ html, body {
137
  .btn.ghost { background: transparent; border-color: var(--line); }
138
  .btn.ghost:hover { background: var(--bg-3); }
139
 
 
 
 
 
 
 
 
140
  .btn.danger {
141
  background: rgba(255,93,93,.12);
142
  border-color: rgba(255,93,93,.35);
143
  color: #ffb3b3;
144
  }
145
 
146
- /* Icon button */
147
  .icon-btn{
148
  width:30px; height:30px; display:inline-flex; align-items:center; justify-content:center;
149
  background:transparent; border:1px solid transparent;
@@ -151,7 +156,6 @@ html, body {
151
  }
152
  .icon-btn:hover { background: var(--bg-3); color: var(--ink-0); }
153
 
154
- /* Keyboard chip */
155
  .kbd-chip{
156
  display:inline-block;
157
  font-family: var(--font-mono); font-size: var(--fs-kbd);
@@ -160,16 +164,15 @@ html, body {
160
  line-height: 1.4;
161
  }
162
 
163
- /* Pill (status) */
164
  .pill{
165
  display:inline-flex; align-items:center; gap:6px;
166
  padding: 5px 10px; border-radius: 999px;
167
  background: var(--bg-2); border:1px solid var(--line);
168
  color: var(--ink-1); font-size: var(--fs-label);
169
  }
170
- .pill .dot{ width:6px; height:6px; border-radius:50%; background: var(--grade-ok); }
171
 
172
- /* Top bar / app shell */
173
  .app-topbar{
174
  height:48px; display:flex; align-items:center; gap:14px;
175
  padding: 0 var(--space-4);
@@ -177,8 +180,6 @@ html, body {
177
  border-bottom: 1px solid var(--line);
178
  flex-shrink: 0;
179
  }
180
-
181
- /* Status bar */
182
  .app-statusbar{
183
  height:30px; display:flex; align-items:center; gap:18px;
184
  padding: 0 var(--space-4);
@@ -189,12 +190,10 @@ html, body {
189
  }
190
  .app-statusbar .sep { width:1px; height:14px; background: var(--line); }
191
 
192
- /* Tabs (Convert / Corrector) */
193
  .tabs{ display:flex; gap:2px; background: var(--bg-2); padding:3px; border-radius: 8px; border:1px solid var(--line); }
194
  .tabs .tab{ padding: 5px 12px; border-radius: 6px; font-size: 12.5px; color: var(--ink-2); cursor: pointer; }
195
  .tabs .tab.is-active{ background: var(--bg-3); color: var(--ink-0); box-shadow: inset 0 0 0 1px var(--line-strong); }
196
 
197
- /* Panel header */
198
  .panel-head{
199
  display:flex; align-items:center; gap:8px;
200
  padding: 10px 14px;
@@ -206,7 +205,6 @@ html, body {
206
  text-transform: uppercase; color: var(--ink-2); margin: 0;
207
  }
208
 
209
- /* Mode pill (shows current corrector mode) */
210
  .mode-pill{
211
  padding: 3px 10px; border-radius: 999px;
212
  font-size: 11px; font-weight: 700; letter-spacing: .06em;
@@ -216,7 +214,6 @@ html, body {
216
  .mode-pill.add{ background: rgba(106,163,255,.16); color: #cfe2ff; border-color: rgba(106,163,255,.4); }
217
  .mode-pill.glyph{ background: rgba(255,153,51,.16); color: #ffcc88; border-color: rgba(255,153,51,.4); }
218
 
219
- /* Slider */
220
  .slider{
221
  appearance: none; height: 4px; border-radius: 99px;
222
  background: var(--bg-3); outline: none; width: 120px;
@@ -226,8 +223,17 @@ html, body {
226
  background: var(--accent); border: 2px solid #1a1004; cursor: pointer;
227
  }
228
 
229
- /* Number/text input */
230
- input[type="number"], input[type="text"], select{
 
 
 
 
 
 
 
 
 
231
  background: #0a0d14;
232
  border: 1px solid var(--line);
233
  border-radius: 5px;
@@ -236,70 +242,11 @@ input[type="number"], input[type="text"], select{
236
  font-family: var(--font-mono);
237
  font-size: 11.5px;
238
  }
239
- input:focus, select:focus{
 
 
 
240
  outline: none;
241
  border-color: var(--accent-dim);
242
  box-shadow: 0 0 0 2px var(--accent-soft);
243
  }
244
-
245
- /* ============================================================
246
- MARKER OVERLAY (score notes) — REPLACES current .marker rules
247
- ============================================================ */
248
-
249
- .marker{ pointer-events: all; cursor: pointer; transition: r .1s, stroke-width .1s; }
250
- .marker:hover{ r: 10; }
251
-
252
- /* Voice fill */
253
- .marker.voice1{ fill: rgba(106,163,255,.78); stroke: #4a7adc; stroke-width: 1; }
254
- .marker.voice2{ fill: rgba(255,108,122,.78); stroke: #d44352; stroke-width: 1; }
255
- .marker.voice3{ fill: rgba(109,209,126,.78); stroke: #4ca85f; stroke-width: 1; }
256
- .marker.voice4{ fill: rgba(255,194,74,.78); stroke: #d49832; stroke-width: 1; }
257
- .marker.rest-marker{ fill: rgba(120,128,144,.4); stroke: #5a6273; stroke-width: 1; r: 6; }
258
-
259
- /* Grade confidence */
260
- .marker.grade-low{ fill: rgba(255,80,80,.85); stroke: var(--grade-low); stroke-width: 2;
261
- filter: drop-shadow(0 0 4px rgba(255,82,82,.6)); }
262
- .marker.grade-mid{ fill: rgba(255,174,58,.85); stroke: var(--grade-mid); stroke-width: 1.5; }
263
- .marker.grade-ok{ fill: rgba(124,212,84,.7); stroke: var(--grade-ok); stroke-width: 1; }
264
-
265
- /* Selection (preserve worklog #8 — multi must beat voice colors) */
266
- .marker.selected{
267
- stroke: var(--sel-single); stroke-width: 4; r: 12;
268
- filter: drop-shadow(0 0 8px var(--sel-single)) drop-shadow(0 0 16px rgba(255,216,107,.5));
269
- }
270
- .marker.multi-selected{
271
- stroke: var(--sel-multi) !important; stroke-width: 4 !important; r: 11;
272
- filter: drop-shadow(0 0 4px rgba(255,93,93,.8)) !important;
273
- }
274
- .marker.multi-selected.selected{
275
- stroke: var(--sel-multi) !important; stroke-width: 5 !important; r: 12;
276
- filter: drop-shadow(0 0 6px rgba(255,93,93,.9)) !important;
277
- }
278
-
279
- .marker.modified{ stroke: var(--grade-ok); stroke-width: 2.5;
280
- filter: drop-shadow(0 0 3px var(--grade-ok)); }
281
- .marker.modified.selected{ stroke: var(--sel-single); stroke-width: 3;
282
- filter: drop-shadow(0 0 4px var(--grade-ok)); }
283
-
284
- .marker.playback-highlight{ stroke: var(--grade-low); stroke-width: 3; r: 11;
285
- fill: rgba(255,80,80,.9); }
286
-
287
- /* Rubber-band selection */
288
- .score-rubber{
289
- border: 2px dashed var(--sel-multi);
290
- background: rgba(255,93,93,.08);
291
- pointer-events: none; z-index: 100;
292
- }
293
- .tl-rubber{
294
- border: 2px dashed var(--sel-single);
295
- background: rgba(255,216,107,.10);
296
- pointer-events: none; z-index: 100;
297
- }
298
-
299
- /* Barline overlays */
300
- .barline-overlay.high { stroke: var(--bl-high); stroke-width: 1.5; opacity: .55; }
301
- .barline-overlay.medium { stroke: var(--bl-medium); stroke-width: 1.5; opacity: .55; }
302
- .barline-overlay.low { stroke: var(--bl-low); stroke-width: 1.5; opacity: .55; stroke-dasharray: 4 4; }
303
- .barline-overlay.manual { stroke: var(--bl-manual); stroke-width: 2; opacity: .75; }
304
- .barline-overlay.implicit { stroke: var(--bl-implicit); stroke-width: 2; opacity: .65; stroke-dasharray: 6 3; }
305
- .barline-overlay.bl-selected { stroke: var(--bl-selected); stroke-width: 3; opacity: 1; }
 
1
  /* ============================================================
2
+ Score → MML · Design Tokens · v3 (2026-05-17)
3
  ------------------------------------------------------------
4
+ STATUS: this file is ALREADY linked from corrector.html as the
5
+ FIRST stylesheet (before corrector.css).
6
+
7
+ PHILOSOPHY (v3):
8
+ "Calm chrome, working workspace."
9
+
10
+ - Chrome (topbar, toolbar, panels, status, popups' wrapper):
11
+ slate neutrals + ONE amber action accent. Restrained.
12
+ - Score canvas overlays (markers, pulse, accidental labels,
13
+ voice colours, grade colours, rubber-band): UNCHANGED.
14
+ These are user-tuned & functional, not decorative.
15
+ - Timeline: compact (24px lanes), DAW-style voice gradients
16
+ retained, but pink chrome (border-left, title glow) dropped.
17
+ - Popups' chrome (chord neutral, glyph orange) keeps its
18
+ semantic colour but in token form.
19
+
20
+ This file owns:
21
+ :root tokens
22
+ html/body defaults
23
+ .btn .icon-btn .kbd-chip .pill .tabs .panel-head .mode-pill
24
+ .app-topbar .app-statusbar .slider input
25
+
26
+ This file does NOT redefine any .marker, .barline-overlay,
27
+ .free-glyph-box, .score-rubber, .tl-rubber, .tl-note, .acc-label,
28
+ .ghost-marker, .ghost-label, .rhythm-warning, .ghost-barline,
29
+ .measure-highlight, .barline-label, .debug-line, .staff-overlay,
30
+ .playback-cursor, #chord-popup, #glyph-popup rule.
31
+ Those live in corrector.css and stay there. See PRESERVED_VALUES.md.
32
  ============================================================ */
33
 
 
34
  @import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css");
35
  @import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap");
36
 
37
  :root{
38
+ /* Surfaces */
39
+ --bg-0: #0a0c10; /* canvas surround */
40
+ --bg-1: #0e1118; /* app shell */
41
+ --bg-2: #141822; /* panels */
42
+ --bg-3: #1b2030; /* raised: buttons, inputs */
43
+ --bg-4: #232a3c; /* button hover */
44
  --line: #262c3a;
45
  --line-strong: #323a4f;
46
 
47
+ /* Ink */
48
+ --ink-0: #eef1f7;
49
+ --ink-1: #b9c0cf;
50
+ --ink-2: #7f8898;
51
+ --ink-3: #535b6c;
52
 
53
+ /* Accent (action only) */
54
+ --accent: #f3a25b;
55
  --accent-dim: #a87246;
56
  --accent-soft: rgba(243,162,91,.14);
57
 
58
+ /* Status semantic */
59
+ --ok: #7cd454;
60
+ --warn: #ffae3a;
61
+ --danger:#ff5d5d;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
  /* Type */
64
  --font-sans: "Pretendard Variable", Pretendard, ui-sans-serif, sans-serif;
65
  --font-mono: "JetBrains Mono", ui-monospace, monospace;
66
 
67
+ /* Type scale (UI chrome only does NOT apply to .acc-label or popup contents) */
68
+ --fs-display: 18px; /* topbar brand, page title */
69
+ --fs-title: 14px; /* section headers, panel heads */
70
+ --fs-body: 13px; /* default UI */
71
+ --fs-label: 12px; /* form labels, toolbar buttons */
72
+ --fs-meta: 11.5px; /* status bar, helper text */
73
+ --fs-kbd: 10.5px; /* keyboard hints */
74
 
75
  /* Spacing */
76
  --space-1: 4px;
 
93
  }
94
 
95
  /* ============================================================
96
+ GLOBAL DEFAULTS
97
  ============================================================ */
98
 
99
  html, body {
 
105
  text-rendering: geometricPrecision;
106
  }
107
 
 
108
  .mono, code, kbd, .kbd-chip {
109
  font-family: var(--font-mono);
110
  font-variant-numeric: tabular-nums;
111
  }
112
 
113
  /* ============================================================
114
+ COMPONENT PRIMITIVES
115
  ============================================================ */
116
 
 
117
  .btn{
118
  display:inline-flex; align-items:center; gap:6px;
119
  height:30px; padding:0 12px; border-radius: var(--r-md);
 
136
  .btn.ghost { background: transparent; border-color: var(--line); }
137
  .btn.ghost:hover { background: var(--bg-3); }
138
 
139
+ .btn.success { /* for #btn-download (retokened green) */
140
+ background: rgba(124,212,84,.12);
141
+ border-color: rgba(124,212,84,.4);
142
+ color: #b6e4a4;
143
+ }
144
+ .btn.success:hover { background: rgba(124,212,84,.2); }
145
+
146
  .btn.danger {
147
  background: rgba(255,93,93,.12);
148
  border-color: rgba(255,93,93,.35);
149
  color: #ffb3b3;
150
  }
151
 
 
152
  .icon-btn{
153
  width:30px; height:30px; display:inline-flex; align-items:center; justify-content:center;
154
  background:transparent; border:1px solid transparent;
 
156
  }
157
  .icon-btn:hover { background: var(--bg-3); color: var(--ink-0); }
158
 
 
159
  .kbd-chip{
160
  display:inline-block;
161
  font-family: var(--font-mono); font-size: var(--fs-kbd);
 
164
  line-height: 1.4;
165
  }
166
 
 
167
  .pill{
168
  display:inline-flex; align-items:center; gap:6px;
169
  padding: 5px 10px; border-radius: 999px;
170
  background: var(--bg-2); border:1px solid var(--line);
171
  color: var(--ink-1); font-size: var(--fs-label);
172
  }
173
+ .pill .dot{ width:6px; height:6px; border-radius:50%; background: var(--ok); }
174
 
175
+ /* App shell */
176
  .app-topbar{
177
  height:48px; display:flex; align-items:center; gap:14px;
178
  padding: 0 var(--space-4);
 
180
  border-bottom: 1px solid var(--line);
181
  flex-shrink: 0;
182
  }
 
 
183
  .app-statusbar{
184
  height:30px; display:flex; align-items:center; gap:18px;
185
  padding: 0 var(--space-4);
 
190
  }
191
  .app-statusbar .sep { width:1px; height:14px; background: var(--line); }
192
 
 
193
  .tabs{ display:flex; gap:2px; background: var(--bg-2); padding:3px; border-radius: 8px; border:1px solid var(--line); }
194
  .tabs .tab{ padding: 5px 12px; border-radius: 6px; font-size: 12.5px; color: var(--ink-2); cursor: pointer; }
195
  .tabs .tab.is-active{ background: var(--bg-3); color: var(--ink-0); box-shadow: inset 0 0 0 1px var(--line-strong); }
196
 
 
197
  .panel-head{
198
  display:flex; align-items:center; gap:8px;
199
  padding: 10px 14px;
 
205
  text-transform: uppercase; color: var(--ink-2); margin: 0;
206
  }
207
 
 
208
  .mode-pill{
209
  padding: 3px 10px; border-radius: 999px;
210
  font-size: 11px; font-weight: 700; letter-spacing: .06em;
 
214
  .mode-pill.add{ background: rgba(106,163,255,.16); color: #cfe2ff; border-color: rgba(106,163,255,.4); }
215
  .mode-pill.glyph{ background: rgba(255,153,51,.16); color: #ffcc88; border-color: rgba(255,153,51,.4); }
216
 
 
217
  .slider{
218
  appearance: none; height: 4px; border-radius: 99px;
219
  background: var(--bg-3); outline: none; width: 120px;
 
223
  background: var(--accent); border: 2px solid #1a1004; cursor: pointer;
224
  }
225
 
226
+ /* Inputs scope to chrome (not popups; corrector.js sets inline styles) */
227
+ .app-topbar input[type="number"],
228
+ .app-topbar input[type="text"],
229
+ .app-topbar select,
230
+ #mode-panel input[type="number"],
231
+ #mode-panel input[type="text"],
232
+ #mode-panel select,
233
+ #toolbar input[type="number"],
234
+ #toolbar input[type="range"],
235
+ #upload-bar input[type="number"],
236
+ #upload-bar input[type="file"]{
237
  background: #0a0d14;
238
  border: 1px solid var(--line);
239
  border-radius: 5px;
 
242
  font-family: var(--font-mono);
243
  font-size: 11.5px;
244
  }
245
+ .app-topbar input:focus,
246
+ #mode-panel input:focus,
247
+ #toolbar input:focus,
248
+ #upload-bar input:focus{
249
  outline: none;
250
  border-color: var(--accent-dim);
251
  box-shadow: 0 0 0 2px var(--accent-soft);
252
  }