@graphty/visual-review 0.2.0 → 0.2.2

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.
@@ -8,6 +8,8 @@
8
8
  --ok: #1d7a3a;
9
9
  --bad: #b3261e;
10
10
  --warn: #8a5a00;
11
+ --wait: #f0f3f7;
12
+ --on-accent: #ffffff;
11
13
  color-scheme: light dark;
12
14
  }
13
15
 
@@ -22,6 +24,9 @@
22
24
  --ok: #5cc27d;
23
25
  --bad: #ff7a70;
24
26
  --warn: #e0b04c;
27
+ --wait: #262a30;
28
+ /* Dark text on the light blue: white on it is 2.5:1, this is over 7:1. */
29
+ --on-accent: #16181c;
25
30
  }
26
31
  }
27
32
 
@@ -29,15 +34,27 @@
29
34
  box-sizing: border-box;
30
35
  }
31
36
 
32
- /* The page is one screen: the header on top, and main scrolling under it. */
37
+ /*
38
+ * The page is one screen that never scrolls: the sticky stack (header, the screen's own bar, the
39
+ * status row) on top, and main under it. On the story screen only the panes scroll, so nothing
40
+ * (iPad Safari's on-screen keyboard included) can push the decision buttons off the screen.
41
+ */
42
+ html,
43
+ body {
44
+ height: 100%;
45
+ overflow: hidden;
46
+ }
47
+
33
48
  body {
34
49
  display: flex;
35
50
  flex-direction: column;
36
51
  height: 100vh;
37
52
  height: 100dvh;
38
53
  margin: 0;
54
+ padding: 0 env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
55
+ /* In rem, so the browser's own text size setting scales the page. */
39
56
  font:
40
- 14px/1.4 system-ui,
57
+ 0.875rem/1.4 system-ui,
41
58
  sans-serif;
42
59
  background: var(--bg);
43
60
  color: var(--fg);
@@ -45,30 +62,166 @@ body {
45
62
 
46
63
  header {
47
64
  display: flex;
48
- gap: 16px;
65
+ gap: 10px;
49
66
  align-items: center;
50
- padding: 8px 16px;
67
+ min-height: 3.15em;
68
+ padding: max(4px, env(safe-area-inset-top)) 16px 4px;
51
69
  border-bottom: 1px solid var(--line);
52
70
  background: var(--card);
53
71
  flex: none;
54
72
  }
55
73
 
74
+ header h1 {
75
+ margin: 0;
76
+ font-size: inherit;
77
+ flex: none;
78
+ }
79
+
56
80
  #home {
57
81
  font-weight: 700;
82
+ white-space: nowrap;
58
83
  }
59
84
 
60
- #home,
85
+ .sr-only {
86
+ position: absolute;
87
+ width: 1px;
88
+ height: 1px;
89
+ overflow: hidden;
90
+ clip-path: inset(50%);
91
+ white-space: nowrap;
92
+ }
93
+
94
+ #keys-button,
61
95
  #copy-link {
62
96
  flex: none;
63
97
  white-space: nowrap;
64
98
  }
65
99
 
66
- #status {
100
+ /* The pickers shrink first when the window is narrow; a native menu truncates its own label. */
101
+ .picker {
102
+ display: flex;
103
+ gap: 4px;
104
+ align-items: center;
105
+ flex: 0 1 16em;
106
+ min-width: 5em;
107
+ color: var(--muted);
108
+ }
109
+
110
+ .picker select {
111
+ min-width: 0;
112
+ width: 100%;
113
+ flex: 1 1 auto;
114
+ }
115
+
116
+ .picker[hidden] {
117
+ display: none;
118
+ }
119
+
120
+ /* Finish never shrinks: the pickers give up their width first, so its count is always whole. */
121
+ #finish-slot {
122
+ display: flex;
123
+ gap: 8px;
124
+ align-items: center;
67
125
  margin-left: auto;
126
+ flex: none;
127
+ white-space: nowrap;
128
+ }
129
+
130
+ #finish-slot .finish {
131
+ flex: none;
132
+ }
133
+
134
+ /* Narrow: the pickers' labels and the tail of Finish's reason keep their text for screen readers
135
+ (a press on Finish says the reason too), and give the width to the menus. */
136
+ @media (max-width: 1049.98px) {
137
+ .picker > span,
138
+ #finish-slot .wide-only {
139
+ position: absolute;
140
+ width: 1px;
141
+ height: 1px;
142
+ overflow: hidden;
143
+ clip-path: inset(50%);
144
+ }
145
+
146
+ /* An iPad held upright, Split View or a zoomed page: the menus take a row of their own, half
147
+ each, so a project's name is readable; nothing in the header runs past the screen's edge. */
148
+ header {
149
+ flex-wrap: wrap;
150
+ row-gap: 6px;
151
+ }
152
+
153
+ .picker {
154
+ order: 1;
155
+ flex: 1 1 calc(50% - 5px);
156
+ }
157
+ }
158
+
159
+ #bar {
160
+ flex: none;
161
+ background: var(--card);
162
+ border-bottom: 1px solid var(--line);
163
+ }
164
+
165
+ #bar:empty {
166
+ display: none;
167
+ }
168
+
169
+ /* One line tall from 1050 px, two below, whatever it says: a message never moves anything. */
170
+ #status-row {
171
+ flex: none;
172
+ display: flex;
173
+ gap: 6px;
174
+ align-items: flex-start;
175
+ height: calc(1.4em + 10px);
176
+ padding: 5px 16px;
177
+ overflow: hidden;
68
178
  color: var(--muted);
