@novacraft-engineering/mailbox 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +7 -2
  2. package/app/api/mail/accessors/route.ts +28 -24
  3. package/app/api/mail/emails/[id]/attachments/download/route.ts +3 -1
  4. package/app/api/mail/emails/[id]/attachments/forward/route.ts +3 -1
  5. package/app/api/mail/emails/[id]/route.ts +17 -9
  6. package/app/api/mail/emails/route.ts +35 -4
  7. package/app/api/mail/inbox/counts/route.ts +5 -4
  8. package/app/api/mail/inbox/route.ts +41 -262
  9. package/app/api/mail/maintenance/received/route.ts +64 -0
  10. package/app/api/mail/pixel/[id]/route.ts +8 -2
  11. package/app/api/mail/request-reset/route.ts +29 -25
  12. package/app/api/mail/signature-logo/route.ts +33 -8
  13. package/app/api/mail/threads/route.ts +1 -1
  14. package/app/brand/[file]/placeholder.ts +70 -0
  15. package/app/brand/[file]/route.ts +7 -1
  16. package/app/layout.tsx +10 -2
  17. package/app/mail/MailSelect.tsx +23 -4
  18. package/app/mail/RichEditor.tsx +167 -53
  19. package/app/mail/Ticker.tsx +40 -0
  20. package/app/mail/page.module.css +643 -76
  21. package/app/mail/page.tsx +1062 -247
  22. package/app/mail/search.ts +1 -1
  23. package/app/share/[id]/page.tsx +5 -3
  24. package/app/share/[id]/share.module.css +5 -5
  25. package/lib/email-html.ts +9 -1
  26. package/lib/emails/index.ts +26 -0
  27. package/lib/emails/templates/action.html +47 -0
  28. package/lib/fonts.ts +7 -1
  29. package/lib/mail-provider.ts +50 -5
  30. package/lib/mailbox.ts +303 -34
  31. package/lib/receive.ts +318 -0
  32. package/lib/sent-access.ts +22 -0
  33. package/lib/ses-send.ts +49 -0
  34. package/package.json +2 -2
  35. package/public/brand/README.md +5 -1
  36. package/scripts/sample-files.mjs +208 -28
  37. package/scripts/seed-dev.mjs +69 -26
  38. package/tools/README.md +1 -1
@@ -515,6 +515,18 @@
515
515
  transition: color var(--dur-fast) var(--nc-ease-out), border-color var(--dur-fast) var(--nc-ease-out);
516
516
  }
517
517
  .refreshBtn:hover { color: var(--nc-fg-1); border-color: var(--nc-line-2); }
518
+ .crossFolderHint {
519
+ align-self: flex-start;
520
+ margin: 0 16px 8px;
521
+ padding: 6px 11px;
522
+ border: 1px solid var(--nc-line-1);
523
+ border-radius: 999px;
524
+ background: var(--nc-ink-2);
525
+ color: var(--nc-violet-500);
526
+ font: 500 12px/1 var(--nc-font-sans);
527
+ cursor: pointer;
528
+ }
529
+ .crossFolderHint:hover { border-color: var(--nc-violet-500); }
518
530
  .refreshBtn svg { width: 14px; height: 14px; }
519
531
  .filterChip {
520
532
  margin-left: auto;
@@ -652,12 +664,187 @@
652
664
  cursor: progress;
653
665
  }
654
666
 
667
+ /* Floats over the list rather than in it, so it does not move as rows load. The pane is
668
+ the positioning context; the list itself scrolls underneath. */
669
+ .toTop {
670
+ position: absolute;
671
+ right: 16px;
672
+ bottom: 18px;
673
+ z-index: 4;
674
+ width: 34px;
675
+ height: 34px;
676
+ display: grid;
677
+ place-items: center;
678
+ border: none;
679
+ border-radius: 999px;
680
+ background: var(--nc-violet-500);
681
+ color: #fff;
682
+ cursor: pointer;
683
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.22);
684
+ opacity: 0;
685
+ transform: translateY(8px);
686
+ pointer-events: none;
687
+ transition: opacity 140ms ease, transform 140ms ease;
688
+ }
689
+ .toTop svg {
690
+ width: 18px;
691
+ height: 18px;
692
+ /* The only chevron in the set points down. */
693
+ transform: rotate(180deg);
694
+ }
695
+ .toTopOn {
696
+ opacity: 1;
697
+ transform: translateY(0);
698
+ pointer-events: auto;
699
+ }
700
+ .toTop:hover {
701
+ background: var(--nc-violet-400);
702
+ }
703
+ .toTop:focus-visible {
704
+ outline: 2px solid var(--nc-violet-500);
705
+ outline-offset: 2px;
706
+ }
707
+ @media (prefers-reduced-motion: reduce) {
708
+ .toTop {
709
+ transition: none;
710
+ }
711
+ }
712
+
655
713
  .list {
656
714
  flex: 1;
657
715
  min-height: 0;
658
716
  overflow-y: auto;
659
717
  overscroll-behavior: contain;
660
718
  padding: 0 10px 16px;
719
+ /* Firefox, and Chrome from 121. Where these are honoured the WebKit rules below are
720
+ ignored outright, so the two have to describe the same scrollbar rather than
721
+ combine into one. */
722
+ scrollbar-width: thin;
723
+ scrollbar-color: color-mix(in srgb, var(--nc-violet-500) 55%, transparent) transparent;
724
+ }
725
+ .snoozeWrap {
726
+ position: relative;
727
+ display: inline-flex;
728
+ }
729
+ .snoozeMenu {
730
+ position: absolute;
731
+ top: calc(100% + 6px);
732
+ right: 0;
733
+ z-index: 60;
734
+ min-width: 232px;
735
+ padding: 6px;
736
+ display: flex;
737
+ flex-direction: column;
738
+ gap: 2px;
739
+ background: var(--nc-ink-1);
740
+ border: 1px solid var(--nc-line-2);
741
+ border-radius: var(--radius-card);
742
+ box-shadow: 0 18px 44px -18px rgba(0, 0, 0, .65);
743
+ }
744
+ .snoozeItem {
745
+ display: flex;
746
+ align-items: center;
747
+ justify-content: space-between;
748
+ gap: 16px;
749
+ min-height: 38px;
750
+ padding: 0 10px;
751
+ border: none;
752
+ border-radius: var(--radius-control);
753
+ background: transparent;
754
+ color: var(--nc-fg-1);
755
+ font: 500 13px/1.3 var(--nc-font-sans);
756
+ cursor: pointer;
757
+ text-align: left;
758
+ }
759
+ .snoozeItem:hover { background: var(--nc-ink-3); }
760
+ .snoozeWhen {
761
+ font: 500 12px/1.3 var(--nc-font-mono);
762
+ color: var(--nc-fg-3);
763
+ white-space: nowrap;
764
+ }
765
+
766
+ /* One band, moved to whichever row is being dragged, rather than a wrapper per row: the
767
+ rows are staggered on entry by :nth-child and wrapping them would reset every one of
768
+ those to the first position. */
769
+ .swipeBand {
770
+ position: absolute;
771
+ left: 10px;
772
+ right: 10px;
773
+ display: flex;
774
+ align-items: center;
775
+ padding: 0 20px;
776
+ border-radius: 12px;
777
+ font: 600 13px/1 var(--nc-font-sans);
778
+ color: #fff;
779
+ pointer-events: none;
780
+ }
781
+ .swipeBandRight {
782
+ justify-content: flex-start;
783
+ background: color-mix(in srgb, var(--nc-violet-500) 55%, transparent);
784
+ }
785
+ .swipeBandLeft {
786
+ justify-content: flex-end;
787
+ background: color-mix(in srgb, var(--nc-danger) 55%, transparent);
788
+ }
789
+ .swipeBandArmed.swipeBandRight { background: var(--nc-violet-500); }
790
+ .swipeBandArmed.swipeBandLeft { background: var(--nc-danger); }
791
+
792
+ .list {
793
+ position: relative;
794
+ }
795
+ /* The row rides the finger, so it must not animate back on every frame. */
796
+ .item {
797
+ touch-action: pan-y;
798
+ }
799
+
800
+ /* Sits between the header and the rows, growing out of the gap as the list is pulled
801
+ down, so the gesture has something to answer it. */
802
+ .pullHint {
803
+ display: flex;
804
+ align-items: center;
805
+ justify-content: center;
806
+ overflow: hidden;
807
+ transition: opacity 120ms ease;
808
+ }
809
+ .pullSpinner {
810
+ display: grid;
811
+ place-items: center;
812
+ width: 30px;
813
+ height: 30px;
814
+ border-radius: 999px;
815
+ background: var(--nc-ink-2);
816
+ border: 1px solid var(--nc-line-1);
817
+ color: var(--nc-fg-3);
818
+ }
819
+ .pullSpinner svg {
820
+ width: 16px;
821
+ height: 16px;
822
+ }
823
+ .pullSpinnerReady {
824
+ background: var(--nc-violet-500);
825
+ border-color: var(--nc-violet-500);
826
+ color: #fff;
827
+ }
828
+
829
+ /* Safari, and older Chrome. */
830
+ .list::-webkit-scrollbar {
831
+ width: 10px;
832
+ }
833
+ .list::-webkit-scrollbar-track {
834
+ background: transparent;
835
+ }
836
+ .list::-webkit-scrollbar-thumb {
837
+ /* The transparent border plus padding-box clipping is what insets the thumb: there is
838
+ no padding property on a scrollbar, and a full-width bar against the rows reads as
839
+ part of the list rather than a control over it. */
840
+ background: color-mix(in srgb, var(--nc-violet-500) 45%, transparent);
841
+ background-clip: padding-box;
842
+ border: 3px solid transparent;
843
+ border-radius: 999px;
844
+ }
845
+ .list::-webkit-scrollbar-thumb:hover {
846
+ background: color-mix(in srgb, var(--nc-violet-500) 78%, transparent);
847
+ background-clip: padding-box;
661
848
  }
