domma-cms 0.75.0 → 0.76.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/admin/js/app.js +4 -4
  2. package/admin/js/lib/api-tokens-arrange.js +3 -0
  3. package/admin/js/lib/effects-arrange.js +3 -0
  4. package/admin/js/lib/layouts-arrange.js +1 -0
  5. package/admin/js/lib/my-profile-arrange.js +1 -0
  6. package/admin/js/lib/notifications-arrange.js +1 -0
  7. package/admin/js/lib/plugins-arrange.js +2 -0
  8. package/admin/js/lib/roles-arrange.js +1 -0
  9. package/admin/js/lib/search-settings-arrange.js +1 -0
  10. package/admin/js/lib/sidebar-renderer.js +2 -2
  11. package/admin/js/lib/site-settings-arrange.js +1 -0
  12. package/admin/js/lib/users-arrange.js +1 -0
  13. package/admin/js/templates/api-tokens.html +83 -10
  14. package/admin/js/templates/effects.html +155 -72
  15. package/admin/js/templates/layouts.html +126 -43
  16. package/admin/js/templates/my-profile.html +100 -12
  17. package/admin/js/templates/notifications.html +70 -10
  18. package/admin/js/templates/plugins.html +100 -28
  19. package/admin/js/templates/roles.html +78 -7
  20. package/admin/js/templates/search.html +193 -216
  21. package/admin/js/templates/settings.html +404 -472
  22. package/admin/js/templates/users.html +69 -9
  23. package/admin/js/views/api-tokens.js +62 -8
  24. package/admin/js/views/effects.js +31 -1
  25. package/admin/js/views/layouts.js +81 -1
  26. package/admin/js/views/my-profile.js +1 -1
  27. package/admin/js/views/notifications.js +18 -1
  28. package/admin/js/views/plugins.js +97 -39
  29. package/admin/js/views/roles.js +49 -4
  30. package/admin/js/views/search.js +7 -1
  31. package/admin/js/views/settings.js +13 -3
  32. package/admin/js/views/users.js +13 -4
  33. package/package.json +1 -1
  34. package/plugins/_lib/admin/mail/compose-window.js +97 -40
  35. package/plugins/_lib/admin/mail/contacts.js +4 -1
  36. package/plugins/_lib/admin/mail/diagnostics-section.js +4 -1
  37. package/plugins/_lib/admin/mail/folder-tree.js +1 -1
  38. package/plugins/_lib/admin/mail/image-senders-section.js +69 -90
  39. package/plugins/_lib/admin/mail/mail.css +1 -1
  40. package/plugins/_lib/admin/mail/mail.html +1 -1
  41. package/plugins/_lib/admin/mail/reader-view.js +303 -44
  42. package/plugins/_lib/admin/mail/when-closed.js +26 -0
  43. package/plugins/_lib/mail/readRoutes.js +43 -1
  44. package/plugins/_lib/mail/unreadBadge.js +42 -0
  45. package/plugins/free-tier.lock.json +5 -5
  46. package/plugins/mail-reader/CLAUDE.md +1 -0
  47. package/plugins/mail-reader/plugin.js +3 -1
  48. package/plugins/mail-reader/plugin.json +5 -4
  49. package/server/routes/api/api-tokens.js +56 -0
  50. package/server/routes/api/auth.js +7 -1
  51. package/server/routes/api/collections.js +13 -1
  52. package/server/routes/api/effects.js +68 -13
  53. package/server/routes/api/layouts.js +93 -2
  54. package/server/routes/api/notifications.js +140 -14
  55. package/server/routes/api/plugins.js +135 -4
  56. package/server/routes/api/roles.js +111 -0
  57. package/server/routes/api/search.js +38 -4
  58. package/server/routes/api/settings.js +52 -0
  59. package/server/routes/api/users.js +53 -1
  60. package/server/server.js +12 -0
  61. package/server/services/plugins.js +7 -0
  62. package/server/services/roles.js +3 -0
  63. package/server/services/search.js +58 -0
  64. package/server/services/users.js +14 -0
  65. package/admin/js/templates/plugin-marketplace.html +0 -29
  66. package/admin/js/views/plugin-marketplace.js +0 -1
@@ -72,6 +72,7 @@ import {applyIdentity, findIdentity, identityFrom, swapSignature} from './identi
72
72
  import {describeWhen, quickTimes} from './scheduling.js';
73
73
  import {applyTemplate, expandTemplate} from './templates.js';
74
74
  import {makeResizable} from './resizable.js';