179
+ border-bottom: 1px solid var(--line);
180
+ }
181
+
182
+ #status-row > span {
183
+ min-width: 0;
184
+ }
185
+
186
+ #elapsed,
187
+ #status-more {
188
+ flex: none;
189
+ }
190
+
191
+ /* More: the whole message, however long, until the next one. */
192
+ #status-row.open {
193
+ height: auto;
194
+ }
195
+
196
+ #status-row.open > span {
197
+ -webkit-line-clamp: unset;
198
+ line-clamp: unset;
199
+ }
200
+
201
+ #status-row > span {
202
+ display: -webkit-box;
203
+ -webkit-box-orient: vertical;
204
+ -webkit-line-clamp: 1;
205
+ line-clamp: 1;
206
+ overflow: hidden;
207
+ }
208
+
209
+ #status-row > span:empty {
210
+ display: none;
69
211
  }
70
212
 
71
- #status.error,
213
+ @media (max-width: 1049.98px) {
214
+ #status-row {
215
+ height: calc(2.8em + 10px);
216
+ }
217
+
218
+ #status-row > span {
219
+ -webkit-line-clamp: 2;
220
+ line-clamp: 2;
221
+ }
222
+ }
223
+
224
+ #alert,
72
225
  .error {
73
226
  color: var(--bad);
74
227
  }
@@ -80,18 +233,56 @@ main {
80
233
  padding: 16px;
81
234
  }
82
235
 
236
+ main.story-screen {
237
+ overflow: hidden;
238
+ padding: 0 16px 8px;
239
+ }
240
+
83
241
  a {
84
242
  color: var(--accent);
85
243
  }
86
244
 