662
849
  .app[data-density='relaxed'] {
663
850
  --row-pad: 18px 16px;
@@ -776,9 +963,9 @@
776
963
  color: var(--nc-success);
777
964
  border-color: color-mix(in srgb, var(--nc-success) 40%, transparent);
778
965
  }
779
- .chipScheduled { color: var(--nc-warn); border-color: rgba(251,191,36,.3); }
966
+ .chipScheduled { color: var(--nc-warn); border-color: color-mix(in srgb, var(--nc-warn) 30%, transparent); }
780
967
  .chipDelivered { color: var(--nc-success); border-color: rgba(52,211,153,.3); }
781
- .chipBounced { color: var(--nc-danger); border-color: rgba(248,113,113,.3); }
968
+ .chipBounced { color: var(--nc-danger); border-color: color-mix(in srgb, var(--nc-danger) 30%, transparent); }
782
969
  .chipDraft { color: var(--nc-violet-200); border-color: rgba(165,120,255,.3); }
783
970
 
784
971
  .emptyList {
@@ -918,8 +1105,8 @@
918
1105
  }
919
1106
  .actionBtn:hover { color: var(--nc-fg-1); border-color: var(--nc-line-2); background: var(--nc-ink-2); }
920
1107
  .actionBtn svg { width: 13px; height: 13px; }
921
- .actionDanger { color: var(--nc-danger); border-color: rgba(248,113,113,.3); }
922
- .actionDanger:hover { color: var(--nc-danger-soft); border-color: rgba(248,113,113,.55); background: rgba(248,113,113,.06); }
1108
+ .actionDanger { color: var(--nc-danger); border-color: color-mix(in srgb, var(--nc-danger) 30%, transparent); }
1109
+ .actionDanger:hover { color: var(--nc-danger-soft); border-color: color-mix(in srgb, var(--nc-danger) 55%, transparent); background: color-mix(in srgb, var(--nc-danger) 6%, transparent); }
923
1110
  .countdownChip {
924
1111
  display: inline-flex;
925
1112
  align-items: center;
@@ -928,7 +1115,7 @@
928
1115
  font-size: 12px;
929
1116
  color: var(--nc-warn);
930
1117
  padding: 8px 12px;
931
- border: 1px solid rgba(251,191,36,.3);
1118
+ border: 1px solid color-mix(in srgb, var(--nc-warn) 30%, transparent);
932
1119
  border-radius: 9px;
933
1120
  }
934
1121
  .activity {
@@ -1096,12 +1283,11 @@
1096
1283
  padding: 10px 0 2px;
1097
1284
  }
1098
1285
  .attachFloatBody .attachGrid { margin: 0 14px 10px; }
1099
- /* One column down the sheet: the text is capped and centred for readability, so anything
1100
- that follows it lines up with it instead of hugging the far left of a wide pane. */
1286
+ /* Tiles sit in the sheet's own bottom-left corner, flush with its border, the way they
1287
+ did before the sheet became a centred page. The page stays centred; only the tiles
1288
+ went back to the corner. */
1101
1289
  .threadMsgBody .attachGrid {
1102
- max-width: none;
1103
- margin: 0;
1104
- padding: 0 34px 30px;
1290
+ margin: 12px 0 0;
1105
1291
  }