75
+ import {whenClosed} from './when-closed.js';
75
76
  import {
76
77
  collectGroups,
77
78
  currentFragment,
@@ -509,16 +510,6 @@ export function openCompose({
509
510
  }
510
511
  form.appendChild(bodyWrap);
511
512
 
512
- // What the window opened showing, read back out of the thing showing it.
513
- //
514
- // NOT `draft.body`: the editor is seeded from `draft.html` and derives its
515
- // own text from that, so the two already differed on every HTML reply -
516
- // which is why discarding an untouched reply asked whether to lose
517
- // changes nobody had made. A signature makes that worse, since it means
518
- // even a new message opens "changed". Comparing against what was actually
519
- // put in front of the person is both the fix and the only baseline that
520
- // stays true once anything else seeds the body.
521
- const openedText = editor ? editor.getText() : bodyInput.value;
522
513
 
523
514
  // Attachments
524
515
  // Seeded from the draft, so reopening one keeps the files that were
@@ -653,7 +644,13 @@ export function openCompose({
653
644
  const saveDraftBtn = document.createElement('button');
654
645
  saveDraftBtn.className = 'btn btn-sm btn-ghost';
655
646
  saveDraftBtn.textContent = 'Save draft';
656
- saveDraftBtn.addEventListener('click', async () => {
647
+ /**
648
+ * Save this message to the mailbox's Drafts folder - the button, and what
649
+ * leaving the screen does with a message that has changed.
650
+ *
651
+ * @returns {Promise<object|null>} the server's answer, or null if it failed
652
+ */
653
+ async function saveDraft() {
657
654
  saveDraftBtn.disabled = true;
658
655
  say('Saving\u2026');
659
656
  try {
@@ -663,47 +660,56 @@ export function openCompose({
663
660
  replaces: savedUid
664
661
  });
665
662
  savedUid = result.uid ?? savedUid;
663
+ // Saved is the new baseline: closing now loses nothing.
664
+ savedSnapshot = snapshot();
666
665
  say(`Saved to ${result.savedTo}.`, 'ok');
667
666
  // The Drafts folder now holds a different set of UIDs, so a list
668
667
  // showing it is out of date whether or not the window closes.
669
668
  onSaved(result);
669
+ return result;
670
670
  } catch (err) {
671
671
  say(err.message, 'bad');
672
+ return null;
672
673
  } finally {
673
674
  saveDraftBtn.disabled = false;
674
675
  }
675
- });
676
+ }
677
+ saveDraftBtn.addEventListener('click', () => { saveDraft(); });
676
678
 
677
679
  const cancel = document.createElement('button');
678
680
  cancel.className = 'btn btn-sm btn-ghost';
679
681
  cancel.textContent = 'Discard';
680
- cancel.addEventListener('click', async () => {
681
- // Closing on something half-written should ask, because the window is
682
- // the only place that text exists until it is saved.
682
+ /** What the message says now - compared with the last saved (or opened) state. */
683
+ function snapshot() {
683
684
  const current = editor ? editor.getText() : bodyInput.value;
684
- const changed = subjectInput.value.trim() !== (draft.subject ?? '').trim()
685
- || current.trim() !== openedText.trim();
686
- // A reopened draft is already saved, so closing it unchanged is not a
687
- // loss and should not ask. Changed since opening, it is.
688
- //
689
- // `changed` is the whole test. A new message that has had a subject
690
- // typed into it is already changed, because a new draft opens with an
691
- // empty one - so the extra "or the subject is non-empty" clause this
692
- // used to carry caught nothing new, and DID catch every reply and
693
- // forward, which open with `Re:` / `Fwd:` prefilled and so were
694
- // always treated as unsaved work.
695
- const unsaved = changed;
696
- if (unsaved) {
697
- // Two different losses, and saying the wrong one is worse than
698
- // saying nothing: one has never been saved, the other has changes
699
- // since it was.
700
- const question = savedUid
701
- ? 'Close without saving? The changes since you opened this draft will be lost.'
702
- : 'Discard this message? It has not been saved as a draft.';
703
- if (!await E.confirm(question)) return;
704
- }
705
- slideover.close();
706
- });
685
+ return JSON.stringify([subjectInput.value.trim(), current.trim(), toInput.value.trim(),
686
+ ccInput.value.trim(), bccInput.value.trim(), attachments.length]);
687
+ }
688
+ let savedSnapshot = null; // set once the window is built (below)
689
+
690
+ /**
691
+ * Whether closing now would lose something.
692
+ *
693
+ * Compared with the last saved state, or what the window opened with - so
694
+ * a reopened draft closed unchanged does not ask, and a reply or forward,
695
+ * which opens with `Re:` / `Fwd:` and a quote already in it, does not
696
+ * count as unsaved work until the reader actually changes something.
697
+ */
698
+ function unsaved() {
699
+ return savedSnapshot !== null && snapshot() !== savedSnapshot;
700
+ }
701
+
702
+ // Discard, the X, Escape and the backdrop all go through slideover.close,
703
+ // which asks (see the guard at the end) - so this button just closes.
704
+ cancel.addEventListener('click', () => slideover.close());
705
+
706
+ /** The question the close guard asks, for what would be lost. */
707
+ function discardQuestion() {
708
+ return savedUid
709
+ ? 'Close without saving? The changes since you last saved this draft will be lost.'
710
+ : 'Discard this message? It has not been saved as a draft.';
711
+ }
712
+
707
713
 
708
714
  /**
709
715
  * The message as it currently stands.
@@ -855,7 +861,7 @@ export function openCompose({
855
861
  replaces: savedUid
856
862
  });
857
863
 
858
- slideover.close();
864
+ slideover.close(true);
859
865
 
860
866
  // Three outcomes, three different things to say. A message held
861
867
  // for ten seconds has NOT been sent, and saying it has is how
@@ -905,10 +911,61 @@ export function openCompose({
905
911
  }
906
912
  }, 60);
907
913
 
914
+ // The baseline for "changed": what the window opened showing, read back out
915
+ // of the thing showing it (taken once the editor has been seeded).
916
+ //
917
+ // NOT `draft.body`: the editor is seeded from `draft.html` and derives its
918
+ // own text from that, so the two already differed on every HTML reply -
919
+ // which is why discarding an untouched reply asked whether to lose
920
+ // changes nobody had made. A signature makes that worse, since it means
921
+ // even a new message opens "changed". Comparing against what was actually
922
+ // put in front of the person is both the fix and the only baseline that
923
+ // stays true once anything else seeds the body.
924
+ setTimeout(() => { if (savedSnapshot === null) savedSnapshot = snapshot(); }, 0);
925
+
926
+ // Every way out asks when something would be lost - Discard, the X, Escape
927
+ // and the backdrop all call close(). Once closed it goes straight through:
928
+ // a later close() (the cleanup below, or the screen being left) must not ask again.
929
+ const rawClose = slideover.close.bind(slideover);
930
+ let closed = false;
931
+ let asking = false;
932
+ const closedListeners = [];
933
+ slideover.close = async (force) => {
934
+ if (!closed && force !== true && unsaved()) {
935
+ if (asking) return undefined;
936
+ asking = true;
937
+ // Two different losses, and saying the wrong one is worse than
938
+ // saying nothing: one has never been saved, the other has changes
939
+ // since it was.
940
+ const ok = await E.confirm(discardQuestion());
941
+ asking = false;
942
+ if (!ok) return undefined;
943
+ }
944
+ if (closed) return undefined;
945
+ closed = true;
946
+ return rawClose();
947
+ };
948
+
908
949
  // The editor is a live component with its own listeners and autosave.
909
- slideover.on?.('close', () => {
950
+ whenClosed(slideover, () => {
951
+ closed = true;
910
952
  editor?.destroy?.();
911
953
  stopResizing();
912
954
  for (const suggester of suggesters) suggester.destroy?.();
955
+ closedListeners.splice(0).forEach(fn => { try { fn(); } catch { /* ignore */ } });
956
+ // Domma hides a closed slideover rather than removing it: every message
957
+ // written would otherwise leave its window behind in the page.
958
+ setTimeout(() => { try { slideover.element?.remove(); } catch { /* gone */ } }, 400);
913
959
  });
960
+
961
+ return {
962
+ /** Whether closing would lose something the reader wrote. */
963
+ dirty: () => !closed && unsaved(),
964
+ /** Save to Drafts now (what leaving the screen does with a changed message). */
965
+ saveDraft,
966
+ /** Close; `true` skips the question. */
967
+ close: (force) => slideover.close(force),
968
+ /** Called once, when the window has closed however it closed. */
969
+ onClosed: (fn) => { if (closed) fn(); else closedListeners.push(fn); }
970
+ };
914
971
  }
@@ -28,7 +28,10 @@
28
28
  * not hold. A 404 here is indistinguishable from "not installed", which is
29
29
  * exactly the behaviour wanted.
30
30
  */
31
- const CONTACTS_ENDPOINT = '/api/plugins/contacts/contacts';
31
+ // Core since 0.67.0 (it was the Contacts plugin, at /api/plugins/contacts/contacts:
32
+ // that address has 404'd ever since, so "Add sender to contacts" and group
33
+ // expansion in compose silently vanished).
34
+ const CONTACTS_ENDPOINT = '/api/contacts/contacts';
32
35
 
