@wtfalch/email 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,22 @@
1
1
  /* The mail client's own layout. Everything visual comes from
2
2
  `@wtfalch/design`'s tokens; nothing here defines a colour, a radius or a
3
- space of its own. Import it after the design stylesheet. */
3
+ space of its own. Import it after the design stylesheet.
4
+
5
+ Two rules this file is written to, both from design's CLAUDE.md and both
6
+ learned the hard way there:
7
+
8
+ - A token, never a value. No hex, no pixel spacing, no font size. The one
9
+ exception is `--rail`, declared below: it is a 3px optical detail, like
10
+ the system's own `--nudge`, and a spacing step it is not.
11
+ - Nothing drawn over a surface names the colour it sits on. The row's
12
+ hover and selection are `currentColor` at low alpha, so they are a step
13
+ from whatever is behind them in whatever theme is on. */
4
14
 
5
15
  .mail {
16
+ /* Declared here rather than inline: the unread bar and the row's padding
17
+ have to agree, and an optical width is not a spacing step. */
18
+ --rail: 3px;
19
+
6
20
  display: flex;
7
21
  flex-direction: column;
8
22
  height: 100%;
@@ -12,29 +26,196 @@
12
26
  font-size: var(--text-sm);
13
27
  }
14
28
 
29
+ /* ---------------------------------------------------------------- the bar */
30
+
15
31
  .mail-bar {
16
- display: flex;
32
+ display: grid;
33
+ /* Three tracks, and the middle one centred in the *window* rather than in
34
+ the space the other two leave over. A flex bar puts the search wherever
35
+ the account name's length happens to leave it, which moves every time
36
+ somebody with a longer address signs in. */
37
+ grid-template-columns: 1fr minmax(0, 34rem) 1fr;
17
38
  align-items: center;
18
39
  gap: var(--space-3);
19
- padding: var(--space-3);
40
+ padding: var(--space-2) var(--space-3);
20
41
  border-bottom: var(--border-width) solid var(--border);
42
+ background: var(--panel);
21
43
  flex: none;
22
44
  }
23
45
 
46
+ .mail-bar-start {
47
+ display: flex;
48
+ align-items: center;
49
+ gap: var(--space-2);
50
+ min-width: 0;
51
+ }
52
+
53
+ .mail-bar-end {
54
+ display: flex;
55
+ align-items: center;
56
+ justify-content: flex-end;
57
+ gap: var(--space-2);
58
+ min-width: 0;
59
+ }
60
+
61
+ .mail-wordmark {
62
+ font-size: var(--text-sm);
63
+ font-weight: var(--weight-strong);
64
+ letter-spacing: 0.01em;
65
+ white-space: nowrap;
66
+ }
67
+
68
+ /* The search reads as a field and behaves as one, but it is quiet: it is the
69
+ thing you reach for, not the thing you look at. It used to be a full-width
70
+ input, which made the most prominent object on the screen the one nobody
71
+ uses until they need it. */
24
72
  .mail-search {
73
+ display: flex;
74
+ align-items: center;
75
+ gap: var(--space-2);
76
+ min-width: 0;
77
+ padding: 0 var(--space-2);
78
+ border: var(--border-width) solid var(--border);
79
+ border-radius: var(--radius-pill);
80
+ background: var(--control);
81
+ transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
82
+ }
83
+
84
+ .mail-search:focus-within {
85
+ border-color: var(--accent);
86
+ box-shadow: var(--focus-ring);
87
+ }
88
+
89
+ .mail-search-icon {
90
+ flex: none;
91
+ color: var(--muted);
92
+ }
93
+
94
+ .mail-search-field {
25
95
  flex: 1 1 auto;
26
96
  min-width: 0;
97
+ padding: var(--space-2) 0;
98
+ border: none;
99
+ background: transparent;
100
+ color: var(--text);
101
+ font: inherit;
102
+ font-size: var(--text-sm);
103
+ }
104
+
105
+ .mail-search-field:focus {
106
+ outline: none;
107
+ }
108
+
109
+ /* Safari draws its own cross inside a `type=search`, in its own colours and
110
+ at its own size, beside the one this draws. */
111
+ .mail-search-field::-webkit-search-cancel-button {
112
+ appearance: none;
27
113
  }