87
- button {
245
+ button,
246
+ select,
247
+ input {
88
248
  font: inherit;
249
+ }
250
+
251
+ button,
252
+ select {
253
+ min-height: 2.15em;
89
254
  padding: 4px 10px;
90
255
  border: 1px solid var(--line);
91
256
  border-radius: 4px;
92
257
  background: var(--card);
93
258
  color: var(--fg);
94
259
  cursor: pointer;
260
+ touch-action: manipulation;
261
+ }
262
+
263
+ input[type="text"],
264
+ input[type="search"] {
265
+ padding: 4px 8px;
266
+ border: 1px solid var(--line);
267
+ border-radius: 4px;
268
+ background: var(--card);
269
+ color: var(--fg);
270
+ min-height: 2.15em;
271
+ }
272
+
273
+ :focus-visible {
274
+ outline: 2px solid var(--accent);
275
+ outline-offset: 3px;
276
+ }
277
+
278
+ kbd {
279
+ font: inherit;
280
+ font-size: 0.85em;
281
+ padding: 0 4px;
282
+ margin-left: 4px;
283
+ border: 1px solid var(--line);
284
+ border-radius: 3px;
285
+ color: var(--muted);
95
286
  }
96
287
 
97
288
  button[aria-pressed="true"] {
@@ -99,22 +290,25 @@ button[aria-pressed="true"] {
99
290
  box-shadow: inset 0 0 0 1px var(--accent);
100
291
  }
101
292
 
102
- button:disabled {
103
- opacity: 0.5;
104
- cursor: default;
105
- }
106
-
107
293
  button.link {
108
294
  border: none;
109
295
  background: none;
110
296
  padding: 0;
297
+ min-height: 0;
111
298
  color: var(--accent);
112
299
  }
113
300
 
114
301
  button.primary {
115
302
  background: var(--accent);
116
303
  border-color: var(--accent);
117
- color: #fff;
304
+ color: var(--on-accent);
305
+ }
306
+
307
+ button.primary[aria-disabled="true"],
308
+ button.primary:disabled {
309
+ background: var(--card);
310
+ color: var(--muted);
311
+ border-color: var(--line);
118
312
  }
119
313
 
120
314
  button.accept {
@@ -127,6 +321,223 @@ button.reject {
127
321
  color: var(--bad);
128
322
  }
129
323
 
324
+ /* Unavailable: muted text at full opacity (4.5:1), never removed, so nothing beside it moves. */
325
+ button:disabled,
326
+ button[aria-disabled="true"],
327
+ button.accept[aria-disabled="true"],
328
+ button.reject[aria-disabled="true"] {
329
+ color: var(--muted);
330
+ border-color: var(--line);
331
+ border-style: dashed;
332
+ background: transparent;
333
+ cursor: default;
334
+ }
335
+
336
+ button[aria-disabled="true"][aria-pressed="true"] {
337
+ color: var(--fg);
338
+ border-color: var(--accent);
339
+ }
340
+
341
+ /* A Reject or Exclude waiting for its reason. */
342
+ button.waiting {
343
+ outline: 2px dashed currentColor;
344
+ outline-offset: -5px;
345
+ }
346
+
347
+ .spinner {
348
+ display: inline-block;
349
+ width: 0.9em;
350
+ height: 0.9em;
351
+ margin-right: 6px;
352
+ vertical-align: -0.1em;
353
+ border: 2px solid currentColor;
354
+ border-right-color: transparent;
355
+ border-radius: 50%;
356
+ animation: spin 0.8s linear infinite;
357
+ }
358
+
359
+ @keyframes spin {
360
+ to {
361
+ transform: rotate(360deg);
362
+ }
363
+ }
364
+
365
+ @media (prefers-reduced-motion: reduce) {
366
+ .spinner,
367
+ .placeholder {
368
+ animation: none;
369
+ }
370
+ }
371
+
372
+ /* The page's own dialogs: questions (ask in review.js), Finish's sheet and the key overlay. */
373
+ dialog.ask,
374
+ dialog.keys {
375
+ max-width: min(680px, calc(100vw - 32px));
376
+ max-height: calc(100dvh - 32px);
377
+ border: 1px solid var(--line);
378
+ border-radius: 6px;
379
+ background: var(--card);
380
+ color: var(--fg);
381
+ padding: 16px;
382
+ }
383
+
384
+ dialog[open].ask,
385
+ dialog[open].keys {
386
+ display: flex;
387
+ flex-direction: column;
388
+ }
389
+
390
+ .ask-text {
391
+ white-space: pre-wrap;
392
+ overflow: auto;
393
+ min-height: 0;
394
+ }
395
+
396
+ .ask-text p {
397
+ margin: 0 0 8px;
398
+ }
399
+
400
+ .ask-text h2 {
401
+ font-size: 1rem;
402
+ margin: 0 0 8px;
403
+ }
404
+
405
+ .ask .actions {
406
+ display: flex;
407
+ gap: 8px;
408
+ justify-content: flex-end;
409
+ flex: none;
410
+ margin: 8px 0 0;
411
+ }
412
+
413
+ /*
414
+ * The wait box: a wait that blocks what the reader can do, in the middle of the screen over
415
+ * everything. One width, and every line keeps its height whether it says anything or not, so
416
+ * nothing in the box moves while its counts and its time change.
417
+ */
418
+ dialog.wait {
419
+ width: min(480px, calc(100vw - 32px));
420
+ max-height: calc(100dvh - 32px);
421
+ margin: auto;
422
+ border: 1px solid var(--line);
423
+ border-radius: 8px;
424
+ background: var(--card);
425
+ color: var(--fg);
426
+ padding: 20px;
427
+ font-size: 1.05rem;
428
+ box-shadow: 0 8px 32px rgb(0 0 0 / 0.3);
429
+ }
430
+
431
+ dialog.wait::backdrop {
432
+ background: rgb(0 0 0 / 0.45);
433
+ }
434
+
435
+ dialog.wait[open] {
436
+ display: flex;
437
+ flex-direction: column;
438
+ gap: 8px;
439
+ }
440
+
441
+ dialog.wait h2 {
442
+ font-size: 1.2rem;
443
+ margin: 0;
444
+ }
445
+
446
+ dialog.wait p {
447
+ margin: 0;
448
+ min-height: 1.4em;
449
+ overflow-wrap: anywhere;
450
+ }
451
+
452
+ /* A retry under way and a failure never show together: they share one two-line place. */
453
+ #wait-notes {
454
+ display: grid;
455
+ min-height: 2.8em;
456
+ }
457
+
458
+ #wait-notes > p {
459
+ grid-area: 1 / 1;
460
+ }
461
+
462
+ #wait-net {
463
+ color: var(--warn);
464
+ }
465
+
466
+ #wait-error {
467
+ color: var(--bad);
468
+ }
469
+
470
+ /* The box itself takes focus so that no key presses Cancel by accident; it needs no ring. */
471
+ dialog.wait:focus {
472
+ outline: none;
473
+ }
474
+
475
+ #wait-body {
476
+ overflow: auto;
477
+ min-height: 0;
478
+ }
479
+
480
+ #wait-body ol {
481
+ margin: 0;
482
+ padding-left: 20px;
483
+ }
484
+
485
+ #wait-body li.done {
486
+ color: var(--muted);
487
+ }
488
+
489
+ #wait-body li.now {
490
+ font-weight: 600;
491
+ }
492
+
493
+ #wait-bar {
494
+ width: 100%;
495
+ height: 10px;
496
+ }
497
+
498
+ /* A box that failed keeps its bar's place, empty. */
499
+ dialog.wait.failed #wait-bar {
500
+ visibility: hidden;
501
+ }
502
+
503
+ dialog.wait .actions {
504
+ display: flex;
505
+ gap: 8px;
506
+ align-items: center;
507
+ justify-content: flex-end;
508
+ margin-top: 4px;
509
+ }
510
+
511
+ /* An action that does not apply keeps its place, so the box never changes size. */
512
+ dialog.wait .actions button.unused {
513
+ visibility: hidden;
514
+ }
515
+
516
+ #wait-elapsed {
517
+ margin-right: auto;
518
+ color: var(--muted);
519
+ font-variant-numeric: tabular-nums;
520
+ }
521
+
522
+ /* Finish's notes scroll inside the sheet; the summary above and the buttons below stay put. */
523
+ .notes {
524
+ max-height: 30vh;
525
+ overflow: auto;
526
+ margin: 0 0 8px;
527
+ padding-left: 20px;
528
+ }
529
+
530
+ .keys table td:first-child {
531
+ white-space: nowrap;
532
+ font-weight: 600;
533
+ }
534
+
535
+ .keys .recent {
536
+ max-height: 30vh;
537
+ overflow: auto;
538
+ color: var(--muted);
539
+ }
540
+
130
541
  .card {
131
542
  background: var(--card);
132
543
  border: 1px solid var(--line);
@@ -137,7 +548,12 @@ button.reject {
137
548
 
138
549
  .card h2 {
139
550
  margin: 0 0 4px;
140
- font-size: 16px;
551
+ font-size: 1rem;
552
+ }
553
+
554
+ .card.placeholder {
555
+ min-height: 140px;
556
+ background: var(--wait);
141
557
  }
142
558
 
143
559
  .meta {
@@ -145,6 +561,14 @@ button.reject {
145
561
  margin: 4px 0;
146
562
  }
147
563
 
564
+ .listline {
565
+ display: flex;
566
+ gap: 12px;
567
+ align-items: center;
568
+ margin: 0 0 12px;
569
+ color: var(--muted);
570
+ }
571
+
148
572
  table {
149
573
  border-collapse: collapse;
150
574
  width: 100%;
@@ -163,8 +587,9 @@ td {
163
587
  margin: 0 4px;
164
588
  border: 1px solid var(--line);
165
589
  border-radius: 10px;
166
- font-size: 12px;
590
+ font-size: 0.75rem;
167
591
  font-weight: 400;
592
+ white-space: nowrap;
168
593
  }
169
594
 
170
595
  .badge.warn,
@@ -174,35 +599,135 @@ td {
174
599
  color: var(--warn);
175
600
  }
176
601
 
177
- .badge.accept,
178
- .badge.new {
179
- border-color: var(--ok);
180
- color: var(--ok);
602
+ .badge.accept,
603
+ .badge.new {
604
+ border-color: var(--ok);
605
+ color: var(--ok);
606
+ }
607
+
608
+ .badge.reject,
609
+ .badge.removed {
610
+ border-color: var(--bad);
611
+ color: var(--bad);
612
+ }
613
+
614
+ .badge.changed,
615
+ .badge.moved,
616
+ .badge.exclude {
617
+ border-color: var(--accent);
618
+ color: var(--accent);
619
+ }
620
+
621
+ .warning {
622
+ color: var(--warn);
623
+ margin: 4px 0;
624
+ }
625
+
626
+ .spacer {
627
+ flex: 1;
628
+ }
629
+
630
+ /* ---------------------------------------------------------------- the grid */
631
+
632
+ .gridbar {
633
+ display: flex;
634
+ gap: 8px;
635
+ align-items: center;
636
+ padding: 6px 16px;
637
+ min-height: calc(2.15em + 14px);
638
+ white-space: nowrap;
639
+ }
640
+
641
+ .gridbar > .row > *,
642
+ .gridbar [role="group"] > * {
643
+ flex: none;
644
+ }
645
+
646
+ #more-filters {
647
+ flex: 0 1 auto;
648
+ min-width: 7em;
649
+ }
650
+
651
+ .gridbar .row {
652
+ display: contents;
653
+ }
654
+
655
+ .gridbar [role="group"] {
656
+ display: flex;
657
+ gap: 4px;
658
+ align-items: center;
659
+ }
660
+
661
+ .gridbar label {
662
+ display: flex;
663
+ gap: 6px;
664
+ align-items: center;
665
+ color: var(--muted);
666
+ }
667
+
668
+ #find {
669
+ width: 14em;
181
670
  }
182
671
 
183
- .badge.reject,
184
- .badge.removed {
185
- border-color: var(--bad);
186
- color: var(--bad);
672
+ /* Two rows below 1300 px (an iPad either way up): what to look at, then what to do. */
673
+ @media (max-width: 1299.98px) {
674
+ .gridbar {
675
+ flex-direction: column;
676
+ align-items: stretch;
677
+ }
678
+
679
+ .gridbar .row {
680
+ display: flex;
681
+ flex-wrap: wrap;
682
+ gap: 8px;
683
+ align-items: center;
684
+ }
685
+
686
+ #find {
687
+ width: 100%;
688
+ min-width: 8em;
689
+ }
690
+
691
+ .gridbar > .row > label {
692
+ flex: 1 1 auto;
693
+ min-width: 0;
694
+ }
187
695
  }
188
696
 
189
- .badge.changed,
190
- .badge.moved,
191
- .badge.exclude {
192
- border-color: var(--accent);
193
- color: var(--accent);
697
+ /* A small menu: a native details element, opened by its summary. */
698
+ details.menu {
699
+ position: relative;
194
700
  }
195
701
 
196
- .toolbar {
702
+ details.menu > summary {
703
+ list-style: none;
704
+ padding: 4px 10px;
705
+ min-height: 2.15em;
197
706
  display: flex;
198
- flex-wrap: wrap;
199
- gap: 8px;
200
707
  align-items: center;
201
- margin-bottom: 12px;
708
+ border: 1px solid var(--line);
709
+ border-radius: 4px;
710
+ cursor: pointer;
202
711
  }
203
712
 
204
- .spacer {
205
- flex: 1;
713
+ details.menu > summary::-webkit-details-marker {
714
+ display: none;
715
+ }
716
+
717
+ details.menu > div {
718
+ position: absolute;
719
+ right: 0;
720
+ top: calc(100% + 4px);
721
+ z-index: 10;
722
+ display: flex;
723
+ flex-direction: column;
724
+ gap: 4px;
725
+ min-width: 14em;
726
+ padding: 8px;
727
+ background: var(--card);
728
+ border: 1px solid var(--line);
729
+ border-radius: 6px;
730
+ box-shadow: 0 4px 16px #0003;
206
731
  }
207
732
 
208
733
  .component {
@@ -214,7 +739,11 @@ td {
214
739
  gap: 12px;
215
740
  align-items: center;
216
741
  margin: 0 0 8px;
217
- font-size: 15px;
742
+ font-size: 0.9375rem;
743
+ }
744
+
745
+ .component h3 button {
746
+ font-weight: 400;
218
747
  }
219
748
 
220
749
  .stories {
@@ -232,7 +761,7 @@ td {
232
761
  }
233
762
 
234
763
  .story-name {
235
- font-size: 12px;
764
+ font-size: 0.75rem;
236
765
  font-weight: 600;
237
766
  margin-bottom: 4px;
238
767
  word-break: break-all;
@@ -254,16 +783,26 @@ td {
254
783
  }
255
784
 
256
785
  .tile img,
257
- .tile .noimage {
786
+ .tile .noimage,
787
+ .tile .thumb-wait {
258
788
  width: 100%;
259
789
  aspect-ratio: 4 / 3;
260
790
  object-fit: contain;
261
791
  background: repeating-conic-gradient(#8882 0 25%, transparent 0 50%) 0 0 / 16px 16px;
262
792
  }
263
793
 
264
- .tile .noimage {
794
+ .tile .noimage,
795
+ .tile .thumb-wait {
265
796
  display: grid;
266
797
  place-items: center;
798
+ color: var(--muted);
799
+ }
800
+
801
+ .tile .thumb-wait {
802
+ background: var(--wait);
803
+ }
804
+
805
+ .tile .noimage {
267
806
  color: var(--warn);
268
807
  }
269
808
 
@@ -284,7 +823,7 @@ td {
284
823
  flex-wrap: wrap;
285
824
  gap: 2px 6px;
286
825
  align-items: baseline;
287
- font-size: 12px;
826
+ font-size: 0.75rem;
288
827
  font-weight: 600;
289
828
  }
290
829
 
@@ -309,14 +848,14 @@ td {
309
848
 
310
849
  .decision button {
311
850
  padding: 0 8px;
312
- font-size: 12px;
851
+ min-height: 0;
852
+ font-size: 0.75rem;
313
853
  font-weight: 400;
314
854
  }
315
855
 
316
856
  .decision .meta {
317
857
  margin: 0;
318
858
  font-weight: 400;
319
- white-space: nowrap;
320
859
  }
321
860
 
322
861
  .error-list .decision .what {
@@ -325,7 +864,7 @@ td {
325
864
  }
326
865
 
327
866
  .tile .name {
328
- font-size: 12px;
867
+ font-size: 0.75rem;
329
868
  word-break: break-all;
330
869
  }
331
870
 
@@ -334,11 +873,11 @@ td {
334
873
  font-variant-numeric: tabular-nums;
335
874
  }
336
875
 
337
- /* The story last opened, when the grid comes back. */
876
+ /* The story last opened, when the grid comes back: flush, unlike the focus ring. */
338
877
  .tile.current,
339
878
  .error-list li.current {
340
879
  outline: 3px solid var(--accent);
341
- outline-offset: 2px;
880
+ outline-offset: 0;
342
881
  opacity: 1;
343
882
  }
344
883
 
@@ -349,9 +888,10 @@ td {
349
888
  margin-bottom: 20px;
350
889
  }
351
890
 
352
- .errors h3 {
891
+ .errors > summary {
353
892
  color: var(--bad);
354
- margin: 0 0 4px;
893
+ font-weight: 600;
894
+ cursor: pointer;
355
895
  }
356
896
 
357
897
  .error-list li {
@@ -362,38 +902,296 @@ td {
362
902
  color: var(--bad);
363
903
  }
364
904
 
365
- #filter,
366
- #goto {
367
- min-width: 220px;
368
- font: inherit;
369
- padding: 4px 8px;
370
- border: 1px solid var(--line);
371
- border-radius: 4px;
372
- background: var(--card);
373
- color: var(--fg);
905
+ .tile .moved-from {
906
+ font-size: 0.75rem;
907
+ word-break: break-all;
908
+ color: var(--accent);
374
909
  }
375
910
 
376
- /* The story screen fills main: its controls, then the two panes taking all the height left. */
377
- .story-view {
911
+ /* ---------------------------------------------------------------- the story */
912
+
913
+ /*
914
+ * The decision bar: every button always present, each in a cell of fixed width, so a button's
915
+ * box is the same on every item. One row from 1280 px; below, the cells share the window's width
916
+ * (never more), in two rows, or three (decisions, movement, the note) on an iPad held upright and
917
+ * below 900 px, and four columns below 600 px (Split View, a zoomed page), so Undo and the note
918
+ * box are always on screen.
919
+ */
920
+ .decisionbar {
921
+ display: grid;
922
+ grid-template-columns: 6.2em 6em 12em 6em 7em 7em 7.5em 6.5em minmax(8em, 1fr);
923
+ grid-template-areas: "grid prev pos next accept reject exclude undo note";
924
+ gap: 6px;
925
+ align-items: center;
926
+ padding: 4px 16px;
927
+ }
928
+
929
+ .decisionbar > button {
930
+ width: 100%;
931
+ min-height: 44px;
932
+ white-space: nowrap;
933
+ overflow: hidden;
934
+ text-overflow: ellipsis;
935
+ }
936
+
937
+ #to-grid {
938
+ grid-area: grid;
939
+ }
940
+
941
+ #prev {
942
+ grid-area: prev;
943
+ }
944
+
945
+ #position {
946
+ grid-area: pos;
947
+ overflow: hidden;
948
+ white-space: nowrap;
949
+ text-overflow: ellipsis;
950
+ text-align: center;
951
+ font-variant-numeric: tabular-nums;
952
+ }
953
+
954
+ #next {
955
+ grid-area: next;
956
+ }
957
+
958
+ #accept {
959
+ grid-area: accept;
960
+ }
961
+
962
+ #reject {
963
+ grid-area: reject;
964
+ }
965
+
966
+ #exclude {
967
+ grid-area: exclude;
968
+ }
969
+
970
+ #undo {
971
+ grid-area: undo;
972
+ }
973
+
974
+ .notebox {
975
+ grid-area: note;
378
976
  display: flex;
379
- flex-direction: column;
380
- height: 100%;
977
+ gap: 6px;
978
+ align-items: center;
979
+ min-width: 0;
980
+ }
981
+
982
+ .notebox label {
983
+ color: var(--muted);
984
+ }
985
+
986
+ #note {
987
+ flex: 1;
988
+ min-width: 0;
989
+ min-height: 44px;
990
+ }
991
+
992
+ @media (max-width: 1279.98px) {
993
+ .decisionbar {
994
+ grid-template-columns: repeat(6, minmax(0, 1fr));
995
+ grid-template-areas:
996
+ "grid accept reject exclude undo ."
997
+ "prev pos pos next note note";
998
+ }
999
+ }
1000
+
1001
+ /* Three rows on an iPad held upright, or any window under 900 px: a short landscape window keeps
1002
+ two, so its panes keep their height. */
1003
+ @media (max-width: 899.98px), (max-width: 1049.98px) and (orientation: portrait) {
1004
+ .decisionbar {
1005
+ grid-template-columns: repeat(5, minmax(0, 1fr));
1006
+ grid-template-areas:
1007
+ "grid accept reject exclude undo"
1008
+ "prev pos pos pos next"
1009
+ "note note note note note";
1010
+ }
1011
+ }
1012
+
1013
+ @media (max-width: 599.98px) {
1014
+ .decisionbar {
1015
+ grid-template-columns: repeat(4, minmax(0, 1fr));
1016
+ grid-template-areas:
1017
+ "accept reject exclude undo"
1018
+ "grid prev pos next"
1019
+ "note note note note";
1020
+ }
1021
+
1022
+ /* The keys are in the Keys list; here the words need the width. */
1023
+ .decisionbar > button {
1024
+ padding: 4px;
1025
+ }
1026
+
1027
+ .decisionbar > button kbd {
1028
+ display: none;
1029
+ }
1030
+ }
1031
+
1032
+ /* Images loading: the spinner sits in Accept's left padding, so its label and key chip keep
1033
+ their place and are never cut. */
1034
+ .decisionbar > button {
1035
+ position: relative;
1036
+ }
1037
+
1038
+ .decisionbar > button > .spinner {
1039
+ position: absolute;
1040
+ left: 3px;
1041
+ top: calc(50% - 0.35em);
1042
+ width: 0.7em;
1043
+ height: 0.7em;
1044
+ margin: 0;
1045
+ border-width: 1.5px;
381
1046
  }
382
1047
 
383
- .story-view > * {
1048
+ /* The item line: two lines, clamped; tapping it shows it all (the reader's choice to move the panes). */
1049
+ .itemline {
384
1050
  flex: none;
1051
+ height: calc(2 * 1.5em + 8px);
1052
+ padding: 4px 0;
1053
+ overflow: hidden;
1054
+ cursor: pointer;
385
1055
  }
386
1056
 
387
- .story-view h2 {
388
- margin: 0 0 4px;
389
- font-size: 18px;
1057
+ .itemline.open {
1058
+ height: auto;
1059
+ max-height: 40vh;
1060
+ overflow: auto;
1061
+ }
1062
+
1063
+ .itemline h2 {
1064
+ margin: 0;
1065
+ font-size: 0.9375rem;
1066
+ line-height: 1.5em;
1067
+ white-space: nowrap;
1068
+ overflow: hidden;
1069
+ text-overflow: ellipsis;
1070
+ }
1071
+
1072
+ .itemline p {
1073
+ margin: 0;
1074
+ line-height: 1.5em;
1075
+ color: var(--muted);
1076
+ white-space: nowrap;
1077
+ overflow: hidden;
1078
+ text-overflow: ellipsis;
1079
+ }
1080
+
1081
+ .itemline.open h2,
1082
+ .itemline.open p {
1083
+ white-space: normal;
1084
+ }
1085
+
1086
+ /* The view bar: one row from 1280 px; below (an iPad either way up) always two, the views on the
1087
+ first, so the panes start at the same height whichever view and options an item shows. */
1088
+ .viewbar {
1089
+ flex: none;
1090
+ display: flex;
1091
+ flex-wrap: wrap;
1092
+ gap: 6px 8px;
1093
+ align-items: center;
1094
+ padding: 2px 0 8px;
1095
+ white-space: nowrap;
1096
+ }
1097
+
1098
+ /* A finger needs 44 px: every control, in every bar and dialog. */
1099
+ @media (pointer: coarse) {
1100
+ button,
1101
+ button.link,
1102
+ select,
1103
+ input[type="text"],
1104
+ input[type="search"],
1105
+ summary,
1106
+ details.menu > summary {
1107
+ min-height: 44px;
1108
+ }
1109
+ }
1110
+
1111
+ .viewbar [role="group"] {
1112
+ display: flex;
1113
+ flex: none;
1114
+ }
1115
+
1116
+ .viewbar [role="group"] > button {
1117
+ border-radius: 0;
1118
+ margin-left: -1px;
1119
+ }
1120
+
1121
+ .viewbar [role="group"] > button:first-child {
1122
+ border-radius: 4px 0 0 4px;
1123
+ margin-left: 0;
1124
+ }
1125
+
1126
+ .viewbar [role="group"] > button:last-child {
1127
+ border-radius: 0 4px 4px 0;
1128
+ }
1129
+
1130
+ .viewbar > * {
1131
+ flex: none;
1132
+ }
1133
+
1134
+ .viewbar > .row-break {
1135
+ display: none;
1136
+ }
1137
+
1138
+ /* Fixed widths for what changes between items and views: the bar wraps the same way on all. */
1139
+ .viewbar > .view-option {
1140
+ width: 10.5em;
1141
+ }
1142
+
1143
+ .viewbar > #box-count {
1144
+ width: 5.5em;
1145
+ }
1146
+
1147
+ #ask-alert:empty {
1148
+ display: none;
1149
+ }
1150
+
1151
+ @media (max-width: 1279.98px) {
1152
+ .viewbar > .row-break {
1153
+ display: block;
1154
+ flex-basis: 100%;
1155
+ height: 0;
1156
+ }
1157
+ }
1158
+
1159
+ .details-pop {
1160
+ position: relative;
1161
+ }
1162
+
1163
+ .details-pop > div {
1164
+ position: fixed;
1165
+ right: 16px;
1166
+ z-index: 10;
1167
+ max-width: min(640px, calc(100vw - 32px));
1168
+ max-height: 50vh;
1169
+ overflow: auto;
1170
+ padding: 8px 12px;
1171
+ white-space: normal;
1172
+ background: var(--card);
1173
+ border: 1px solid var(--line);
1174
+ border-radius: 6px;
1175
+ box-shadow: 0 4px 16px #0003;
1176
+ }
1177
+
1178
+ .details-pop > summary {
1179
+ cursor: pointer;
1180
+ padding: 4px 6px;
1181
+ }
1182
+
1183
+ /* The story screen fills main: item line, view bar, then the panes taking all the height left. */
1184
+ .story-view {
1185
+ display: flex;
1186
+ flex-direction: column;
1187
+ height: 100%;
390
1188
  }
391
1189
 
392
1190
  .stage {
393
1191
  display: flex;
394
1192
  gap: 16px;
395
1193
  flex: 1 1 0;
396
- min-height: 200px;
1194
+ min-height: 120px;
397
1195
  }
398
1196
 
399
1197
  /* Two panes of the same size, whatever each holds. */
@@ -424,6 +1222,25 @@ td {
424
1222
  border-style: dashed;
425
1223
  }
426
1224
 
1225
+ .frame .wait,
1226
+ .frame .failed {
1227
+ margin: auto;
1228
+ padding: 16px;
1229
+ text-align: center;
1230
+ color: var(--muted);
1231
+ }
1232
+
1233
+ .frame .failed {
1234
+ color: var(--bad);
1235
+ }
1236
+
1237
+ .frame pre.console {
1238
+ margin: 0;
1239
+ flex: 1;
1240
+ overflow: auto;
1241
+ border: none;
1242
+ }
1243
+
427
1244
  /* Centred while it fits; auto margins drop to 0 when it overflows, so every part scrolls in. */
428
1245
  .sheet {
429
1246
  position: relative;
@@ -466,16 +1283,6 @@ td {
466
1283
  text-overflow: ellipsis;
467
1284
  }
468
1285
 
469
- .console-block {
470
- margin-top: 8px;
471
- }
472
-
473
- .console-block .console {
474
- max-height: 30vh;
475
- overflow: auto;
476
- margin: 4px 0 0;
477
- }
478
-
479
1286
  .console {
480
1287
  background: var(--card);
481
1288
  border: 1px solid var(--line);
@@ -483,31 +1290,56 @@ td {
483
1290
  white-space: pre-wrap;
484
1291
  }
485
1292
 
486
- .actions {
1293
+ /* The end of a pass, in place of the panes. */
1294
+ .endcard {
1295
+ flex: 1 1 0;
1296
+ min-height: 0;
1297
+ overflow: auto;
1298
+ border: 1px solid var(--line);
1299
+ border-radius: 6px;
1300
+ padding: 16px;
1301
+ background: var(--card);
1302
+ }
1303
+
1304
+ .endcard h2 {
1305
+ margin: 0 0 12px;
1306
+ font-size: 1rem;
1307
+ }
1308
+
1309
+ .offers {
487
1310
  display: flex;
488
1311
  flex-wrap: wrap;
489
1312
  gap: 8px;
490
- align-items: center;
491
- padding-top: 8px;
1313
+ margin: 0 0 12px;
492
1314
  }
493
1315
 
494
- #reason {
495
- flex: 1;
496
- min-width: 240px;
497
- font: inherit;
498
- padding: 4px 8px;
499
- border: 1px solid var(--line);
500
- border-radius: 4px;
501
- background: var(--card);
502
- color: var(--fg);
1316
+ .offers button {
1317
+ min-height: 44px;
503
1318
  }
504
1319
 
505
- .tile .moved-from {
506
- font-size: 12px;
507
- word-break: break-all;
508
- color: var(--accent);
1320
+ /* Finish: its steps while it runs, and its result. */
1321
+ .finish-panel ol {
1322
+ padding-left: 20px;
1323
+ }
1324
+
1325
+ .finish-panel li {
1326
+ margin: 4px 0;
1327
+ }
1328
+
1329
+ .finish-panel li.done {
1330
+ color: var(--muted);
1331
+ }
1332
+
1333
+ .finish-panel li.now {
1334
+ font-weight: 600;
1335
+ }
1336
+
1337
+ .finish-panel progress {
1338
+ margin-left: 8px;
1339
+ vertical-align: middle;
509
1340
  }
510
1341
 
511
- #app:focus {
1342
+ #app:focus,
1343
+ #app:focus-visible {
512
1344
  outline: none;
513
1345
  }