33
36
  /**
34
37
  * Fetch the address book once.
@@ -129,5 +129,8 @@ export function createDiagnosticsSection(host) {
129
129
  container.appendChild(actions);
130
130
  }
131
131
 
132
- return {id: 'diagnostics', label: 'Diagnostics', render};
132
+ return {
133
+ id: 'diagnostics', label: 'Diagnostics', render,
134
+ help: 'What this screen can see about itself - whether it runs the build the server is serving, the mailbox and folder, where the list came from, and recent errors. Copy diagnostics puts it on the clipboard for a report.'
135
+ };
133
136
  }
@@ -245,7 +245,7 @@ export function folderMenuItems(roots, {current = null, onPick, label = null}) {
245
245
  return {
246
246
  label: node.name,
247
247
  icon: iconForFolder(node),
248
- submenu: [target(node), {type: 'separator'}, ...walk(node.children)]
248
+ submenu: [target(node), {type: 'divider'}, ...walk(node.children)]
249
249
  };
250
250
  });
251
251
  }
@@ -11,23 +11,29 @@
11
11
  * @module _lib/admin/mail/image-senders-section
12
12
  */
13
13
 
14
+ /** The status line's colour, by tone - classes, so a theme override reaches it. */
15
+ const TONE = {ok: 'text-success', bad: 'text-danger', quiet: 'text-muted'};
16
+
14
17
  /**
15
- * Say something in a status line, in the right colour.
16
- *
17
- * A class rather than `style.color`: an inline colour cannot read a `--dm-*`
18
- * token that a theme override has redefined, so a status set that way is the
19
- * same shade in every theme.
20
- *
21
- * @param {HTMLElement} el
22
- * @param {string} text
23
- * @param {'ok'|'bad'|'quiet'} [tone]
24
- * @param {string} [base] - the layout classes the line always carries
25
- * @returns {void}
18
+ * The section's markup. Its own DOM, never passed through `_.render`, so the
19
+ * row's `{{address}}` is the list's template and not eaten on the way in.
26
20
  */
27
- function say(el, text, tone = 'quiet', base = 'text-sm mail-status') {
28
- el.textContent = text ?? '';
29
- el.className = `${base} ` + ({ok: 'text-success', bad: 'text-danger'}[tone] ?? 'text-muted');
30
- }
21
+ const TEMPLATE = `
22
+ <div class="mail-image-senders">
23
+ <p class="text-sm text-muted mb-3">Remote images are blocked by default, because loading one tells the sender that the message was opened and who opened it. Senders listed here are trusted, and their images load without asking.</p>
24
+ <label class="mail-toggle-row text-sm">
25
+ <input type="checkbox" class="mail-remember" data-bind-checked="remember.value">
26
+ Offer to remember senders
27
+ </label>
28
+ <p class="text-sm text-muted mail-none" data-bind-hidden="senders.value.length">No senders trusted yet.</p>
29
+ <div class="mail-sender-list" data-each="senders.value key=address">
30
+ <div class="mail-box-row" data-address="{{address}}">
31
+ <span class="flex-1 text-sm mail-ellipsis">{{address}}</span>
32
+ <button type="button" class="btn btn-sm btn-ghost text-danger mail-untrust" data-bind-disabled="busy">Stop trusting</button>
33
+ </div>
34
+ </div>
35
+ <div data-bind-class="'text-sm mail-status mt-2 ' + statusClass.value" data-bind-text="status.value"></div>
36
+ </div>`;
31
37
 