1106
1292
  .attachPillWrap {
1107
1293
  position: absolute;
@@ -1131,23 +1317,20 @@
1131
1317
  .readerBody {
1132
1318
  flex: 1;
1133
1319
  margin: 0 28px 24px;
1134
- border-radius: var(--radius-card);
1135
- border: 1px solid var(--nc-line-1);
1136
1320
  overflow: hidden;
1137
- background: var(--nc-ink-1);
1138
1321
  min-height: 0;
1139
1322
  }
1140
1323
  .readerFrame {
1141
1324
  width: 100%;
1142
1325
  height: 100%;
1143
1326
  border: none;
1144
- background: var(--nc-ink-1);
1327
+ background: transparent;
1145
1328
  }
1146
1329
  .readerText {
1147
- max-width: 800px;
1148
- margin: 0 auto;
1149
- padding: 30px 34px;
1150
- font: 400 15px/1.7 var(--nc-font-sans);
1330
+ max-width: 860px;
1331
+ margin: 0;
1332
+ padding: 8px 0 24px;
1333
+ font: 400 15px/var(--mail-lead, 1.3) var(--nc-font-sans);
1151
1334
  color: var(--nc-fg-1);
1152
1335
  white-space: pre-wrap;
1153
1336
  word-break: break-word;
@@ -1156,7 +1339,6 @@
1156
1339
  overflow-y: auto;
1157
1340
  /* A plain-text part carries no styling of its own, so it follows the app rather
1158
1341
  than sitting on the white paper an HTML email brings with it. */
1159
- background: var(--nc-ink-2);
1160
1342
  -webkit-font-smoothing: antialiased;
1161
1343
  }
1162
1344
  .readerSource {
@@ -1186,7 +1368,73 @@
1186
1368
  padding: 10px 16px calc(12px + env(safe-area-inset-bottom, 0px));
1187
1369
  background: var(--nc-ink-0);
1188
1370
  border-top: 1px solid var(--nc-line-1);
1371
+ animation: ncReplyIn .24s cubic-bezier(.2, .7, .3, 1) both;
1189
1372
  }
1373
+ @keyframes ncReplyIn {
1374
+ from { opacity: 0; transform: translateY(14px); }
1375
+ to { opacity: 1; transform: none; }
1376
+ }
1377
+ @media (prefers-reduced-motion: reduce) {
1378
+ .quickReplyBar { animation: none; }
1379
+ }
1380
+ /* The phone's stand-in for the docked reply: one line that opens the composer as a page.
1381
+ It keeps the dock's place at the foot of the reader so the layout does not shift. */
1382
+ .replyLaunch {
1383
+ flex: 0 0 auto;
1384
+ margin-top: auto;
1385
+ position: sticky;
1386
+ bottom: 0;
1387
+ z-index: 6;
1388
+ display: flex;
1389
+ gap: 8px;
1390
+ padding: 10px 16px calc(12px + env(safe-area-inset-bottom, 0px));
1391
+ background: var(--nc-ink-0);
1392
+ border-top: 1px solid var(--nc-line-1);
1393
+ }
1394
+ .replyLaunchBtn {
1395
+ flex: 1;
1396
+ min-width: 0;
1397
+ display: flex;
1398
+ align-items: center;
1399
+ gap: 10px;
1400
+ min-height: 44px;
1401
+ padding: 0 16px;
1402
+ border-radius: 12px;
1403
+ border: 1px solid var(--nc-line-1);
1404
+ background: var(--nc-ink-2);
1405
+ color: var(--nc-fg-2);
1406
+ font: 400 14px/1.4 var(--nc-font-sans);
1407
+ text-align: left;
1408
+ cursor: pointer;
1409
+ }
1410
+ .replyLaunchBtn span {
1411
+ min-width: 0;
1412
+ overflow: hidden;
1413
+ white-space: nowrap;
1414
+ text-overflow: ellipsis;
1415
+ }
1416
+ .replyLaunchBtn svg {
1417
+ flex: 0 0 auto;
1418
+ width: 16px;
1419
+ height: 16px;
1420
+ color: var(--nc-fg-3);
1421
+ }
1422
+ .replyLaunchAll {
1423
+ flex: 0 0 44px;
1424
+ height: 44px;
1425
+ display: grid;
1426
+ place-items: center;
1427
+ border-radius: 12px;
1428
+ border: 1px solid var(--nc-line-1);
1429
+ background: transparent;
1430
+ color: var(--nc-fg-2);
1431
+ cursor: pointer;
1432
+ }
1433
+ .replyLaunchAll svg {
1434
+ width: 17px;
1435
+ height: 17px;
1436
+ }
1437
+
1190
1438
  .replyRecips {
1191
1439
  display: flex;
1192
1440
  flex-direction: column;
@@ -1433,7 +1681,7 @@
1433
1681
  border-radius: 6px;
1434
1682
  background: var(--nc-ink-1);
1435
1683
  color: var(--nc-fg-1);
1436
- font: 500 12px/1 var(--nc-font-sans);
1684
+ font: 500 11px/1 var(--nc-font-sans);
1437
1685
  }
1438
1686
  .rteSwatches { display: flex; gap: 3px; }
1439
1687
  .rteSwatch {
@@ -1445,6 +1693,58 @@
1445
1693
  padding: 0;
1446
1694
  }
1447
1695
  .rteSwatchHi { border-radius: 50%; }
1696
+ .tickerHost {
1697
+ display: inline-block;
1698
+ max-width: 100%;
1699
+ min-width: 0;
1700
+ overflow: hidden;
1701
+ white-space: nowrap;
1702
+ text-overflow: ellipsis;
1703
+ vertical-align: bottom;
1704
+ }
1705
+ .tickerBlock { display: block; }
1706
+ .tickerHost[data-ticker='on'] { text-overflow: clip; }
1707
+ .tickerText { display: inline-block; white-space: nowrap; }
1708
+ .tickerHost[data-ticker='on'] .tickerText { animation: ncTicker var(--ticker-duration, 8s) ease-in-out var(--ticker-delay, 0s) infinite alternate; }
1709
+ @keyframes ncTicker {
1710
+ 0%, 14% { transform: translateX(0); }
1711
+ 86%, 100% { transform: translateX(calc(-1 * var(--ticker-shift, 0px))); }
1712
+ }
1713
+ @media (prefers-reduced-motion: reduce) {
1714
+ .tickerHost[data-ticker='on'] .tickerText { animation: none; }
1715
+ .tickerHost[data-ticker='on'] { text-overflow: ellipsis; }
1716
+ }
1717
+ .itemSubject { display: flex; align-items: center; gap: 4px; }
1718
+ .itemSubjectText { flex: 1; }
1719
+ .threadMsgFrom { display: inline-flex; align-items: center; gap: 6px; max-width: 100%; }
1720
+ .threadMsgFromText { flex: 1; }
1721
+ .rteColourDot {
1722
+ display: inline-block;
1723
+ width: 16px;
1724
+ height: 16px;
1725
+ border-radius: 50%;
1726
+ background: conic-gradient(#b91c1c, #b45309, #047857, #1d4ed8, #6d28d9, #b91c1c);
1727
+ }
1728
+ .rtePalette {
1729
+ display: flex;
1730
+ align-items: center;
1731
+ gap: 10px;
1732
+ padding: 0 12px;
1733
+ max-height: 0;
1734
+ opacity: 0;
1735
+ overflow: hidden;
1736
+ transform: translateY(-6px);
1737
+ background: var(--nc-ink-2);
1738
+ transition: max-height .28s var(--nc-ease-out), opacity .22s var(--nc-ease-out), transform .28s var(--nc-ease-out), padding .28s var(--nc-ease-out);
1739
+ }
1740
+ .rtePaletteOpen {
1741
+ max-height: 60px;
1742
+ opacity: 1;
1743
+ transform: none;
1744
+ padding: 8px 12px;
1745
+ border-bottom: 1px solid var(--nc-line-1);
1746
+ }
1747
+ .rtePaletteGroup { display: flex; align-items: center; gap: 6px; }
1448
1748
  .rteContent {
1449
1749
  flex: 1;
1450
1750
  min-height: 0;
@@ -1457,23 +1757,24 @@
1457
1757
  min-height: 260px;
1458
1758
  padding: 20px 22px;
1459
1759
  outline: none;
1460
- font: 400 var(--rte-size, 15px)/1.65 var(--rte-font, Arial, Helvetica, sans-serif);
1760
+ font: 400 var(--rte-size, 15px)/var(--rte-lead, 1.3) var(--rte-font, Arial, Helvetica, sans-serif);
1461
1761
  color: var(--nc-fg-1);
1462
1762
  }
1463
- .rteSurface p { margin: 0 0 14px; }
1763
+ .rteSurface p { margin: 0 0 var(--rte-gap, 8px); }
1464
1764
  .rteSurface h1 { font-size: 24px; margin: 22px 0 12px; }
1465
1765
  .rteSurface h2 { font-size: 20px; margin: 20px 0 10px; }
1466
1766
  .rteSurface h3 { font-size: 17px; margin: 18px 0 8px; }
1467
- .rteSurface ul, .rteSurface ol { margin: 0 0 14px; padding-left: 22px; }
1767
+ .rteSurface ul, .rteSurface ol { margin: 0 0 var(--rte-gap, 8px); padding-left: 22px; }
1468
1768
  .rteSurface li { margin: 0 0 6px; }
1469
1769
  .rteSurface a { color: var(--nc-violet-500); text-decoration: underline; }
1470
1770
  .rteSurface blockquote {
1471
- margin: 0 0 14px;
1771
+ margin: 0 0 var(--rte-gap, 8px);
1472
1772
  padding-left: 14px;
1473
1773
  border-left: 3px solid var(--nc-line-2);
1474
1774
  color: var(--nc-fg-3);
1475
1775
  }
1476
1776
  .rteSurface pre {
1777
+ margin: 0 0 var(--rte-gap, 8px);
1477
1778
  background: var(--nc-ink-3);
1478
1779
  padding: 12px 14px;
1479
1780
  border-radius: 6px;
@@ -1481,7 +1782,7 @@
1481
1782
  font: 400 13px/1.5 var(--nc-font-mono);
1482
1783
  }
1483
1784
  .rteSurface img { max-width: 100%; height: auto; }
1484
- .rteImageWrap { position: relative; margin: 0 0 14px; }
1785
+ .rteImageWrap { position: relative; margin: 0 0 var(--rte-gap, 8px); }
1485
1786
  .rteImageWrap[data-align='center'] { text-align: center; }
1486
1787
  .rteImageWrap[data-align='right'] { text-align: right; }
1487
1788
  .rteImageBox {
@@ -1563,7 +1864,7 @@
1563
1864
  font-size: 11px;
1564
1865
  color: var(--nc-fg-2);
1565
1866
  }
1566
- .rteSurface table { border-collapse: collapse; margin: 0 0 14px; width: 100%; }
1867
+ .rteSurface table { border-collapse: collapse; margin: 0 0 var(--rte-gap, 8px); width: 100%; }
1567
1868
  .rteSurface td, .rteSurface th {
1568
1869
  border: 1px solid var(--nc-line-2);
1569
1870
  padding: 7px 10px;
@@ -1607,8 +1908,8 @@
1607
1908
  max-height: 64px;
1608
1909
  margin-bottom: 10px;
1609
1910
  }
1610
- .composeSignatureBody { font: 400 13px/1.6 var(--nc-font-sans); color: var(--nc-fg-2); }
1611
- .composeSignatureBody p { margin: 0 0 6px; }
1911
+ .composeSignatureBody { font: 400 13px/1 var(--nc-font-sans); color: var(--nc-fg-2); }
1912
+ .composeSignatureBody p { margin: 0 0 2px; }
1612
1913
 
1613
1914
  /* Logo picker in settings. */
1614
1915
  .sigLogoRow {
@@ -1857,7 +2158,7 @@
1857
2158
  padding: 0 2px;
1858
2159
  }
1859
2160
  .addrChip button:hover { color: var(--nc-danger); }
1860
- .addrChipBad { border-color: rgba(248,113,113,.5); color: var(--nc-danger-soft); }
2161
+ .addrChipBad { border-color: color-mix(in srgb, var(--nc-danger) 50%, transparent); color: var(--nc-danger-soft); }
1861
2162
 
1862
2163
  .toolbar {
1863
2164
  display: flex;
@@ -1918,6 +2219,24 @@
1918
2219
  transition: border-color var(--dur-fast) var(--nc-ease-out), color var(--dur-fast) var(--nc-ease-out);
1919
2220
  }
1920
2221
  .selectBtn:hover { border-color: var(--nc-line-2); color: var(--nc-fg-1); }
2222
+ .selectGlyph {
2223
+ flex-shrink: 0;
2224
+ display: inline-flex;
2225
+ align-items: center;
2226
+ justify-content: center;
2227
+ width: 16px;
2228
+ height: 16px;
2229
+ font: 700 11px/1 var(--nc-font-sans);
2230
+ letter-spacing: .01em;
2231
+ color: var(--nc-fg-3);
2232
+ }
2233
+ .selectGlyph svg { width: 13px; height: 13px; }
2234
+ .selectGlyphSerif { font: 700 13px/1 Georgia, 'Times New Roman', serif; }
2235
+ .selectLabel {
2236
+ flex-shrink: 0;
2237
+ color: var(--nc-fg-3);
2238
+ white-space: nowrap;
2239
+ }
1921
2240
  .selectBtn > span {
1922
2241
  overflow: hidden;
1923
2242
  text-overflow: ellipsis;
@@ -1960,6 +2279,7 @@
1960
2279
  .selectOptionOn { background: var(--nc-ink-4); color: var(--nc-violet-200); }
1961
2280
 
1962
2281
  .toolbar .selectBtn { padding: 7px 10px; font-size: 12px; }
2282
+ .selectBtn.rteSelect { font-size: 11.5px; }
1963
2283
  .campaignSelectBtn {
1964
2284
  width: 100%;
1965
2285
  justify-content: space-between;
@@ -2029,7 +2349,7 @@
2029
2349
  border-radius: 7px;
2030
2350
  transition: color var(--dur-fast) var(--nc-ease-out), background-color var(--dur-fast) var(--nc-ease-out);
2031
2351
  }
2032
- .tplDelete:hover { color: var(--nc-danger); background: rgba(248, 113, 113, .08); }
2352
+ .tplDelete:hover { color: var(--nc-danger); background: color-mix(in srgb, var(--nc-danger) 8%, transparent); }
2033
2353
  .tplSaveRow {
2034
2354
  display: flex;
2035
2355
  gap: 6px;
@@ -2079,9 +2399,9 @@
2079
2399
  border: none;
2080
2400
  background: transparent;
2081
2401
  color: var(--nc-fg-3);
2082
- font: 600 12px/1 var(--nc-font-sans);
2083
- padding: 6px 10px;
2084
- border-radius: 6px;
2402
+ font: 600 11.5px/1 var(--nc-font-sans);
2403
+ padding: 4px 9px;
2404
+ border-radius: 5px;
2085
2405
  cursor: pointer;
2086
2406
  transition: background-color var(--dur-fast) var(--nc-ease-out), color var(--dur-fast) var(--nc-ease-out);
2087
2407
  }
@@ -2093,16 +2413,16 @@
2093
2413
  display: flex;
2094
2414
  align-items: center;
2095
2415
  justify-content: space-between;
2096
- gap: 10px;
2416
+ gap: 8px;
2097
2417
  flex-wrap: wrap;
2098
- margin: 0 28px 12px;
2418
+ margin: 0 28px 8px;
2099
2419
  }
2100
2420
  .readerModeTabs {
2101
2421
  display: inline-flex;
2102
2422
  gap: 2px;
2103
2423
  background: var(--nc-ink-2);
2104
2424
  border: 1px solid var(--nc-line-1);
2105
- border-radius: 8px;
2425
+ border-radius: 7px;
2106
2426
  padding: 2px;
2107
2427
  }
2108
2428
 
@@ -2235,18 +2555,21 @@
2235
2555
  }
2236
2556
  .threadMsg {
2237
2557
  flex: 0 0 auto;
2238
- border: 1px solid var(--nc-line-1);
2239
- border-radius: var(--radius-card);
2558
+ border: 0;
2559
+ border-radius: 0;
2240
2560
  overflow: hidden;
2241
- background: var(--nc-ink-1);
2561
+ background: transparent;
2242
2562
  }
2243
- .threadMsgOpen { border-color: var(--nc-line-2); }
2563
+ .threadMsg + .threadMsg { border-top: 1px solid var(--nc-line-1); }
2564
+ .threadMsgOpen .threadMsgHead { border-bottom: 1px solid var(--nc-line-1); }
2244
2565
  .threadMsgHead {
2245
2566
  display: flex;
2246
2567
  align-items: center;
2247
2568
  gap: 12px;
2248
2569
  width: 100%;
2249
2570
  padding: var(--thread-pad);
2571
+ padding-left: 0;
2572
+ padding-right: 0;
2250
2573
  background: transparent;
2251
2574
  border: none;
2252
2575
  cursor: pointer;
@@ -2287,12 +2610,32 @@
2287
2610
  .threadClip svg { width: 13px; height: 13px; }
2288
2611
  /* ── Attachment tiles ────────────────────────────────────── */
2289
2612
  .attachGrid {
2290
- display: grid;
2291
- grid-template-columns: repeat(auto-fill, minmax(278px, 1fr));
2613
+ display: flex;
2292
2614
  gap: 8px;
2293
2615
  margin: 0 28px 14px;
2616
+ overflow-x: auto;
2617
+ scroll-snap-type: x proximity;
2618
+ padding-bottom: 6px;
2619
+ scrollbar-width: thin;
2620
+ scrollbar-color: color-mix(in srgb, var(--nc-violet-500) 55%, transparent) transparent;
2621
+ }
2622
+ .attachGrid::-webkit-scrollbar { height: 8px; }
2623
+ .readerAttach .attachGrid { margin: 0; flex: 1 1 100%; }
2624
+ .attachGrid::-webkit-scrollbar-track { background: transparent; }
2625
+ .attachGrid::-webkit-scrollbar-thumb {
2626
+ background: color-mix(in srgb, var(--nc-violet-500) 45%, transparent);
2627
+ background-clip: padding-box;
2628
+ border: 2px solid transparent;
2629
+ border-radius: 999px;
2630
+ }
2631
+ .attachGrid::-webkit-scrollbar-thumb:hover {
2632
+ background: color-mix(in srgb, var(--nc-violet-500) 78%, transparent);
2633
+ background-clip: padding-box;
2294
2634
  }
2295
2635
  .attachTile {
2636
+ flex: 0 0 auto;
2637
+ width: min(300px, 78vw);
2638
+ scroll-snap-align: start;
2296
2639
  display: flex;
2297
2640
  align-items: center;
2298
2641
  gap: 9px;
@@ -2566,8 +2909,8 @@
2566
2909
  vertical-align: middle;
2567
2910
  white-space: nowrap;
2568
2911
  }
2569
- .linkBadgeWarn { color: var(--nc-warn); border: 1px solid rgba(251,191,36,.4); }
2570
- .linkBadgeBad { color: var(--nc-danger); border: 1px solid rgba(248,113,113,.45); }
2912
+ .linkBadgeWarn { color: var(--nc-warn); border: 1px solid color-mix(in srgb, var(--nc-warn) 40%, transparent); }
2913
+ .linkBadgeBad { color: var(--nc-danger); border: 1px solid color-mix(in srgb, var(--nc-danger) 45%, transparent); }
2571
2914
  .orderDivider {
2572
2915
  width: 1px;
2573
2916
  height: 16px;
@@ -2576,12 +2919,12 @@
2576
2919
  flex-shrink: 0;
2577
2920
  }
2578
2921
  .orderToggle {
2579
- padding: 5px 10px;
2580
- border-radius: 7px;
2922
+ padding: 3px 8px;
2923
+ border-radius: 6px;
2581
2924
  border: 1px solid var(--nc-line-1);
2582
2925
  background: transparent;
2583
2926
  color: var(--nc-fg-2);
2584
- font: 600 11.5px/1 var(--nc-font-mono);
2927
+ font: 600 11px/1 var(--nc-font-mono);
2585
2928
  cursor: pointer;
2586
2929
  white-space: nowrap;
2587
2930
  transition: color var(--dur-fast) var(--nc-ease-out), border-color var(--dur-fast) var(--nc-ease-out), background-color var(--dur-fast) var(--nc-ease-out);
@@ -2611,18 +2954,8 @@
2611
2954
  all four sides, so you can see where the mail begins and where it ends rather than a
2612
2955
  band of white running into the furniture above it. */
2613
2956
  .threadMsgBody {
2614
- /* A page, not a wall: the paper is the width of the column it carries and sits centred in
2615
- the pane, rather than stretching the full width with the text stranded in the middle. */
2616
- /* Auto margins on a grid item shrink it to its content, which sized the page to whatever
2617
- the longest line happened to be. The width is stated, and the margins only centre it. */
2618
- width: min(868px, calc(100% - var(--thread-inset) * 2));
2619
- margin: var(--thread-inset) auto;
2620
- border: 1px solid var(--nc-line-1);
2621
- border-radius: var(--radius-card);
2622
- /* The sheet is the paper the mail is printed on, so it is the paper's colour. Left on the
2623
- card's own tone it vanished into the card, and the only edge you could see was the one
2624
- the message's own white background drew, square, wherever its markup happened to start. */
2625
- background: var(--nc-ink-0);
2957
+ width: 100%;
2958
+ margin: 0 0 8px;
2626
2959
  font-size: var(--row-subject);
2627
2960
  line-height: var(--row-lead);
2628
2961
  overflow: hidden;
@@ -2631,7 +2964,7 @@
2631
2964
  width: 100%;
2632
2965
  min-height: 120px;
2633
2966
  border: none;
2634
- background: var(--nc-ink-1);
2967
+ background: transparent;
2635
2968
  display: block;
2636
2969
  }
2637
2970
  .threadMsgBody .readerText,
@@ -2645,9 +2978,10 @@
2645
2978
  .threadMsgBody .readerText,
2646
2979
  .threadMsgBody .readerSource {
2647
2980
  max-width: none;
2981
+ padding: 6px 0 12px;
2648
2982
  margin: 0;
2649
2983
  }
2650
- .threadMsgBody .readerText { padding-bottom: 16px; }
2984
+ .threadMsgBody .readerText { padding-bottom: 16px; padding-left: 0; padding-right: 0; }
2651
2985
  .threadMsgBody .readerAttach { padding-top: 14px; }
2652
2986
 
2653
2987
  .campaign {
@@ -3014,6 +3348,11 @@
3014
3348
  font: 500 14px/1 var(--nc-font-sans);
3015
3349
  color: var(--nc-fg-1);
3016
3350
  }
3351
+ .toastError {
3352
+ border-color: color-mix(in srgb, var(--nc-danger) 45%, transparent);
3353
+ color: var(--nc-danger);
3354
+ }
3355
+
3017
3356
  .toastCount {
3018
3357
  font-family: var(--nc-font-mono);
3019
3358
  color: var(--nc-warn);
@@ -3097,9 +3436,9 @@
3097
3436
  flex-direction: column;
3098
3437
  gap: 7px;
3099
3438
  padding: 11px 13px;
3100
- border: 1px solid rgba(248, 113, 113, .32);
3439
+ border: 1px solid color-mix(in srgb, var(--nc-danger) 32%, transparent);
3101
3440
  border-radius: var(--radius-control);
3102
- background: rgba(248, 113, 113, .07);
3441
+ background: color-mix(in srgb, var(--nc-danger) 7%, transparent);
3103
3442
  }
3104
3443
  .loginError {
3105
3444
  font: 500 13px/1.45 var(--nc-font-sans);
@@ -3368,8 +3707,8 @@
3368
3707
  margin: 0 28px 14px;
3369
3708
  padding: 10px 14px;
3370
3709
  border-radius: 10px;
3371
- border: 1px solid rgba(251,191,36,.3);
3372
- background: rgba(251,191,36,.06);
3710
+ border: 1px solid color-mix(in srgb, var(--nc-warn) 30%, transparent);
3711
+ background: color-mix(in srgb, var(--nc-warn) 6%, transparent);
3373
3712
  color: var(--nc-fg-2);
3374
3713
  font: 500 13px/1.4 var(--nc-font-sans);
3375
3714
  }
@@ -3471,7 +3810,8 @@
3471
3810
  position: fixed;
3472
3811
  inset: 0;
3473
3812
  background: rgba(5, 3, 9, .55);
3474
- z-index: 440;
3813
+ /* Above .topRight, which floats at 500 and was landing squarely on the close button. */
3814
+ z-index: 530;
3475
3815
  display: flex;
3476
3816
  align-items: center;
3477
3817
  justify-content: center;
@@ -3767,6 +4107,41 @@
3767
4107
  gap: 15px;
3768
4108
  padding: var(--thread-pad);
3769
4109
  }
4110
+ /* The whole reader, drawn as shapes, for the moment between clicking a conversation and
4111
+ its messages arriving. It stands in for the header as well as the body, because that is
4112
+ what is missing: the pane otherwise keeps showing "Nothing open" or the last message. */
4113
+ .readerSkeleton {
4114
+ display: flex;
4115
+ flex-direction: column;
4116
+ gap: 18px;
4117
+ /* The real header, actions and thread all sit 28px in; anything else makes the page
4118
+ jump sideways the moment it replaces the skeleton. */
4119
+ padding: 22px 28px 0;
4120
+ }
4121
+ /* Scoped under the skeleton so they outrank the plain .skelBar rule, which comes later in
4122
+ the file and would otherwise flatten the subject back to a 13px line. */
4123
+ .readerSkeleton .skelSubject {
4124
+ height: 22px;
4125
+ width: 62%;
4126
+ border-radius: 8px;
4127
+ }
4128
+ .skelActions {
4129
+ display: flex;
4130
+ gap: 10px;
4131
+ }
4132
+ .skelActions .skelBar {
4133
+ width: 96px;
4134
+ height: 40px;
4135
+ border-radius: var(--radius-control);
4136
+ }
4137
+ .readerSkeleton .bodySkeleton {
4138
+ padding: 0;
4139
+ }
4140
+ /* The block placeholder is ink-2, which is white in the light theme — the same white the
4141
+ reader is painted on, so it vanished. ink-3 is a visible step in all three themes. */
4142
+ .readerSkeleton .skelBlock {
4143
+ background: var(--nc-ink-3);
4144
+ }
3770
4145
  .skelHeadRow {
3771
4146
  display: flex;
3772
4147
  align-items: center;
@@ -4463,7 +4838,7 @@
4463
4838
  gap: 2px;
4464
4839
  background: var(--nc-ink-2);
4465
4840
  border: 1px solid var(--nc-line-1);
4466
- border-radius: 8px;
4841
+ border-radius: 7px;
4467
4842
  padding: 2px;
4468
4843
  }
4469
4844
 
@@ -5016,6 +5391,27 @@
5016
5391
  animation: ncNoticeIn .26s cubic-bezier(.2, .7, .3, 1) both;
5017
5392
  will-change: transform, opacity;
5018
5393
  }
5394
+ /* The card behind peeks out above the one in front, so the next thing to deal with is
5395
+ visible without being readable or clickable until it comes forward. */
5396
+ .pwNoticeBehind {
5397
+ z-index: calc(75 - var(--depth, 1));
5398
+ transform: translateY(calc(var(--depth, 1) * -12px)) scale(calc(1 - var(--depth, 1) * .04));
5399
+ opacity: .92;
5400
+ pointer-events: none;
5401
+ animation: none;
5402
+ transition: transform .26s cubic-bezier(.2, .7, .3, 1);
5403
+ }
5404
+ .pwNoticeMore {
5405
+ position: absolute;
5406
+ top: -9px;
5407
+ right: 14px;
5408
+ padding: 3px 8px;
5409
+ border-radius: 999px;
5410
+ background: var(--nc-violet-500);
5411
+ color: #fff;
5412
+ font: 600 11px/1 var(--nc-font-sans);
5413
+ letter-spacing: .01em;
5414
+ }
5019
5415
 
5020
5416
  .pwNoticeTitle {
5021
5417
  margin: 0;
@@ -5364,7 +5760,7 @@
5364
5760
  }
5365
5761
  .sigPreviewOpen .sigPreviewFold { max-height: 2000px; }
5366
5762
  .sigPreviewBody {
5367
- font: 400 13px/1.6 var(--nc-font-sans);
5763
+ font: 400 13px/1 var(--nc-font-sans);
5368
5764
  color: var(--nc-fg-2);
5369
5765
  }
5370
5766
  .sigPreviewBody a { color: var(--nc-violet-500); }
@@ -5910,9 +6306,7 @@
5910
6306
  background: linear-gradient(to bottom, transparent 0%, var(--nc-ink-1) 78%);
5911
6307
  pointer-events: none;
5912
6308
  }