114
+
115
+ .mail-search-key {
116
+ flex: none;
117
+ padding: 0 var(--space-1);
118
+ border: var(--border-width) solid var(--border);
119
+ border-radius: var(--radius-sm);
120
+ color: var(--muted);
121
+ font-family: var(--font-mono);
122
+ font-size: var(--text-2xs);
123
+ line-height: 1.6;
124
+ }
125
+
126
+ .mail-search-clear {
127
+ display: inline-flex;
128
+ align-items: center;
129
+ flex: none;
130
+ padding: var(--space-1);
131
+ border: none;
132
+ border-radius: var(--radius-pill);
133
+ background: transparent;
134
+ color: var(--muted);
135
+ cursor: pointer;
136
+ }
137
+
138
+ .mail-search-clear:hover {
139
+ color: var(--text);
140
+ }
141
+
142
+ .mail-search-clear:focus-visible {
143
+ outline: none;
144
+ box-shadow: var(--focus-ring);
145
+ }
146
+
147
+ /* The word goes when the bar gets tight; the pen stays, and the button keeps
148
+ its accessible name from the icon's title. */
149
+ .mail-compose-word {
150
+ white-space: nowrap;
151
+ }
152
+
28
153
  .mail-panes {
154
+ display: flex;
29
155
  flex: 1 1 auto;
30
156
  min-height: 0;
157
+ /* A flex item's `min-width` is `auto`, which means "no smaller than your
158
+ content" -- and this one's content is a list of rows full of `nowrap`
159
+ text. Without the override the list sized itself to the longest subject
160
+ in the mailbox and pushed the whole page wider than the window, which is
161
+ a horizontal scrollbar on every phone and the one layout rule with no
162
+ exceptions. It did not show on the wide layout because `SplitPane` gives
163
+ both panes an explicit width. */
164
+ min-width: 0;
165
+ }
166
+
167
+ .mail-panes > * {
168
+ min-width: 0;
169
+ }
170
+
171
+ .mail-panes-split {
172
+ flex: 1 1 auto;
173
+ min-width: 0;
174
+ }
175
+
176
+ /* What just happened. Pinned to the bottom corner, out of the way of the
177
+ panes, and gone in four seconds. */
178
+ .mail-note {
179
+ position: fixed;
180
+ left: 50%;
181
+ bottom: var(--space-4);
182
+ z-index: 40;
183
+ padding: var(--space-2) var(--space-4);
184
+ border: var(--border-width) solid var(--border);
185
+ border-radius: var(--radius-pill);
186
+ background: var(--panel);
187
+ box-shadow: var(--shadow-3);
188
+ color: var(--text);
189
+ font-size: var(--text-xs);
190
+ /* Out of the layout and out of the hit-testing when there is nothing to
191
+ say, but still in the accessibility tree, which is the point of it
192
+ always being rendered. */
193
+ opacity: 0;
194
+ transform: translate(-50%, var(--space-2));
195
+ pointer-events: none;
196
+ transition: opacity var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
197
+ }
198
+
199
+ .mail-note.is-shown {
200
+ opacity: 1;
201
+ transform: translate(-50%, 0);
31
202
  }
32
203
 
33
204
  /* ---------------------------------------------------------------- mailboxes */
205
+
34
206
  .mailboxes {
35
207
  height: 100%;
208
+ padding: var(--space-2);
36
209
  background: var(--panel);
210
+ border-right: var(--border-width) solid var(--border);
211
+ }
212
+
213
+ .mailboxes.is-rail {
214
+ flex: none;
215
+ width: calc(var(--space-10) + var(--space-4));
216
+ padding: var(--space-2) var(--space-1);
37
217
  }
218
+
38
219
  .mailboxes-quiet,
39
220
  .threads-quiet,
40
221
  .message-quiet {
@@ -49,8 +230,28 @@
49
230
  margin: 0;
50
231
  padding: 0;
51
232
  }