32
38
  /**
33
39
  * @param {HTMLElement} container
@@ -43,89 +49,62 @@ async function render(container, ctx) {
43
49
  const params = new URLSearchParams({account: ctx.account.id});
44
50
  const data = await ctx.api(`${ctx.base}/image-senders?${params}`);
45
51
 
46
- const intro = document.createElement('p');
47
- intro.className = 'text-sm text-muted mb-3';
48
- intro.textContent = 'Remote images are blocked by default, because loading one tells the sender that the message was opened and who opened it. Senders listed here are trusted, and their images load without asking.';
49
- container.appendChild(intro);
50
-
51
- const toggleRow = document.createElement('label');
52
- toggleRow.className = 'mail-toggle-row text-sm';
53
- const toggle = document.createElement('input');
54
- toggle.type = 'checkbox';
55
- toggle.checked = data.remember !== false;
56
- toggleRow.appendChild(toggle);
57
- toggleRow.appendChild(document.createTextNode('Offer to remember senders'));
58
- container.appendChild(toggleRow);
59
-
60
- const list = document.createElement('div');
61
- list.className = 'mail-sender-list';
62
- container.appendChild(list);
63
-
64
- const status = document.createElement('div');
65
- status.className = 'text-sm mail-status text-muted mt-2';
66
- container.appendChild(status);
67
-
68
- /**
69
- * @param {string[]} senders
70
- * @returns {void}
71
- */
72
- function paint(senders) {
73
- list.textContent = '';
74
- if (!senders.length) {
75
- const empty = document.createElement('p');
76
- empty.className = 'text-sm text-muted mail-none';
77
- empty.textContent = 'No senders trusted yet.';
78
- list.appendChild(empty);
79
- return;
80
- }
52
+ // ---- State ----
53
+ const busy = new Set();
54
+ const list = M.observable((data.senders ?? []).map(address => ({address, busy: false})));
55
+ const remember = M.observable(data.remember !== false);
56
+ const status = M.observable('');
57
+ const tone = M.observable('quiet');
58
+ const say = (text, t = 'quiet') => { status.value = text ?? ''; tone.value = t; };
59
+ const paint = (senders) => { list.value = senders.map(address => ({address, busy: busy.has(address)})); };
81
60
 
82
- for (const address of senders) {
83
- const row = document.createElement('div');
84
- row.className = 'mail-box-row';
85
-
86
- const name = document.createElement('span');
87
- name.className = 'flex-1 text-sm mail-ellipsis';
88
- name.textContent = address;
89
- row.appendChild(name);
90
-
91
- const remove = document.createElement('button');
92
- remove.className = 'btn btn-sm btn-ghost';
93
- remove.classList.add('text-danger');
94
- remove.textContent = 'Stop trusting';
95
- remove.addEventListener('click', async () => {
96
- remove.disabled = true;
97
- try {
98
- const query = new URLSearchParams({account: ctx.account.id, address});
99
- const result = await ctx.api(`${ctx.base}/image-senders?${query}`, 'DELETE');
100
- paint(result.senders);
101
- say(status, `Images from ${address} will be blocked again.`,
102
- 'quiet', 'text-sm mail-status mt-2');
103
- } catch (err) {
104
- say(status, err.message, 'bad', 'text-sm mail-status mt-2');
105
- remove.disabled = false;
106
- }
107
- });
108
- row.appendChild(remove);
109
- list.appendChild(row);
61
+ async function untrust(address) {
62
+ busy.add(address);
63
+ paint(list.peek().map(r => r.address));
64
+ try {
65
+ const query = new URLSearchParams({account: ctx.account.id, address});
66
+ const result = await ctx.api(`${ctx.base}/image-senders?${query}`, 'DELETE');
67
+ busy.delete(address);
68
+ paint(result.senders ?? []);
69
+ say(`Images from ${address} will be blocked again.`);
70
+ } catch (err) {
71
+ busy.delete(address);
72
+ paint(list.peek().map(r => r.address));
73
+ say(err.message, 'bad');
110
74
  }
111
75
  }
112
76
 
113
- paint(data.senders ?? []);
114
-
115
- toggle.addEventListener('change', async () => {
77
+ async function saveRemember(wanted) {
78
+ remember.value = wanted;
116
79
  try {
117
- const result = await ctx.api(`${ctx.base}/image-senders/remember?${params}`, 'PUT',
118
- {remember: toggle.checked});
119
- status.className = 'text-sm mail-status mt-2 text-muted';
120
- status.textContent = result.remember
121
- ? 'Senders can be remembered.'
122
- : 'Remembering is off. The list is kept but ignored.';
80
+ const result = await ctx.api(`${ctx.base}/image-senders/remember?${params}`, 'PUT', {remember: wanted});
81
+ say(result.remember ? 'Senders can be remembered.' : 'Remembering is off. The list is kept but ignored.');
123
82
  } catch (err) {
124
- say(status, err.message, 'bad', 'text-sm mail-status mt-2');
125
- toggle.checked = !toggle.checked;
83
+ remember.value = !wanted;
84
+ say(err.message, 'bad');
126
85
  }
86
+ }
87
+
88
+ const holder = document.createElement('div');
89
+ holder.innerHTML = TEMPLATE.trim();
90
+ const root = holder.firstElementChild;
91
+ container.appendChild(root);
92
+ M.applyBindings({
93
+ senders: list, remember, status,
94
+ statusClass: M.computed(() => TONE[tone.value] ?? TONE.quiet)
95
+ }, root);
96
+ root.querySelector('.mail-remember').addEventListener('change', (event) => saveRemember(event.target.checked));
97
+
98
+ // One listener for every row's button - the rows are re-rendered by the list.
99
+ root.addEventListener('click', (event) => {
100
+ const button = event.target.closest('.mail-untrust');
101
+ const row = button?.closest('[data-address]');
102
+ if (row && !button.disabled) untrust(row.dataset.address);
127
103
  });
128
104
  }
129
105
 
130
106
  /** The section both editions show. */
131
- export const imageSendersSection = {id: 'images', label: 'Images', render};
107
+ export const imageSendersSection = {
108
+ id: 'images', label: 'Images', render,
109
+ help: 'Pictures in mail are blocked until you ask for them, because loading one tells the sender you opened it. Senders listed here are trusted: their pictures load straight away.'
110
+ };
@@ -1 +1 @@
1
- #view-container.mail-fullbleed{max-width:none}.mail-layout [hidden],.dm-slideover [hidden]{display:none!important}.mail-reader-summary{display:flex;align-items:baseline;gap:.5rem;flex-wrap:wrap}.mail-header-toggle,.mail-extras-toggle{padding:0!important;font-size:.78rem;opacity:.75;text-decoration:underline}.mail-header-toggle:hover,.mail-extras-toggle:hover{opacity:1}.mail-header-details{display:grid;grid-template-columns:max-content 1fr;gap:.15rem .75rem;margin:.5rem 0 0;font-size:.8rem}.mail-header-details dt{opacity:.65}.mail-header-details dd{margin:0;overflow-wrap:anywhere}.mail-layout{display:grid;grid-template-columns:var(--mail-col-1, 190px) auto var(--mail-col-2, 340px) auto 1fr;height:calc(100vh - 140px);min-height:320px;border:1px solid var(--dm-border, #e2e8f0);border-radius:var(--dm-radius-md, 8px);overflow:hidden;background:var(--dm-surface, #fff)}.mail-folders-pane,.mail-list-pane{display:flex;flex-direction:column;border-right:1px solid var(--dm-border, #e2e8f0);min-width:0;min-height:0}.mail-reader-pane{display:flex;flex-direction:column;min-width:0;min-height:0}.mail-pane-head{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.5rem .75rem;font-size:.8rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--dm-text-muted, #64748b);border-bottom:1px solid var(--dm-border, #e2e8f0);flex-shrink:0}.mail-folders-list,.mail-list{overflow-y:auto;flex:1;min-height:0}.mail-search{display:flex;align-items:center;gap:.4rem;padding:.4rem .6rem;border-bottom:1px solid var(--dm-border, #e2e8f0);color:var(--dm-text-muted, #64748b);flex-shrink:0}.mail-search input{flex:1;min-width:0;font-size:.85rem;padding:.25rem .4rem}.mail-search-note{padding:.3rem .6rem;font-size:.74rem;color:var(--dm-text-muted, #64748b);border-bottom:1px solid var(--dm-border, #e2e8f0);flex-shrink:0}.mail-slideover-resizer{padding:0;position:absolute;left:0;top:0;bottom:0;width:6px;cursor:col-resize;z-index:2;background:transparent}.mail-slideover-resizer:hover,.is-resizing>.mail-slideover-resizer{background:var(--dm-primary, #2563eb);opacity:.7}.is-resizing{user-select:none;transition:none!important}.mail-selection{display:flex;align-items:center;gap:.35rem;flex-wrap:wrap;padding:.35rem .6rem;font-size:.78rem;background:var(--dm-primary, #2563eb);color:#fff;flex-shrink:0}.mail-selection span{flex:1;min-width:0}.mail-selection .btn{color:#fff;border:1px solid currentColor;background:transparent}.mail-selection .btn:hover{background:#ffffff26}.mail-item.is-selected{background:var(--dm-active-bg, rgba(15, 23, 42, .07));box-shadow:inset 3px 0 0 var(--dm-primary, #2563eb)}.mail-undo{display:flex;align-items:center;gap:.5rem;padding:.4rem .6rem;font-size:.78rem;background:var(--dm-active-bg, rgba(15, 23, 42, .07));border-bottom:1px solid var(--dm-border, #e2e8f0);flex-shrink:0}.mail-undo span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-splitter{width:5px;cursor:col-resize;background:var(--dm-border, #e2e8f0);flex-shrink:0;background-clip:content-box;border-left:2px solid transparent;border-right:2px solid transparent;box-sizing:content-box}.mail-splitter:hover,.mail-layout.is-resizing .mail-splitter{background-color:var(--dm-primary, #2563eb)}.mail-layout.is-resizing{user-select:none}.mail-layout.is-resizing .mail-folders-pane,.mail-layout.is-resizing .mail-list-pane,.mail-layout.is-resizing .mail-reader-pane,.mail-layout.is-resizing iframe{pointer-events:none}.mail-unread-filter{display:flex;align-items:center;gap:.3rem;font-size:.7rem;text-transform:none;letter-spacing:0;cursor:pointer;margin-left:auto;margin-right:.25rem}.mail-unread-pill{flex-shrink:0;min-width:1.15rem;padding:0 .3rem;border-radius:999px;background:var(--dm-primary, #2563eb);color:#fff;font-size:.66rem;font-weight:700;line-height:1.35;text-align:center}.mail-folder.is-context .mail-folder-name{opacity:.6}.mail-account{display:flex;align-items:center;gap:.4rem;width:100%;padding:.45rem .6rem;border:none;background:var(--dm-surface, #fff);cursor:pointer;text-align:left;font-size:.78rem;font-weight:700;text-transform:uppercase;letter-spacing:.03em;color:var(--dm-text-muted, #64748b)}.mail-account:hover{background:var(--dm-hover-bg, rgba(15, 23, 42, .04))}.mail-account.is-active{color:var(--dm-text, #1e293b);position:sticky;top:0;z-index:1;background:var(--dm-surface, #fff)}.mail-account-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-folder{display:flex;align-items:center;gap:.5rem;padding:.45rem .75rem;font-size:.875rem;cursor:pointer;border:none;background:none;width:100%;text-align:left;color:var(--dm-text, #1e293b)}.mail-folder:hover{background:var(--dm-hover-bg, rgba(15, 23, 42, .04))}.mail-folder.is-active{background:var(--dm-active-bg, rgba(15, 23, 42, .07));color:var(--dm-primary, #2563eb);font-weight:600}.mail-folder.is-unselectable{opacity:.6;cursor:default}.mail-folder.is-unselectable:hover{background:none}.mail-folder-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-item{display:block;width:100%;text-align:left;padding:.55rem .75rem;border:none;border-bottom:1px solid var(--dm-border, #e2e8f0);background:none;cursor:pointer;color:var(--dm-text, #1e293b)}.mail-item:hover{background:var(--dm-hover-bg, rgba(15, 23, 42, .04))}.mail-item.is-active{background:var(--dm-active-bg, rgba(15, 23, 42, .07))}.mail-item-top{display:flex;justify-content:space-between;gap:.5rem;align-items:baseline}.mail-item-from{font-size:.85rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-item.is-unread .mail-item-from,.mail-item.is-unread .mail-item-subject{font-weight:700}.mail-item-date{font-size:.72rem;color:var(--dm-text-muted, #64748b);flex-shrink:0}.mail-item-subject{font-size:.82rem;margin-top:.1rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-item-meta{display:flex;align-items:center;gap:.35rem;margin-top:.15rem;color:var(--dm-text-muted, #64748b)}.mail-pager{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.4rem .6rem;border-top:1px solid var(--dm-border, #e2e8f0);font-size:.75rem;color:var(--dm-text-muted, #64748b);flex-shrink:0}.mail-reader-head{padding:.75rem 1rem;border-bottom:1px solid var(--dm-border, #e2e8f0);flex-shrink:0}.mail-reader-head:empty{display:none}.mail-reader-subject{font-size:1.05rem;font-weight:600;margin:0 0 .3rem;color:var(--dm-text, #1e293b)}.mail-reader-actions{display:flex;gap:.25rem;margin:.1rem 0 .35rem;flex-wrap:wrap}.mail-reader-meta{font-size:.8rem;color:var(--dm-text-muted, #64748b)}.mail-reader-body{flex:1;position:relative;overflow:hidden;display:flex;flex-direction:column;min-height:0;background:var(--dm-surface, #fff)}.mail-reader-body iframe{width:100%;flex:1;border:none;display:block;background:#fff}.mail-notice{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;padding:.4rem .75rem;font-size:.78rem;background:var(--dm-warning, #8a6d1f);color:var(--dm-warning-text, #fff);border-bottom:1px solid var(--dm-border, #e2e8f0)}.mail-notice .btn{color:var(--dm-warning-text, #fff);border:1px solid currentColor;background:transparent;opacity:.95}.mail-notice .btn:hover{background:#ffffff26;opacity:1}.mail-attachments{display:flex;flex-wrap:wrap;gap:.4rem;padding:.5rem .75rem;border-bottom:1px solid var(--dm-border, #e2e8f0);background:var(--dm-background-alt, rgba(15, 23, 42, .03))}.mail-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;height:100%;color:var(--dm-text-muted, #94a3b8);text-align:center;padding:1rem}.mail-empty p{margin:0;font-size:.875rem}@media(max-width:900px){.mail-layout{grid-template-columns:1fr;height:auto!important}.mail-splitter{display:none}.mail-folders-pane,.mail-list-pane{border-right:none;border-bottom:1px solid var(--dm-border, #e2e8f0)}.mail-folders-list,.mail-list{max-height:240px}.mail-reader-body{min-height:60vh}}.mail-box-row{display:flex;align-items:center;gap:.5rem;padding:.5rem .6rem;border:1px solid var(--dm-border, #e2e8f0);border-radius:var(--dm-radius-md, 6px)}.mail-item.is-dragging{opacity:.45}.mail-folder.is-drop-target{background:var(--dm-primary, #2563eb);color:#fff;outline:2px solid var(--dm-primary, #2563eb);outline-offset:-2px}.mail-folder.is-drop-target .mail-folder-name,.mail-folder.is-drop-target .mail-unread-pill{color:inherit}.mail-item.is-dragging *{pointer-events:none}.mail-item{position:relative}.mail-item-tick{position:absolute;left:.45rem;top:.55rem;display:inline-flex;opacity:0;color:var(--dm-text-muted, #64748b);transition:opacity .12s ease;padding:2px;margin:-2px;border-radius:var(--dm-radius-sm, 3px)}.mail-item:hover .mail-item-tick,.mail-item:focus-visible .mail-item-tick,.mail-list.is-selecting .mail-item-tick,.mail-item-tick[aria-checked=true]{opacity:1}.mail-item-tick[aria-checked=true]{color:var(--dm-primary, #2563eb)}.mail-item-tick:hover{color:var(--dm-primary, #2563eb);background:var(--dm-hover-bg)}.mail-item-top,.mail-item-subject,.mail-item-meta{padding-left:1.5rem}.mail-select-all{display:inline-flex;align-items:center;margin-right:.1rem;cursor:pointer}.mail-select-all input{cursor:pointer;margin:0}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.mail-item-meta>span{display:inline-flex}.mail-item-meta .is-high{color:var(--dm-danger, #dc2626)}.mail-item-meta .is-low{color:var(--dm-text-muted, #64748b);opacity:.7}.mail-folder.is-hidden-folder .mail-folder-name{opacity:.5;font-style:italic}.mail-thread-chip{display:inline-flex;align-items:center;margin-left:.4rem;padding:0 .35rem;border-radius:999px;font-size:.68rem;font-weight:700;line-height:1.5;color:var(--dm-text-muted, #64748b);background:var(--dm-active-bg, rgba(15, 23, 42, .07));cursor:pointer}.mail-thread-chip:hover{color:var(--dm-primary, #2563eb)}.mail-thread-chip.is-unread{background:var(--dm-primary, #2563eb);color:#fff}.mail-item.is-in-thread{padding-left:1.6rem;box-shadow:inset 3px 0 0 var(--dm-border, #e2e8f0)}.mail-item.is-in-thread .mail-item-from{font-size:.8rem}.mail-pane-head .btn.is-on{color:var(--dm-primary, #2563eb);background:var(--dm-active-bg, rgba(15, 23, 42, .07))}#mail-outbox-btn{font-size:.72rem;font-weight:700}#mail-outbox-btn.is-failed{color:var(--dm-danger, #dc2626)}.mail-attachment{display:inline-flex;align-items:center;border:1px solid var(--dm-border, #e2e8f0);border-radius:var(--dm-radius-md, 6px);overflow:hidden}.mail-attachment .btn{border:none;border-radius:0}.mail-attachment-save{padding-left:.4rem!important;padding-right:.4rem!important;border-left:1px solid var(--dm-border, #e2e8f0)!important;opacity:.7}.mail-attachment-save:hover{opacity:1;color:var(--dm-primary, #2563eb)}.mail-section-heading{margin:1rem 0 .1rem;font-size:.8rem;text-transform:uppercase;letter-spacing:.04em;color:var(--dm-text-muted, #64748b)}.mail-section-heading:first-child{margin-top:.25rem}.mail-section-form .domma-form{margin:0}.mail-section-form .domma-form-field:last-child{margin-bottom:0}.mail-item-card{margin:0}.mail-item-card .card-header{padding:.5rem .6rem}.mail-item-card .card-body{padding:.6rem}.mail-ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.mail-status{min-height:1.2em}.mail-empty-line{margin:.25rem 0}.mail-editor-fallback{min-height:8rem;font-family:inherit;resize:vertical}.mail-clause{display:flex;gap:.3rem;align-items:center;flex-wrap:wrap}.mail-clause .form-select,.mail-clause .form-input{width:auto;flex:0 1 auto}.mail-clause .mail-clause-value{flex:1 1 8rem;min-width:8rem}.mail-section-list{display:flex;flex-direction:column;gap:.5rem}.mail-scroll-sm{max-height:20vh;overflow-y:auto}.mail-scroll-md{max-height:46vh;overflow-y:auto}.mail-section-form .domma-form-field:has(select) .form-label .text-danger{display:none}.mail-compose-form{padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:.6rem;height:100%}.mail-compose-body{display:flex;flex-direction:column;flex:1;min-height:0}.mail-compose-editor{flex:1;min-height:0}.mail-compose-textarea{flex:1;min-height:34vh;font-family:inherit;resize:vertical}.mail-attach-list{display:flex;flex-direction:column;gap:.2rem}.mail-template-row{margin:-.15rem 0 .35rem}.mail-template-picker{max-width:16rem}.mail-suggest-row{display:flex;flex-direction:column;line-height:1.25}.mail-suggest-detail{opacity:.7}.mail-rule-off{opacity:.55}.mail-panel{padding:.25rem 0 .5rem}.mail-panel-stack{padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:1rem}.mail-form-stack{padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:.75rem}.mail-dialog-stack{display:flex;flex-direction:column;gap:.5rem;padding:.25rem 0}.mail-hint{margin-top:.2rem}.mail-box-sub{overflow:hidden;text-overflow:ellipsis}.mail-add-btn{align-self:flex-start;margin-top:.5rem}.mail-none{margin:0;padding:.5rem .6rem}.mail-kv{padding:.5rem 0;display:flex;flex-direction:column;gap:.35rem}.mail-kv-row{display:flex;gap:.75rem}.mail-kv-key{min-width:8.5rem;flex-shrink:0}.mail-toggle-row{display:flex;align-items:center;gap:.5rem;margin-bottom:.75rem}.mail-sender-list{display:flex;flex-direction:column;gap:.2rem;max-height:40vh;overflow-y:auto}.mail-folder-pick{display:flex;align-items:center;gap:.5rem;padding:.3rem .4rem;border-radius:var(--dm-radius-md, 6px)}.mail-diag-log{width:100%;height:30vh;margin-top:.5rem;font-family:monospace;font-size:.72rem}.mail-facts{display:flex;flex-direction:column;gap:.2rem;margin-bottom:.75rem}
1
+ #view-container.mail-fullbleed{max-width:none}.mail-layout [hidden],.dm-slideover [hidden]{display:none!important}.mail-reader-summary{display:flex;align-items:baseline;gap:.5rem;flex-wrap:wrap}.mail-header-toggle,.mail-extras-toggle{padding:0!important;font-size:.78rem;opacity:.75;text-decoration:underline}.mail-header-toggle:hover,.mail-extras-toggle:hover{opacity:1}.mail-header-details{display:grid;grid-template-columns:max-content 1fr;gap:.15rem .75rem;margin:.5rem 0 0;font-size:.8rem}.mail-header-details dt{opacity:.65}.mail-header-details dd{margin:0;overflow-wrap:anywhere}.mail-layout{display:grid;grid-template-columns:var(--mail-col-1, 190px) auto var(--mail-col-2, 340px) auto 1fr;height:calc(100vh - 140px);min-height:320px;border:1px solid var(--dm-border, #e2e8f0);border-radius:var(--dm-radius-md, 8px);overflow:hidden;background:var(--dm-surface, #fff)}.mail-folders-pane,.mail-list-pane{display:flex;flex-direction:column;border-right:1px solid var(--dm-border, #e2e8f0);min-width:0;min-height:0}.mail-reader-pane{display:flex;flex-direction:column;min-width:0;min-height:0}.mail-pane-head{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.5rem .75rem;font-size:.8rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--dm-text-muted, #64748b);border-bottom:1px solid var(--dm-border, #e2e8f0);flex-shrink:0}.mail-folders-list,.mail-list{overflow-y:auto;flex:1;min-height:0}.mail-search{display:flex;align-items:center;gap:.4rem;padding:.4rem .6rem;border-bottom:1px solid var(--dm-border, #e2e8f0);color:var(--dm-text-muted, #64748b);flex-shrink:0}.mail-search input{flex:1;min-width:0;font-size:.85rem;padding:.25rem .4rem}.mail-search-note{padding:.3rem .6rem;font-size:.74rem;color:var(--dm-text-muted, #64748b);border-bottom:1px solid var(--dm-border, #e2e8f0);flex-shrink:0}.mail-slideover-resizer{padding:0;position:absolute;left:0;top:0;bottom:0;width:6px;cursor:col-resize;z-index:2;background:transparent}.mail-slideover-resizer:hover,.is-resizing>.mail-slideover-resizer{background:var(--dm-primary, #2563eb);opacity:.7}.is-resizing{user-select:none;transition:none!important}.mail-selection{display:flex;align-items:center;gap:.35rem;flex-wrap:wrap;padding:.35rem .6rem;font-size:.78rem;background:var(--dm-primary, #2563eb);color:var(--dm-primary-text, #fff);flex-shrink:0}.mail-selection span{flex:1;min-width:0}.mail-selection .btn{color:var(--dm-primary-text, #fff);border:1px solid currentColor;background:transparent}.mail-selection .btn:hover{background:color-mix(in srgb,var(--dm-primary-text, #fff) 15%,transparent)}.mail-item.is-selected{background:var(--dm-active-bg, rgba(15, 23, 42, .07));box-shadow:inset 3px 0 0 var(--dm-primary, #2563eb)}.mail-undo{display:flex;align-items:center;gap:.5rem;padding:.4rem .6rem;font-size:.78rem;background:var(--dm-active-bg, rgba(15, 23, 42, .07));border-bottom:1px solid var(--dm-border, #e2e8f0);flex-shrink:0}.mail-undo span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-splitter{width:5px;cursor:col-resize;background:var(--dm-border, #e2e8f0);flex-shrink:0;background-clip:content-box;border-left:2px solid transparent;border-right:2px solid transparent;box-sizing:content-box}.mail-splitter:hover,.mail-layout.is-resizing .mail-splitter{background-color:var(--dm-primary, #2563eb)}.mail-layout.is-resizing{user-select:none}.mail-layout.is-resizing .mail-folders-pane,.mail-layout.is-resizing .mail-list-pane,.mail-layout.is-resizing .mail-reader-pane,.mail-layout.is-resizing iframe{pointer-events:none}.mail-unread-filter{display:flex;align-items:center;gap:.3rem;font-size:.7rem;text-transform:none;letter-spacing:0;cursor:pointer;margin-left:auto;margin-right:.25rem}.mail-unread-pill{flex-shrink:0;min-width:1.15rem;padding:0 .3rem;border-radius:999px;background:var(--dm-primary, #2563eb);color:var(--dm-primary-text, #fff);font-size:.66rem;font-weight:700;line-height:1.35;text-align:center}.mail-folder.is-context .mail-folder-name{opacity:.6}.mail-account{display:flex;align-items:center;gap:.4rem;width:100%;padding:.45rem .6rem;border:none;background:var(--dm-surface, #fff);cursor:pointer;text-align:left;font-size:.78rem;font-weight:700;text-transform:uppercase;letter-spacing:.03em;color:var(--dm-text-muted, #64748b)}.mail-account:hover{background:var(--dm-hover-bg, rgba(15, 23, 42, .04))}.mail-account.is-active{color:var(--dm-text, #1e293b);position:sticky;top:0;z-index:1;background:var(--dm-surface, #fff)}.mail-account-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-folder{display:flex;align-items:center;gap:.5rem;padding:.45rem .75rem;font-size:.875rem;cursor:pointer;border:none;background:none;width:100%;text-align:left;color:var(--dm-text, #1e293b)}.mail-folder:hover{background:var(--dm-hover-bg, rgba(15, 23, 42, .04))}.mail-folder.is-active{background:var(--dm-active-bg, rgba(15, 23, 42, .07));color:var(--dm-primary, #2563eb);font-weight:600}.mail-folder.is-unselectable{opacity:.6;cursor:default}.mail-folder.is-unselectable:hover{background:none}.mail-folder-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-item{display:block;width:100%;text-align:left;padding:.55rem .75rem;border:none;border-bottom:1px solid var(--dm-border, #e2e8f0);background:none;cursor:pointer;color:var(--dm-text, #1e293b)}.mail-item:hover{background:var(--dm-hover-bg, rgba(15, 23, 42, .04))}.mail-item.is-active{background:var(--dm-active-bg, rgba(15, 23, 42, .07))}.mail-item-top{display:flex;justify-content:space-between;gap:.5rem;align-items:baseline}.mail-item-from{font-size:.85rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-item.is-unread .mail-item-from,.mail-item.is-unread .mail-item-subject{font-weight:700}.mail-item-date{font-size:.72rem;color:var(--dm-text-muted, #64748b);flex-shrink:0}.mail-item-subject{font-size:.82rem;margin-top:.1rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-item-meta{display:flex;align-items:center;gap:.35rem;margin-top:.15rem;color:var(--dm-text-muted, #64748b)}.mail-pager{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.4rem .6rem;border-top:1px solid var(--dm-border, #e2e8f0);font-size:.75rem;color:var(--dm-text-muted, #64748b);flex-shrink:0}.mail-reader-head{padding:.75rem 1rem;border-bottom:1px solid var(--dm-border, #e2e8f0);flex-shrink:0}.mail-reader-head:empty{display:none}.mail-reader-subject{font-size:1.05rem;font-weight:600;margin:0 0 .3rem;color:var(--dm-text, #1e293b)}.mail-reader-actions{display:flex;gap:.25rem;margin:.1rem 0 .35rem;flex-wrap:wrap}.mail-reader-meta{font-size:.8rem;color:var(--dm-text-muted, #64748b)}.mail-reader-body{flex:1;position:relative;overflow:hidden;display:flex;flex-direction:column;min-height:0;background:var(--dm-surface, #fff)}.mail-reader-body iframe{width:100%;flex:1;border:none;display:block;background:#fff}.mail-notice{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;padding:.4rem .75rem;font-size:.78rem;background:var(--dm-warning, #8a6d1f);color:var(--dm-warning-text, #fff);border-bottom:1px solid var(--dm-border, #e2e8f0)}.mail-notice .btn{color:var(--dm-warning-text, #fff);border:1px solid currentColor;background:transparent;opacity:.95}.mail-notice .btn:hover{background:color-mix(in srgb,var(--dm-warning-text, #fff) 15%,transparent);opacity:1}.mail-attachments{display:flex;flex-wrap:wrap;gap:.4rem;padding:.5rem .75rem;border-bottom:1px solid var(--dm-border, #e2e8f0);background:var(--dm-background-alt, rgba(15, 23, 42, .03))}.mail-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;height:100%;color:var(--dm-text-muted, #94a3b8);text-align:center;padding:1rem}.mail-empty p{margin:0;font-size:.875rem}@media(max-width:900px){.mail-layout{grid-template-columns:1fr;height:auto!important}.mail-splitter{display:none}.mail-folders-pane,.mail-list-pane{border-right:none;border-bottom:1px solid var(--dm-border, #e2e8f0)}.mail-folders-list,.mail-list{max-height:240px}.mail-reader-body{min-height:60vh}}.mail-box-row{display:flex;align-items:center;gap:.5rem;padding:.5rem .6rem;border:1px solid var(--dm-border, #e2e8f0);border-radius:var(--dm-radius-md, 6px)}.mail-item.is-dragging{opacity:.45}.mail-folder.is-drop-target{background:var(--dm-primary, #2563eb);color:var(--dm-primary-text, #fff);outline:2px solid var(--dm-primary, #2563eb);outline-offset:-2px}.mail-folder.is-drop-target .mail-folder-name,.mail-folder.is-drop-target .mail-unread-pill{color:inherit}.mail-item.is-dragging *{pointer-events:none}.mail-item{position:relative}.mail-item-tick{position:absolute;left:.45rem;top:.55rem;display:inline-flex;opacity:0;color:var(--dm-text-muted, #64748b);transition:opacity .12s ease;padding:2px;margin:-2px;border-radius:var(--dm-radius-sm, 3px)}.mail-item:hover .mail-item-tick,.mail-item:focus-visible .mail-item-tick,.mail-list.is-selecting .mail-item-tick,.mail-item-tick[aria-checked=true]{opacity:1}.mail-item-tick[aria-checked=true]{color:var(--dm-primary, #2563eb)}.mail-item-tick:hover{color:var(--dm-primary, #2563eb);background:var(--dm-hover-bg)}.mail-item-top,.mail-item-subject,.mail-item-meta{padding-left:1.5rem}.mail-select-all{display:inline-flex;align-items:center;margin-right:.1rem;cursor:pointer}.mail-select-all input{cursor:pointer;margin:0}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.mail-item-meta>span{display:inline-flex}.mail-item-meta .is-high{color:var(--dm-danger, #dc2626)}.mail-item-meta .is-low{color:var(--dm-text-muted, #64748b);opacity:.7}.mail-folder.is-hidden-folder .mail-folder-name{opacity:.5;font-style:italic}.mail-thread-chip{display:inline-flex;align-items:center;margin-left:.4rem;padding:0 .35rem;border-radius:999px;font-size:.68rem;font-weight:700;line-height:1.5;color:var(--dm-text-muted, #64748b);background:var(--dm-active-bg, rgba(15, 23, 42, .07));cursor:pointer}.mail-thread-chip:hover{color:var(--dm-primary, #2563eb)}.mail-thread-chip.is-unread{background:var(--dm-primary, #2563eb);color:var(--dm-primary-text, #fff)}.mail-item.is-in-thread{padding-left:1.6rem;box-shadow:inset 3px 0 0 var(--dm-border, #e2e8f0)}.mail-item.is-in-thread .mail-item-from{font-size:.8rem}.mail-pane-head .btn.is-on{color:var(--dm-primary, #2563eb);background:var(--dm-active-bg, rgba(15, 23, 42, .07))}#mail-outbox-btn{font-size:.72rem;font-weight:700}#mail-outbox-btn.is-failed{color:var(--dm-danger, #dc2626)}.mail-attachment{display:inline-flex;align-items:center;border:1px solid var(--dm-border, #e2e8f0);border-radius:var(--dm-radius-md, 6px);overflow:hidden}.mail-attachment .btn{border:none;border-radius:0}.mail-attachment-save{padding-left:.4rem!important;padding-right:.4rem!important;border-left:1px solid var(--dm-border, #e2e8f0)!important;opacity:.7}.mail-attachment-save:hover{opacity:1;color:var(--dm-primary, #2563eb)}.mail-section-heading{margin:1rem 0 .1rem;font-size:.8rem;text-transform:uppercase;letter-spacing:.04em;color:var(--dm-text-muted, #64748b)}.mail-section-heading:first-child{margin-top:.25rem}.mail-section-form .domma-form{margin:0}.mail-section-form .domma-form-field:last-child{margin-bottom:0}.mail-item-card{margin:0}.mail-item-card .card-header{padding:.5rem .6rem}.mail-item-card .card-body{padding:.6rem}.mail-ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.mail-status{min-height:1.2em}.mail-empty-line{margin:.25rem 0}.mail-editor-fallback{min-height:8rem;font-family:inherit;resize:vertical}.mail-clause{display:flex;gap:.3rem;align-items:center;flex-wrap:wrap}.mail-clause .form-select,.mail-clause .form-input{width:auto;flex:0 1 auto}.mail-clause .mail-clause-value{flex:1 1 8rem;min-width:8rem}.mail-section-list{display:flex;flex-direction:column;gap:.5rem}.mail-scroll-sm{max-height:20vh;overflow-y:auto}.mail-scroll-md{max-height:46vh;overflow-y:auto}.mail-section-form .domma-form-field:has(select) .form-label .text-danger{display:none}.mail-compose-form{padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:.6rem;height:100%}.mail-compose-body{display:flex;flex-direction:column;flex:1;min-height:0}.mail-compose-editor{flex:1;min-height:0}.mail-compose-textarea{flex:1;min-height:34vh;font-family:inherit;resize:vertical}.mail-attach-list{display:flex;flex-direction:column;gap:.2rem}.mail-template-row{margin:-.15rem 0 .35rem}.mail-template-picker{max-width:16rem}.mail-suggest-row{display:flex;flex-direction:column;line-height:1.25}.mail-suggest-detail{opacity:.7}.mail-rule-off{opacity:.55}.mail-panel{padding:.25rem 0 .5rem}.mail-panel-stack{padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:1rem}.mail-form-stack{padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:.75rem}.mail-dialog-stack{display:flex;flex-direction:column;gap:.5rem;padding:.25rem 0}.mail-hint{margin-top:.2rem}.mail-box-sub{overflow:hidden;text-overflow:ellipsis}.mail-add-btn{align-self:flex-start;margin-top:.5rem}.mail-none{margin:0;padding:.5rem .6rem}.mail-kv{padding:.5rem 0;display:flex;flex-direction:column;gap:.35rem}.mail-kv-row{display:flex;gap:.75rem}.mail-kv-key{min-width:8.5rem;flex-shrink:0}.mail-toggle-row{display:flex;align-items:center;gap:.5rem;margin-bottom:.75rem}.mail-sender-list{display:flex;flex-direction:column;gap:.2rem;max-height:40vh;overflow-y:auto}.mail-folder-pick{display:flex;align-items:center;gap:.5rem;padding:.3rem .4rem;border-radius:var(--dm-radius-md, 6px)}.mail-diag-log{width:100%;height:30vh;margin-top:.5rem;font-family:monospace;font-size:.72rem}.mail-facts{display:flex;flex-direction:column;gap:.2rem;margin-bottom:.75rem}.mail-item-more{position:absolute;right:.4rem;bottom:.35rem;display:inline-flex;align-items:center;justify-content:center;min-width:1.4rem;height:1.4rem;border-radius:var(--dm-radius-sm, 3px);font-size:1rem;line-height:1;color:var(--dm-text-muted, #64748b);background:inherit;opacity:0;transition:opacity .12s ease}.mail-item:hover .mail-item-more,.mail-item:focus-visible .mail-item-more,.mail-item.is-active .mail-item-more{opacity:1}.mail-item-more:hover{color:var(--dm-primary, #2563eb)}.mail-section-intro{display:flex;align-items:center;gap:.35rem;margin:0 0 .75rem;padding-bottom:.5rem;border-bottom:1px solid var(--dm-border, #e2e8f0)}
@@ -33,7 +33,7 @@
33
33
  <span data-icon="clock" data-icon-size="14"></span><span id="mail-outbox-count"></span>
34
34
  </button>
35
35
  <button class="btn btn-sm btn-ghost" id="mail-threads-btn" title="Group messages into conversations" aria-pressed="false" hidden>
36
- <span data-icon="messages-square" data-icon-size="14"></span>
36
+ <span data-icon="message-square" data-icon-size="14"></span>
37
37
  </button>
38
38
  <button class="btn btn-sm btn-ghost" id="mail-compose-btn" title="New message" hidden>
39
39
  <span data-icon="edit-3" data-icon-size="14"></span>