5913
- /* Its measured height, set by the element itself, so the reveal finishes with the
5914
- content rather than running on against an arbitrary ceiling. */
5915
- .composeSignatureOpen .composeSignatureBody { max-height: var(--sig-h, 1200px); cursor: default; }
6309
+ .composeSignatureOpen .composeSignatureBody { max-height: none; overflow: visible; cursor: default; }
5916
6310
  .composeSignatureOpen { padding-bottom: 30px; }
5917
6311
 
5918
6312
  @media (prefers-reduced-motion: reduce) {
@@ -6208,20 +6602,23 @@
6208
6602
  .fontInput { flex: 1 1 160px; height: 34px; padding: 0 10px; border: 1px solid var(--nc-line-1); border-radius: 8px; background: var(--nc-ink-1); color: var(--nc-fg-1); font: 500 13px/1 var(--nc-font-sans); }
6209
6603
  .fontUpload { height: 34px; display: inline-flex; align-items: center; padding: 0 12px; border: 1px solid var(--nc-line-1); border-radius: 8px; font: 500 13px/1 var(--nc-font-sans); color: var(--nc-fg-1); cursor: pointer; }
6210
6604
 
6211
- .selectEditable { padding: 0 6px 0 0; gap: 0; }
6212
- .selectInput {
6605
+ .selectEditable { padding: 0 6px 0 8px; gap: 6px; }
6606
+ .selectBtn .selectInput {
6213
6607
  width: 3.5em;
6214
6608
  min-width: 0;
6215
6609
  height: 100%;
6216
- padding: 0 0 0 10px;
6610
+ padding: 0;
6217
6611
  border: 0;
6612
+ border-radius: 0;
6218
6613
  background: transparent;
6614
+ box-shadow: none;
6615
+ appearance: none;
6219
6616
  color: inherit;
6220
6617
  font: inherit;
6221
6618
  outline: none;
6222
6619
  }
6223
- /* Wide enough for the word it actually shows: at 3.5em "Default" arrived as "Defi". */
6224
- .rteSizeSelect .selectInput { padding-left: 8px; width: 6em; }
6620
+ .selectBtn .selectInput:focus { border: 0; box-shadow: none; }
6621
+ .rteSizeSelect .selectInput { width: 3.2em; }
6225
6622
  .selectChevronBtn { display: inline-flex; align-items: center; padding: 0 4px; border: 0; background: transparent; color: inherit; cursor: pointer; }
6226
6623
  .mailboxDomainSelect.selectBtn { padding: 4px 6px; background: transparent; border: 0; }
6227
6624
 
@@ -6259,6 +6656,12 @@
6259
6656
  right: calc(18px + env(safe-area-inset-right, 0px));
6260
6657
  bottom: calc(18px + env(safe-area-inset-bottom, 0px));
6261
6658
  }
6659
+ /* Above the compose button, which appears at this width in the same corner: 54px tall
6660
+ sitting 18px off the bottom, so anything below 84px is underneath it. */
6661
+ .toTop {
6662
+ right: calc(18px + env(safe-area-inset-right, 0px));
6663
+ bottom: calc(84px + env(safe-area-inset-bottom, 0px));
6664
+ }
6262
6665
  .list {
6263
6666
  padding-left: calc(10px + env(safe-area-inset-left, 0px));
6264
6667
  padding-right: calc(10px + env(safe-area-inset-right, 0px));
@@ -6274,6 +6677,7 @@
6274
6677
  width: auto;
6275
6678
  z-index: 445;
6276
6679
  }
6680
+ .pwNoticeBehind { z-index: calc(445 - var(--depth, 1)); }
6277
6681
  /* Forty-four pixels is the smallest thing a thumb finds reliably. */
6278
6682
  .itemStar,
6279
6683
  .iconBtn,
@@ -6318,3 +6722,166 @@
6318
6722
  max-width: none;
6319
6723
  }