52
- .mailboxes-list .mailboxes-list {
53
- padding-left: var(--space-3);
233
+
234
+ .mailboxes-heading {
235
+ margin: var(--space-4) 0 var(--space-1);
236
+ padding: 0 var(--space-2);
237
+ color: var(--muted);
238
+ font-size: var(--text-2xs);
239
+ font-weight: var(--weight-strong);
240
+ letter-spacing: 0.06em;
241
+ text-transform: uppercase;
242
+ }
243
+
244
+ /* On the rail the heading has no room for its word, so it becomes the line
245
+ the word was there to draw. */
246
+ .is-rail .mailboxes-heading {
247
+ margin: var(--space-2) var(--space-2);
248
+ padding: 0;
249
+ }
250
+
251
+ .is-rail .mailboxes-heading .rule {
252
+ display: block;
253
+ height: var(--border-width);
254
+ background: var(--border);
54
255
  }
55
256
 
56
257
  .mailbox {
@@ -58,29 +259,45 @@
58
259
  align-items: center;
59
260
  gap: var(--space-2);
60
261
  width: 100%;
61
- padding: var(--space-2) var(--space-3);
62
- padding-left: calc(var(--space-3) + var(--depth, 0) * var(--space-3));
262
+ padding: var(--space-2);
263
+ padding-left: calc(var(--space-2) + var(--depth, 0) * var(--space-4));
63
264
  border: none;
265
+ border-radius: var(--radius);
64
266
  background: transparent;
65
267
  color: var(--muted);
66
268
  font: inherit;
67
269
  font-size: var(--text-sm);
68
270
  text-align: left;
69
271
  cursor: pointer;
272
+ transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
273
+ }
274
+
275
+ .is-rail .mailbox {
276
+ justify-content: center;
277
+ position: relative;
278
+ padding: var(--space-2);
70
279
  }
71
280
 
72
281
  .mailbox:hover {
73
- background: var(--panel-2);
282
+ background: color-mix(in srgb, currentColor 8%, transparent);
74
283
  color: var(--text);
75
284
  }
285
+
76
286
  .mailbox:focus-visible {
77
287
  outline: none;
78
288
  box-shadow: var(--focus-ring);
79
289
  }
290
+
80
291
  .mailbox.on {
81
- background: var(--panel-2);
292
+ background: color-mix(in srgb, var(--accent) 14%, transparent);
82
293
  color: var(--text);
294
+ font-weight: var(--weight-strong);
295
+ }
296
+
297
+ .mailbox.on .mailbox-icon {
298
+ color: var(--accent);
83
299
  }
300
+
84
301
  /* Unread is weight, so it survives a theme, a projector and a reader who
85
302
  cannot tell the colours apart. */
86
303
  .mailbox.unread {
@@ -88,11 +305,19 @@
88
305
  font-weight: var(--weight-strong);
89
306
  }
90
307
 
308
+ .mailbox.is-skeleton {
309
+ display: flex;
310
+ align-items: center;
311
+ gap: var(--space-2);
312
+ cursor: default;
313
+ }
314
+
91
315
  .mailbox-icon {
316
+ display: inline-flex;
92
317
  flex: none;
93
- width: var(--space-4);
94
- height: var(--space-4);
318
+ color: var(--muted);
95
319
  }
320
+
96
321
  .mailbox-name {
97
322
  flex: 1 1 auto;
98
323
  min-width: 0;
@@ -100,39 +325,140 @@
100
325
  text-overflow: ellipsis;
101
326
  white-space: nowrap;
102
327
  }
328
+
329
+ /* A fixed slot, reserved whether or not there is a number in it: a count
330
+ arriving must not re-measure the name beside it. That is design's
331
+ "content jumping" rule, and a mailbox that goes from 0 to 1 unread is the
332
+ commonest way to see it. */
103
333
  .mailbox-count {
104
334
  flex: none;
335
+ min-width: var(--space-5);
336
+ text-align: right;
337
+ color: var(--muted);
105
338
  font-size: var(--text-2xs);
106
339
  font-variant-numeric: tabular-nums;
107
340
  }
108
341
 
342
+ .mailbox.unread .mailbox-count {
343
+ color: var(--accent);
344
+ }
345
+
346
+ /* A folder gets a dot where a role gets a glyph, so the names still line up
347
+ without a column of identical folder icons carrying no information. */
348
+ .mailbox-pip {
349
+ display: block;
350
+ width: var(--space-1);
351
+ height: var(--space-1);
352
+ border-radius: var(--radius-pill);
353
+ background: currentColor;
354
+ opacity: 0.45;
355
+ }
356
+
357
+ /* On the rail there is no room for a number, so unread becomes a dot on the
358
+ corner of the glyph. */
359
+ .mailbox-pip.is-unread {
360
+ position: absolute;
361
+ top: var(--space-1);
362
+ right: var(--space-1);
363
+ background: var(--accent);
364
+ opacity: 1;
365
+ }
366
+
109
367
  /* ------------------------------------------------------------------ threads */
368
+
110
369
  .threads {
111
370
  display: flex;
112
371
  flex-direction: column;
372
+ flex: 1 1 auto;
113
373
  height: 100%;
114
374
  min-height: 0;
375
+ /* See `.mail-panes`. */
376
+ min-width: 0;
115
377
  background: var(--panel);
378
+ border-right: var(--border-width) solid var(--border);
379
+ }
380
+
381
+ .threads-head {
382
+ display: flex;
383
+ align-items: baseline;
384
+ justify-content: space-between;
385
+ gap: var(--space-2);
386
+ flex: none;
387
+ padding: var(--space-3) var(--space-4);
388
+ border-bottom: var(--border-width) solid var(--border);
389
+ }
390
+
391
+ .threads-title {
392
+ margin: 0;
393
+ font-size: var(--text-sm);
394
+ font-weight: var(--weight-strong);
395
+ overflow: hidden;
396
+ text-overflow: ellipsis;
397
+ white-space: nowrap;
398
+ }
399
+
400
+ .threads-total {
401
+ flex: none;
402
+ color: var(--muted);
403
+ font-size: var(--text-2xs);
404
+ font-variant-numeric: tabular-nums;
116
405
  }
406
+
117
407
  .threads-scroll {
118
408
  flex: 1 1 auto;
409
+ min-height: 0;
119
410
  }
411
+
412
+ .threads-empty {
413
+ display: grid;
414
+ place-items: center;
415
+ flex: 1 1 auto;
416
+ padding: var(--space-6);
417
+ }
418
+
120
419
  .threads-list {
420
+ display: flex;
421
+ flex-direction: column;
422
+ }
423
+
424
+ .threads-rows {
121
425
  list-style: none;
122
426
  margin: 0;
123
427
  padding: 0;
124
428
  }
429
+
125
430
  .threads-pager {
126
431
  flex: none;
127
432
  padding: 0 var(--space-3);
128
433
  border-top: var(--border-width) solid var(--border);
129
434
  }
130
435
 
436
+ /* The day heading, pinned while its own rows are on screen. Scrolling a long
437
+ mailbox with no landmarks is how you lose your place. */
438
+ .threads-day {
439
+ position: sticky;
440
+ top: 0;
441
+ z-index: 1;
442
+ margin: 0;
443
+ padding: var(--space-2) var(--space-4);
444
+ /* Opaque, because rows scroll under it. This is the one place a surface
445
+ token is named directly rather than mixed: a translucent sticky heading
446
+ over moving text is unreadable at any alpha. */
447
+ background: var(--panel);
448
+ border-bottom: var(--border-width) solid var(--border);
449
+ color: var(--muted);
450
+ font-size: var(--text-2xs);
451
+ font-weight: var(--weight-strong);
452
+ letter-spacing: 0.06em;
453
+ text-transform: uppercase;
454
+ }
455
+
131
456
  .thread-row {
132
- display: grid;
133
- gap: var(--space-1);
457
+ display: flex;
458
+ align-items: stretch;
459
+ gap: 0;
134
460
  width: 100%;
135
- padding: var(--space-3);
461
+ padding: 0;
136
462
  border: none;
137
463
  border-bottom: var(--border-width) solid var(--border);
138
464
  background: transparent;
@@ -140,23 +466,59 @@
140
466
  font: inherit;
141
467
  text-align: left;
142
468
  cursor: pointer;
469
+ transition: background var(--dur-fast) var(--ease);
143
470
  }
144
471
 
145
472
  .thread-row:hover {
146
- background: var(--panel-2);
473
+ background: color-mix(in srgb, currentColor 6%, transparent);
147
474
  }
475
+
148
476
  .thread-row:focus-visible {
149
477
  outline: none;
150
478
  box-shadow: var(--focus-ring);
479
+ /* Above the sticky day heading, or the ring is clipped by it -- WCAG 2.2's
480
+ "focus not obscured", and the heading is exactly the persistent UI the
481
+ criterion is about. */
482
+ position: relative;
483
+ z-index: 2;
151
484
  }
485
+
152
486
  .thread-row.on {
153
- background: var(--panel-2);
487
+ background: color-mix(in srgb, var(--accent) 12%, transparent);
154
488
  }
489
+
155
490
  .thread-row.unread {
156
491
  color: var(--text);
157
492
  }
158
- .thread-row.unread .thread-subject-text {
159
- font-weight: var(--weight-strong);
493
+
494
+ .thread-row.is-skeleton {
495
+ cursor: default;
496
+ }
497
+
498
+ /* The unread bar, in the gutter rather than in the text: a read row and an
499
+ unread row have to start their subjects at the same x, and the bullet that
500
+ used to sit inline moved every unread subject two characters right. */
501
+ .thread-rail {
502
+ flex: none;
503
+ width: var(--rail);
504
+ background: transparent;
505
+ }
506
+
507
+ .thread-row.unread .thread-rail {
508
+ background: var(--accent);
509
+ }
510
+
511
+ .thread-row.on .thread-rail {
512
+ background: var(--accent);
513
+ }
514
+
515
+ .thread-main {
516
+ display: flex;
517
+ flex-direction: column;
518
+ gap: var(--space-1);
519
+ flex: 1 1 auto;
520
+ min-width: 0;
521
+ padding: var(--space-3) var(--space-4) var(--space-3) calc(var(--space-4) - var(--rail));
160
522
  }
161
523
 
162
524
  .thread-top {
@@ -165,85 +527,228 @@
165
527
  gap: var(--space-2);
166
528
  min-width: 0;
167
529
  }
530
+
168
531
  .thread-who {
169
532
  flex: 1 1 auto;
170
533
  min-width: 0;
534
+ overflow: hidden;
535
+ text-overflow: ellipsis;
536
+ white-space: nowrap;
537
+ font-size: var(--text-sm);
171
538
  }
172
- .thread-nobody {
539
+
540
+ .thread-row.unread .thread-who {
541
+ font-weight: var(--weight-strong);
542
+ }
543
+
544
+ .thread-marks {
545
+ display: flex;
546
+ align-items: center;
547
+ gap: var(--space-1);
548
+ flex: none;
173
549
  color: var(--muted);
174
- font-style: italic;
175
550
  }
551
+
552
+ .thread-mark.is-flag {
553
+ color: var(--warn);
554
+ }
555
+
556
+ .thread-count {
557
+ padding: 0 var(--space-1);
558
+ border: var(--border-width) solid var(--border);
559
+ border-radius: var(--radius-pill);
560
+ font-size: var(--text-2xs);
561
+ font-variant-numeric: tabular-nums;
562
+ line-height: 1.5;
563
+ }
564
+
565
+ .thread-draft {
566
+ color: var(--warn);
567
+ font-size: var(--text-2xs);
568
+ font-weight: var(--weight-strong);
569
+ text-transform: uppercase;
570
+ letter-spacing: 0.04em;
571
+ }
572
+
176
573
  .thread-when {
177
574
  flex: none;
178
- font-size: var(--text-2xs);
179
575
  color: var(--muted);
576
+ font-size: var(--text-2xs);
180
577
  font-variant-numeric: tabular-nums;
181
578
  }
182
579
 
183
- .thread-subject {
580
+ /* Subject and preview share a line, which is the whole density argument:
581
+ the subject takes what it needs and the preview takes the rest. */
582
+ .thread-bottom {
184
583
  display: flex;
185
584
  align-items: baseline;
186
585
  gap: var(--space-2);
187
586
  min-width: 0;
587
+ font-size: var(--text-xs);
188
588
  }
189
- .thread-dot {
190
- flex: none;
191
- color: var(--accent);
192
- }
193
- .thread-subject-text {
194
- flex: 1 1 auto;
589
+
590
+ /* The subject takes what it needs and the preview takes the rest, which is
591
+ the whole reason they share a line. Both were `flex-basis: auto`, so the
592
+ two shrank in proportion to their content and a nine-word preview
593
+ truncated a two-word subject to "Engine sc…" with half the row empty.
594
+ The preview's basis is zero: it contributes nothing to the line's natural
595
+ width and only ever grows into what is left. */
596
+ .thread-subject {
597
+ flex: 0 1 auto;
598
+ /* Not the whole line either -- a forwarded subject is a paragraph, and one
599
+ that eats the row leaves no evidence of what the message says. */
600
+ max-width: 62%;
195
601
  min-width: 0;
196
602
  overflow: hidden;
197
603
  text-overflow: ellipsis;
198
604
  white-space: nowrap;
605
+ color: var(--text);
199
606
  }
200
- .thread-count {
201
- flex: none;
202
- padding: 0 var(--space-1);
203
- border: var(--border-width) solid var(--border);
204
- border-radius: var(--radius-pill);
205
- font-size: var(--text-2xs);
607
+
608
+ .thread-row:not(.unread) .thread-subject {
206
609
  color: var(--muted);
207
610
  }
208
611
 
612
+ .thread-row.unread .thread-subject {
613
+ font-weight: var(--weight-strong);
614
+ }
615
+
616
+ /* No opacity. `--muted` is the measured token -- design holds it to 4.5:1
617
+ against every theme -- and knocking it back to 85% put the preview under
618
+ the threshold on ten rows in light and one in dark. Measured with axe, not
619
+ judged by eye, which is the rule. */
209
620
  .thread-preview {
210
- color: var(--muted);
211
- font-size: var(--text-xs);
621
+ flex: 1 1 0;
622
+ min-width: 0;
212
623
  overflow: hidden;
213
624
  text-overflow: ellipsis;
214
625
  white-space: nowrap;
215
- }
216
-
217
- .thread-marks {
218
- display: flex;
219
- gap: var(--space-1);
220
- flex-wrap: wrap;
626
+ color: var(--muted);
221
627
  }
222
628
 
223
629
  /* --------------------------------------------------------------- one thread */
630
+
224
631
  .thread {
225
632
  display: flex;
226
633
  flex-direction: column;
227
634
  height: 100%;
228
635
  min-height: 0;
636
+ min-width: 0;
637
+ flex: 1 1 auto;
638
+ background: var(--bg);
639
+ }
640
+
641
+ .thread.is-empty {
642
+ display: grid;
643
+ place-items: center;
644
+ padding: var(--space-6);
645
+ }
646
+
647
+ .thread-hint {
648
+ display: block;
649
+ margin-top: var(--space-2);
650
+ color: var(--muted);
651
+ font-size: var(--text-xs);
229
652
  }
653
+
654
+ .thread-hint kbd,
655
+ .keys kbd,
656
+ .composer-hint kbd {
657
+ display: inline-block;
658
+ min-width: var(--space-5);
659
+ padding: 0 var(--space-1);
660
+ border: var(--border-width) solid var(--border);
661
+ border-radius: var(--radius-sm);
662
+ background: var(--panel-2);
663
+ color: var(--text);
664
+ font-family: var(--font-mono);
665
+ font-size: var(--text-2xs);
666
+ text-align: center;
667
+ }
668
+
669
+ /* Pinned, so archiving the twelfth message of a thread does not mean
670
+ scrolling back up to find the button. */
230
671
  .thread-head {
672
+ display: flex;
673
+ align-items: flex-start;
674
+ justify-content: space-between;
675
+ gap: var(--space-3);
231
676
  flex: none;
232
- padding: var(--space-4);
677
+ padding: var(--space-3) var(--space-4);
233
678
  border-bottom: var(--border-width) solid var(--border);
679
+ background: var(--panel);
234
680
  }
681
+
682
+ .thread-headline {
683
+ display: flex;
684
+ align-items: flex-start;
685
+ gap: var(--space-2);
686
+ min-width: 0;
687
+ }
688
+
689
+ .thread-heading {
690
+ min-width: 0;
691
+ }
692
+
235
693
  .thread-title {
236
694
  margin: 0;
237
695
  font-size: var(--text-lg);
238
696
  font-weight: var(--weight-strong);
697
+ /* Two lines, then an ellipsis: a forwarded subject can be a paragraph, and
698
+ a head that grows to four lines pushes the message off the screen. */
699
+ display: -webkit-box;
700
+ -webkit-line-clamp: 2;
701
+ line-clamp: 2;
702
+ -webkit-box-orient: vertical;
703
+ overflow: hidden;
239
704
  }
705
+
240
706
  .thread-meta {
241
707
  margin: var(--space-1) 0 0;
242
708
  color: var(--muted);
243
709
  font-size: var(--text-xs);
710
+ overflow: hidden;
711
+ text-overflow: ellipsis;
712
+ white-space: nowrap;
713
+ }
714
+
715
+ .thread-meta-sep {
716
+ margin: 0 var(--space-1);
717
+ }
718
+
719
+ .thread-tools {
720
+ display: flex;
721
+ align-items: center;
722
+ gap: var(--space-1);
723
+ flex: none;
724
+ }
725
+
726
+ .thread-tool {
727
+ display: inline-flex;
728
+ }
729
+
730
+ .thread-tools-rule {
731
+ width: var(--border-width);
732
+ align-self: stretch;
733
+ margin: 0 var(--space-1);
734
+ background: var(--border);
244
735
  }
736
+
737
+ .thread-tools .is-flagged {
738
+ color: var(--warn);
739
+ }
740
+
245
741
  .thread-body {
246
742
  flex: 1 1 auto;
743
+ min-height: 0;
744
+ }
745
+
746
+ .thread-foot {
747
+ display: flex;
748
+ align-items: center;
749
+ gap: var(--space-2);
750
+ padding: var(--space-4);
751
+ border-top: var(--border-width) solid var(--border);
247
752
  }
248
753
 
249
754
  .messages {
@@ -255,6 +760,11 @@
255
760
  .message {
256
761
  border-bottom: var(--border-width) solid var(--border);
257
762
  }
763
+
764
+ .message.open {
765
+ background: var(--panel);
766
+ }
767
+
258
768
  .message-head {
259
769
  display: flex;
260
770
  align-items: center;
@@ -281,23 +791,55 @@
281
791
  outline: none;
282
792
  box-shadow: var(--focus-ring);
283
793
  }
284
- .message-when {
794
+
795
+ /* Collapsed, the preview is the only thing that says whether this is the
796
+ message being looked for -- but the sender outranks it, so the basis is
797
+ zero and the preview only ever takes what the name has left. At `auto` the
798
+ two shrank in proportion and a four-word preview cut "Mary Somerville"
799
+ down to "M..", which is the same bug the list's subject had. */
800
+ .message-peek {
801
+ flex: 1 1 0;
802
+ min-width: 0;
803
+ overflow: hidden;
804
+ text-overflow: ellipsis;
805
+ white-space: nowrap;
806
+ color: var(--muted);
807
+ font-size: var(--text-xs);
808
+ }
809
+
810
+ .message-aside {
811
+ display: flex;
812
+ align-items: center;
813
+ gap: var(--space-2);
285
814
  flex: none;
286
815
  margin-left: auto;
287
816
  color: var(--muted);
817
+ }
818
+
819
+ .message-when {
820
+ color: var(--muted);
288
821
  font-size: var(--text-2xs);
822
+ font-variant-numeric: tabular-nums;
823
+ white-space: nowrap;
289
824
  }
825
+
290
826
  .message-nobody {
291
827
  color: var(--muted);
292
828
  font-style: italic;
293
829
  }
294
830
 
295
831
  .message-body {
296
- padding: 0 var(--space-4) var(--space-4);
832
+ /* The measure. Prose past about 80 characters a line is measurably slower
833
+ to read, and a maximised window on a wide monitor gives a message pane
834
+ 160. The body is capped and the chrome around it is not. */
835
+ max-width: 78ch;
836
+ padding: 0 var(--space-4) var(--space-5);
297
837
  }
838
+
298
839
  .message-lines {
299
840
  margin-bottom: var(--space-3);
300
841
  }
842
+
301
843
  .message-line {
302
844
  margin: 0;
303
845
  color: var(--muted);
@@ -305,6 +847,7 @@
305
847
  display: flex;
306
848
  gap: var(--space-2);
307
849
  }
850
+
308
851
  .message-line-label {
309
852
  min-width: var(--space-6);
310
853
  }
@@ -360,30 +903,35 @@
360
903
  font: inherit;
361
904
  font-size: var(--text-xs);
362
905
  cursor: pointer;
906
+ transition: border-color var(--dur-fast) var(--ease);
363
907
  }
364
908
 
365
909
  .attachment:hover:not(:disabled) {
366
910
  border-color: var(--accent);
367
911
  }
912
+
368
913
  .attachment:focus-visible {
369
914
  outline: none;
370
915
  box-shadow: var(--focus-ring);
371
916
  }
917
+
372
918
  .attachment:disabled {
373
919
  cursor: default;
374
920
  color: var(--muted);
375
921
  }
922
+
376
923
  .attachment-icon {
377
924
  flex: none;
378
- width: var(--space-4);
379
- height: var(--space-4);
380
925
  color: var(--muted);
381
926
  }
927
+
382
928
  .attachment-size {
383
929
  color: var(--muted);
930
+ font-variant-numeric: tabular-nums;
384
931
  }
385
932
 
386
933
  /* ----------------------------------------------------------------- composer */
934
+
387
935
  .composer {
388
936
  display: grid;
389
937
  gap: var(--space-3);
@@ -426,11 +974,13 @@
426
974
  display: grid;
427
975
  gap: var(--space-1);
428
976
  }
977
+
429
978
  .composer-files li {
430
979
  display: flex;
431
980
  align-items: center;
432
981
  gap: var(--space-2);
433
982
  }
983
+
434
984
  .composer-file {
435
985
  font-size: var(--text-xs);
436
986
  }
@@ -439,13 +989,109 @@
439
989
  display: flex;
440
990
  align-items: center;
441
991
  gap: var(--space-3);
992
+ padding-top: var(--space-2);
993
+ border-top: var(--border-width) solid var(--border);
442
994
  }
995
+
443
996
  .composer-hint {
997
+ margin-left: auto;
444
998
  color: var(--muted);
445
999
  font-size: var(--text-xs);
446
1000
  }
1001
+
1002
+ .composer-hint kbd + kbd {
1003
+ margin-left: var(--space-1);
1004
+ }
1005
+
447
1006
  .composer-error {
448
1007
  margin: 0;
449
1008
  color: var(--bad);
450
1009
  font-size: var(--text-xs);
451
1010
  }
1011
+
1012
+ /* ---------------------------------------------------------------- shortcuts */
1013
+
1014
+ .keys {
1015
+ display: grid;
1016
+ gap: var(--space-2);
1017
+ margin: 0;
1018
+ }
1019
+
1020
+ .keys-row {
1021
+ display: grid;
1022
+ grid-template-columns: 8rem 1fr;
1023
+ align-items: baseline;
1024
+ gap: var(--space-3);
1025
+ }
1026
+
1027
+ .keys dt {
1028
+ margin: 0;
1029
+ }
1030
+
1031
+ .keys dd {
1032
+ margin: 0;
1033
+ color: var(--muted);
1034
+ font-size: var(--text-xs);
1035
+ }
1036
+
1037
+ /* --------------------------------------------------------------- the widths */
1038
+
1039
+ /* One column. The list and the message are the same space, so the shell
1040
+ swaps them; what is left here is the chrome that has to give way. */
1041
+ .mail.is-narrow .threads,
1042
+ .mail.is-narrow .thread {
1043
+ border-right: none;
1044
+ }
1045
+
1046
+ .mail.is-narrow .mail-bar {
1047
+ grid-template-columns: auto 1fr auto;
1048
+ }
1049
+
1050
+ /* The signed-in address goes, and the disc stays. The account slot is the
1051
+ application's -- `Mail` is handed it as a node and cannot re-render it
1052
+ narrower -- but the width is the shell's problem, so the shell hides the
1053
+ text. Reaching into `Identity`'s classes is the cost of that split; it is
1054
+ a display rule and the name stays in the accessibility tree. */
1055
+ .mail.is-narrow .mail-bar-end .ident-text,
1056
+ .mail.is-medium .mail-bar-end .ident-text {
1057
+ display: none;
1058
+ }
1059
+
1060
+ .mail.is-narrow .mail-compose-word {
1061
+ /* The pen alone. The button keeps its name from the `aria-label` the icon
1062
+ is given, so this is a visual change and not a semantic one. */
1063
+ display: none;
1064
+ }
1065
+
1066
+ .mail.is-narrow .thread-head {
1067
+ /* The toolbar wraps under the subject rather than squeezing it to nothing:
1068
+ six icon buttons and a two-line subject do not fit on 390px. */
1069
+ flex-wrap: wrap;
1070
+ }
1071
+
1072
+ /* One column has room for a name and a date, and not for a preview as well.
1073
+ The preview is the thing that goes: the name and the time are what a
1074
+ collapsed row is for. */
1075
+ .mail.is-narrow .message-peek {
1076
+ display: none;
1077
+ }
1078
+
1079
+ .mail.is-narrow .message-body {
1080
+ padding-inline: var(--space-3);
1081
+ }
1082
+
1083
+ .mail.is-narrow .thread-main {
1084
+ padding-inline: var(--space-3) var(--space-3);
1085
+ }
1086
+
1087
+ @media (max-width: 520px) {
1088
+ .mail-bar {
1089
+ gap: var(--space-2);
1090
+ padding: var(--space-2);
1091
+ }
1092
+
1093
+ .threads-head,
1094
+ .thread-head {
1095
+ padding-inline: var(--space-3);
1096
+ }
1097
+ }