6320
6724
  }
6725
+
6726
+ /* ---- Phone corrections ------------------------------------------------------------
6727
+ Everything here was found by hit-testing the rendered page at 375x812 rather than by
6728
+ reading the stylesheet, because each fault only appears once the list is full width. */
6729
+ @media (max-width: 720px) {
6730
+ /* The notification and theme buttons float over the top-right corner. With a reading
6731
+ pane beside it that corner is empty; without one it is the list header, and these two
6732
+ sat on top of Refresh so it could not be tapped at all. Drop them behind anything that
6733
+ opens over the mailbox, and give the header room to put Refresh beside them. */
6734
+ .topRight {
6735
+ top: 18px;
6736
+ right: 12px;
6737
+ gap: 6px;
6738
+ z-index: 405;
6739
+ }
6740
+ .listHead .refreshBtn { display: none; }
6741
+ .actions { row-gap: 8px; gap: 6px; padding: 12px 15px; }
6742
+ .app { --thread-inset: 15px; }
6743
+ .readerHead { padding: 18px 15px 0; }
6744
+ .readerBody { margin: 0 15px 16px; }
6745
+ .readerModeBar { margin: 0 15px 12px; }
6746
+ .thread { padding-left: 15px; padding-right: 15px; }
6747
+ .attachGrid { margin: 0 15px 12px; }
6748
+ .activity { margin: 0 15px 14px; padding: 10px 12px; }
6749
+ .readerAttach { margin: 0 15px 14px; }
6750
+ .attachFloat {
6751
+ left: 0;
6752
+ right: 0;
6753
+ bottom: 0;
6754
+ border-left: 0;
6755
+ border-right: 0;
6756
+ border-bottom: 0;
6757
+ border-radius: 14px 14px 0 0;
6758
+ }
6759
+ .attachFloatBody .attachGrid { margin: 0 15px 10px; }
6760
+ .readerText { padding: 16px 15px; }
6761
+ .composeSignature { margin: 0 15px 14px; }
6762
+ .quickReplyBar { padding-left: 15px; padding-right: 15px; }
6763
+ .actions .iconBtn { min-width: 40px; width: 40px; padding: 0; }
6764
+ .actionSpacer { flex: 1 0 100%; min-width: 0; height: 0; }
6765
+ .rteToolbar {
6766
+ flex-wrap: nowrap;
6767
+ overflow-x: auto;
6768
+ gap: 6px;
6769
+ padding: 8px 12px;
6770
+ scroll-snap-type: x proximity;
6771
+ scrollbar-width: none;
6772
+ -webkit-overflow-scrolling: touch;
6773
+ }
6774
+ .rteToolbar::-webkit-scrollbar { display: none; }
6775
+ .rteGroup { flex: 0 0 auto; scroll-snap-align: start; }
6776
+ .rtePalette .rteSwatch { width: 24px; height: 24px; }
6777
+ .compose,
6778
+ .composeExpanded {
6779
+ top: auto;
6780
+ bottom: 0;
6781
+ height: 80dvh;
6782
+ max-height: 80dvh;
6783
+ border-radius: 18px 18px 0 0;
6784
+ }
6785
+ .listHead {
6786
+ padding-right: 106px;
6787
+ }
6788
+ .listTitle {
6789
+ flex: 0 0 auto;
6790
+ }
6791
+ .listMeta {
6792
+ min-width: 0;
6793
+ overflow: hidden;
6794
+ white-space: nowrap;
6795
+ text-overflow: ellipsis;
6796
+ }
6797
+ /* "60 convers…" reads as a fault rather than as a count. Below this width the number
6798
+ carries it alone; the unit is still in the header's tooltip and spelled out in full
6799
+ at the end of the list. */
6800
+ .listMetaUnit {
6801
+ display: none;
6802
+ }
6803
+ /* An earlier rule lifted this clear of the compose button; a later one pinned it back
6804
+ over the top, so its own buttons were half covered. */
6805
+ .pwNotice {
6806
+ bottom: calc(84px + env(safe-area-inset-bottom, 0px));
6807
+ }
6808
+ /* Nine formatting buttons at a size a finger can hit wrap onto three rows and take 134px
6809
+ of a reply that has not been written yet. One row that scrolls keeps every control and
6810
+ costs a third of the height. */
6811
+ .replyToolbar,
6812
+ .readerModeTabs,
6813
+ .readerLayoutTabs {
6814
+ flex-wrap: nowrap;
6815
+ overflow-x: auto;
6816
+ scrollbar-width: none;
6817
+ }
6818
+ .replyToolbar::-webkit-scrollbar,
6819
+ .readerModeTabs::-webkit-scrollbar,
6820
+ .readerLayoutTabs::-webkit-scrollbar {
6821
+ display: none;
6822
+ }
6823
+ .replyToolbar > *,
6824
+ .readerModeTabs > *,
6825
+ .readerLayoutTabs > * {
6826
+ flex: 0 0 auto;
6827
+ }
6828
+
6829
+ /* The card scrolls, and the footer scrolled away with it, so Save was reachable only
6830
+ after finding the bottom of a long form. */
6831
+ .settingsFoot {
6832
+ position: sticky;
6833
+ bottom: 0;
6834
+ z-index: 1;
6835
+ margin-top: 8px;
6836
+ padding: 10px 0 2px;
6837
+ background: var(--nc-ink-1);
6838
+ box-shadow: 0 -10px 16px -12px rgba(0, 0, 0, .5);
6839
+ }
6840
+ }
6841
+
6842
+ /* A finger is not a mouse pointer: anything it has to hit needs to be a target, not a
6843
+ label. These were 15px to 32px tall, which is a miss more often than a hit. */
6844
+ @media (pointer: coarse) {
6845
+ .modeTab,
6846
+ .orderToggle {
6847
+ min-height: 24px;
6848
+ }
6849
+ .settingsNavItem {
6850
+ min-height: 40px;
6851
+ }
6852
+ .toolBtn {
6853
+ min-width: 40px;
6854
+ min-height: 40px;
6855
+ }
6856
+ .headerToggle,
6857
+ .replyRecipsToggle,
6858
+ .composeSignatureLabel,
6859
+ .composeSignatureEdit,
6860
+ .composeSignatureMore {
6861
+ min-height: 36px;
6862
+ padding-top: 2px;
6863
+ padding-bottom: 2px;
6864
+ }
6865
+ .headerToggle {
6866
+ padding-left: 8px;
6867
+ padding-right: 8px;
6868
+ }
6869
+ }
6870
+
6871
+ @media (max-width: 720px) {
6872
+ /* Compose is already the whole screen here, so a control that makes it full width has
6873
+ nothing to do, and a phone has no Esc key to press. Between them they wrapped the
6874
+ compose header onto four lines. */
6875
+ .composeExpandBtn,
6876
+ .composeHintKeys {
6877
+ display: none;
6878
+ }
6879
+ /* The expanded composer takes its left edge from --list-w, and the chosen list width
6880
+ is written onto .app as an inline variable — which outranks this breakpoint's zero.
6881
+ So it opened 400px to the right of a 375px screen, wholly out of view. */
6882
+ .composeExpanded {
6883
+ left: 0;
6884
+ right: 0;
6885
+ width: auto;
6886
+ }
6887
+ }