Menu
Public repository / 66e3647948f2e277afdd6ecd16939de8b6ad525f
AkurAI-Mail / src/routes/webmail/+page.svelte
Latest change 66e3647948f2e277afdd6ecd16939de8b6ad525f - Initial commit: AkurAI Mail UI (SvelteKit+Bun) by Ólafur Búi Ólafsson
<script lang="ts">
let { data } = $props();
let status = $derived(data.status);
let admin = $derived(data.admin);
let webmailState: any = $state(null);
let current = $state("");
let folder = $state("Inbox");
let query = $state("");
let selected: any = $state(null);
let notice = $state("");
let busy = $state(false);
let composeOpen = $state(false);
let settingsOpen = $state(false);
let compose: any = $state({ to: "", cc: "", bcc: "", subject: "", body: "", attachments: [] });
let contact = $state({ name: "", email: "" });
let signature = $state("");
let importText = $state("");
let page = $state(0);
let vacation = $state({ enabled: false, subject: "Out of office", body: "" });
let rule = $state({ field: "from", contains: "", action: "Archive", enabled: true });
const pageSize = 40;
const folderList = $derived(webmailState?.folders ?? []);
const messages = $derived(webmailState?.messages ?? []);
const contacts = $derived(webmailState?.contacts ?? []);
const signatures = $derived(webmailState?.signatures ?? {});
const rules = $derived(webmailState?.rules ?? []);
const preferences = $derived(webmailState?.preferences ?? {});
const activeSignature = $derived(signatures[current] ?? "");
const visibleMessages = $derived(messages.slice(page * pageSize, (page + 1) * pageSize));
const pageCount = $derived(Math.max(1, Math.ceil(messages.length / pageSize)));
const selectedDate = $derived(selected?.date ? new Date(selected.date) : null);
$effect(() => {
if (!webmailState) webmailState = data.webmail;
if (!current) current = data.webmail.mailbox || data.status.users?.[0]?.email || "olibuijr@olibuijr.com";
});
$effect(() => {
document.documentElement.dataset.theme = preferences.theme ?? "light";
const onKey = (event: KeyboardEvent) => {
const tag = (event.target as HTMLElement)?.tagName;
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
if (event.key === "c") composeOpen = true;
if (event.key === "/") {
event.preventDefault();
(document.querySelector("[data-search]") as HTMLInputElement | null)?.focus();
}
if (!selected) return;
if (event.key === "r") reply(false);
if (event.key === "f") forward();
if (event.key === "Delete") post({ action: "message", id: selected.id, messageAction: "delete" });
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
});
function shortDate(value: string) {
const date = new Date(value);
if (Number.isNaN(date.valueOf())) return value || "";
const today = new Date();
if (date.toDateString() === today.toDateString()) return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
return date.toLocaleDateString([], { month: "short", day: "numeric" });
}
function senderLabel(message: any) {
return message.from || message.to || "(unknown)";
}
async function loadMailbox(nextFolder = folder) {
busy = true;
const params = new URLSearchParams({ email: current, folder: nextFolder, query });
const response = await fetch(`/webmail/api?${params}`);
webmailState = await response.json();
folder = nextFolder;
selected = null;
page = 0;
busy = false;
}
async function readMessage(id: string) {
busy = true;
const response = await fetch("/webmail/api", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ action: "read", email: current, id })
});
const result = await response.json();
if (result.ok) selected = result.message;
busy = false;
}
async function post(body: Record<string, unknown>, refresh = true) {
busy = true;
notice = "";
const response = await fetch("/webmail/api", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ email: current, ...body })
});
const result = await response.json();
notice = result.ok ? "Saved" : result.error || "Action failed";
if (refresh) await loadMailbox(folder);
busy = false;
return result;
}
async function send() {
const result = await post({ action: "send", message: { ...compose, from: current, signature: activeSignature } });
if (result.ok) {
composeOpen = false;
compose = { to: "", cc: "", bcc: "", subject: "", body: "", attachments: [] };
notice = "Message sent";
}
}
async function saveDraft() {
await post({ action: "draft", message: { ...compose, from: current } });
notice = "Draft saved";
}
async function downloadAttachment(file: any) {
const result = await post({ action: "attachment", id: selected.id, index: file.index }, false);
if (!result.ok) return;
const link = document.createElement("a");
link.href = `data:${result.contentType};base64,${result.data}`;
link.download = result.filename;
link.click();
}
async function exportSettings() {
const result = await post({ action: "export" }, false);
if (result.ok) importText = JSON.stringify(result.webmail, null, 2);
}
async function importSettings() {
try {
await post({ action: "import", value: JSON.parse(importText) }, false);
await loadMailbox(folder);
} catch {
notice = "Import JSON is invalid";
}
}
function reply(all = false) {
if (!selected) return;
compose = {
to: selected.from,
cc: all ? selected.to : "",
bcc: "",
subject: selected.subject.startsWith("Re:") ? selected.subject : `Re: ${selected.subject}`,
body: `\n\nOn ${selected.date}, ${selected.from} wrote:\n${selected.bodyText ?? ""}`,
attachments: []
};
composeOpen = true;
}
function forward() {
if (!selected) return;
compose = {
to: "",
cc: "",
bcc: "",
subject: selected.subject.startsWith("Fwd:") ? selected.subject : `Fwd: ${selected.subject}`,
body: `\n\nForwarded message:\nFrom: ${selected.from}\nDate: ${selected.date}\nSubject: ${selected.subject}\n\n${selected.bodyText ?? ""}`,
attachments: []
};
composeOpen = true;
}
async function attach(event: Event) {
const input = event.currentTarget as HTMLInputElement;
const files = Array.from(input.files ?? []).slice(0, 5);
compose.attachments = await Promise.all(files.map((file) => new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve({ name: file.name, type: file.type, size: file.size, data: String(reader.result).split(",", 2)[1] });
reader.onerror = reject;
reader.readAsDataURL(file);
})));
}
</script>
<svelte:head><title>Webmail · AkurAI Mail</title></svelte:head>
<main class="webmail-shell">
<aside class="platform-rail">
<a class="brand" href="/" aria-label="AkurAI Mail admin home">
<span class="mark">A</span>
<span><strong>AkurAI Mail</strong><small>{status.hostname}</small></span>
</a>
<nav aria-label="Primary">
{#if admin}<a href="/">Admin</a>{/if}
<a class="active" href="/webmail">Webmail</a>
<a href="/webmail/logout">Sign out</a>
</nav>
</aside>
<section class="mail-app">
<header class="mail-topbar">
<div class="mail-title">
<strong>AkurAI Webmail</strong>
<span>{current}</span>
</div>
<label class="search">
<span class="sr">Search mail</span>
<input data-search bind:value={query} onkeydown={(event) => { if (event.key === "Enter") loadMailbox(folder); }} placeholder="Search mail" />
</label>
<div class="top-actions">
<select bind:value={current} onchange={() => loadMailbox("Inbox")} aria-label="Mailbox">
{#each status.users as user}<option value={user.email}>{user.email}</option>{/each}
</select>
<button class="primary" onclick={() => composeOpen = true}>Compose</button>
<button class="quiet" onclick={() => settingsOpen = !settingsOpen} aria-expanded={settingsOpen}>Settings</button>
</div>
</header>
{#if notice}<p class="toast" aria-live="polite">{notice}</p>{/if}
<div class:reading={selected} class="mail-canvas">
<aside class="folder-column" aria-label="Mail folders">
<button class="compose-wide" onclick={() => composeOpen = true}>Compose</button>
<div class="folder-group">
{#each folderList as item}
<button class:active={item.name === folder} onclick={() => loadMailbox(item.name)}>
<span>{item.name}</span>
<em>{item.unread || item.total}</em>
</button>
{/each}
</div>
</aside>
<section class="message-column" aria-label="Message list">
<div class="column-bar">
<div>
<strong>{folder}</strong>
<span>{messages.length} message{messages.length === 1 ? "" : "s"}</span>
</div>
<button class="quiet compact" onclick={() => loadMailbox(folder)} disabled={busy}>Refresh</button>
</div>
<div class="message-list">
{#each visibleMessages as message}
<button class:unread={message.unread} class:selected={selected?.id === message.id} onclick={() => readMessage(message.id)}>
<span class="row-top">
<b>{senderLabel(message)}</b>
<time>{shortDate(message.date)}</time>
</span>
<span class="subject-line">
{#if message.flagged}<i>Starred</i>{/if}
<strong>{message.subject}</strong>
</span>
<span class="preview">{message.preview}</span>
{#if message.attachments?.length || message.spamScore}
<span class="message-tags">
{#if message.attachments?.length}<em>{message.attachments.length} attachment{message.attachments.length === 1 ? "" : "s"}</em>{/if}
{#if message.spamScore}<em>Spam score {message.spamScore}</em>{/if}
</span>
{/if}
</button>
{:else}
<div class="empty-state"><strong>No messages</strong><p>{query ? "No mail matches this search." : "This folder is empty."}</p></div>
{/each}
</div>
<div class="pager">
<button class="quiet" disabled={page === 0} onclick={() => page = Math.max(0, page - 1)}>Previous</button>
<span>{page + 1} / {pageCount}</span>
<button class="quiet" disabled={page + 1 >= pageCount} onclick={() => page = Math.min(pageCount - 1, page + 1)}>Next</button>
</div>
</section>
<section class="reader-column" aria-label="Message reader">
{#if selected}
<div class="reader-toolbar">
<button class="quiet mobile-back" onclick={() => selected = null}>Back</button>
<button class="quiet" onclick={() => reply(false)}>Reply</button>
<button class="quiet" onclick={() => reply(true)}>Reply all</button>
<button class="quiet" onclick={forward}>Forward</button>
<button class="quiet" onclick={() => post({ action: "message", id: selected.id, messageAction: selected.flagged ? "unstar" : "star" })}>{selected.flagged ? "Unstar" : "Star"}</button>
<button class="quiet" onclick={() => post({ action: "message", id: selected.id, messageAction: "archive" })}>Archive</button>
<button class="quiet" onclick={() => post({ action: "message", id: selected.id, messageAction: "spam" })}>Spam</button>
<button class="danger" onclick={() => post({ action: "message", id: selected.id, messageAction: "delete" })}>Delete</button>
</div>
<article class="reader-card">
<header class="reader-head">
<div>
<h1>{selected.subject}</h1>
<p>{selectedDate ? selectedDate.toLocaleString() : selected.date}</p>
</div>
<dl>
<div><dt>From</dt><dd>{selected.from || "(unknown)"}</dd></div>
<div><dt>To</dt><dd>{selected.to || current}</dd></div>
</dl>
</header>
{#if selected.spamReasons?.length}
<div class="warning">
<strong>Spam signals</strong>
<span>{selected.spamReasons.join(", ")}</span>
</div>
{/if}
<div class="message-body">{@html selected.bodyHtml}</div>
{#if selected.attachments?.length}
<div class="attachments">
<strong>Attachments</strong>
{#each selected.attachments as file}<button class="quiet" onclick={() => downloadAttachment(file)}>{file.filename} ({Math.ceil(file.size / 1024)} KB)</button>{/each}
</div>
{/if}
</article>
{:else}
<div class="reader-empty">
<strong>Select a message</strong>
<p>Mail opens here on desktop and in a focused detail view on smaller screens.</p>
</div>
{/if}
</section>
</div>
</section>
{#if composeOpen}
<section class="compose-window" aria-label="Compose message">
<div class="compose-title">
<strong>New message</strong>
<button class="ghost" onclick={() => composeOpen = false}>Close</button>
</div>
<label><span>To</span><input bind:value={compose.to} autocomplete="email" /></label>
<label><span>Cc</span><input bind:value={compose.cc} autocomplete="email" /></label>
<label><span>Bcc</span><input bind:value={compose.bcc} autocomplete="email" /></label>
<label><span>Subject</span><input bind:value={compose.subject} /></label>
<label class="body-field"><span>Message</span><textarea bind:value={compose.body}></textarea></label>
{#if activeSignature}<small>Signature will be appended for {current}.</small>{/if}
<label class="file-field"><span>Attachments</span><input type="file" multiple onchange={attach} /></label>
{#if compose.attachments?.length}<small>{compose.attachments.length} attachment(s) ready</small>{/if}
<div class="compose-actions">
<button class="primary" onclick={send} disabled={busy}>Send</button>
<button class="quiet" onclick={saveDraft} disabled={busy}>Save draft</button>
</div>
</section>
{/if}
{#if settingsOpen}
<aside class="settings-drawer" aria-label="Webmail settings">
<div class="settings-head">
<strong>Settings</strong>
<button class="ghost" onclick={() => settingsOpen = false}>Close</button>
</div>
<section>
<h2>Contacts</h2>
<div class="inline-fields"><input bind:value={contact.name} placeholder="Name" /><input bind:value={contact.email} placeholder="email@example.com" /><button class="primary" onclick={() => post({ action: "config", kind: "contact", value: contact }, false).then(() => loadMailbox(folder))}>Add</button></div>
{#each contacts as item}<p>{item.name || item.email} <code>{item.email}</code></p>{:else}<p class="muted">No contacts saved.</p>{/each}
</section>
<section>
<h2>Signature</h2>
<textarea bind:value={signature} placeholder="Signature for selected mailbox"></textarea>
<button class="primary" onclick={() => post({ action: "config", kind: "signature", value: { email: current, signature } }, false).then(() => loadMailbox(folder))}>Save signature</button>
</section>
<section>
<h2>Vacation reply</h2>
<label class="check"><input type="checkbox" bind:checked={vacation.enabled} /> Enabled</label>
<input bind:value={vacation.subject} placeholder="Subject" />
<textarea bind:value={vacation.body} placeholder="Reply body"></textarea>
<button class="primary" onclick={() => post({ action: "config", kind: "vacation", value: vacation }, false).then(() => loadMailbox(folder))}>Save vacation reply</button>
</section>
<section>
<h2>Preferences</h2>
<select value={preferences.theme ?? "light"} onchange={(event) => post({ action: "config", kind: "preferences", value: { theme: (event.currentTarget as HTMLSelectElement).value } }, false).then(() => loadMailbox(folder))}>
<option value="light">Light mode</option>
<option value="dark">Dark mode</option>
</select>
</section>
<section>
<h2>Rules</h2>
<div class="inline-fields"><select bind:value={rule.field}><option>from</option><option>to</option><option>subject</option><option>preview</option></select><input bind:value={rule.contains} placeholder="contains" /><select bind:value={rule.action}><option>Archive</option><option>Spam</option><option>Trash</option></select><button class="primary" onclick={() => post({ action: "config", kind: "rule", value: rule }, false).then(() => loadMailbox(folder))}>Add rule</button></div>
<button class="quiet" onclick={() => post({ action: "apply-rules", folder })}>Apply rules now</button>
{#each rules as item}<p>{item.field} contains <code>{item.contains}</code> -> {item.action}</p>{:else}<p class="muted">No rules configured.</p>{/each}
</section>
<section>
<h2>Import / export</h2>
<textarea bind:value={importText} placeholder="Settings JSON"></textarea>
<div class="compose-actions"><button class="quiet" onclick={exportSettings}>Export</button><button class="primary" onclick={importSettings}>Import</button></div>
</section>
</aside>
{/if}
</main>
<style>
:global(*){box-sizing:border-box}
:global(html),:global(body){margin:0;overflow:hidden}
:global(body){font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#eef2f0;color:#151d1b}
:global(html[data-theme="dark"] body){background:#101715;color:#eaf3ef}
button,input,select,textarea{font:inherit}
button{cursor:pointer}
button:disabled{cursor:not-allowed;opacity:.55}
input,select,textarea{border:1px solid #cbd8d4;border-radius:6px;background:#fff;color:#151d1b;min-width:0;padding:10px}
input:focus,select:focus,textarea:focus,button:focus-visible{outline:3px solid #b7e7da;outline-offset:1px}
textarea{resize:vertical}
.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)}
.webmail-shell{height:100dvh;display:grid;grid-template-columns:248px minmax(0,1fr);overflow:hidden}
.platform-rail{background:#0d1b18;color:#edf7f3;padding:20px;display:flex;flex-direction:column;gap:20px;min-width:0}
.brand{display:flex;gap:12px;align-items:center;color:inherit;text-decoration:none;min-width:0}
.brand span:last-child{min-width:0}.brand strong,.brand small{display:block;overflow-wrap:anywhere}.brand small{margin-top:2px;color:#96aaa3;font-size:12px}
.mark{display:grid;place-items:center;flex:0 0 auto;width:40px;height:40px;border-radius:8px;background:#c8f05a;color:#0d1b18;font-weight:900}
nav{display:grid;gap:4px}nav a{color:#dce9e4;text-decoration:none;border-radius:6px;padding:10px 11px;min-height:44px;display:flex;align-items:center}
nav a:hover,nav a.active{background:#1c332e}
.mail-app{height:100dvh;display:grid;grid-template-rows:64px minmax(0,1fr);min-width:0}
.mail-topbar{background:#fff;border-bottom:1px solid #dce4e1;display:grid;grid-template-columns:minmax(190px,260px) minmax(220px,1fr) auto;gap:14px;align-items:center;padding:10px 16px;min-width:0}
.mail-title{display:grid;gap:2px;min-width:0}.mail-title strong{font-size:15px}.mail-title span{font-size:12px;color:#61716c;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.search input{width:100%;height:42px;border-radius:999px;background:#f4f8f6;padding-inline:16px}
.top-actions{display:flex;gap:8px;align-items:center;min-width:0}.top-actions select{max-width:260px;height:42px}
.primary,.quiet,.danger,.ghost{min-height:42px;border:0;border-radius:6px;padding:9px 13px;font-weight:800;transition:background .16s ease,color .16s ease,border-color .16s ease}
.primary{background:#126b5a;color:#fff}.primary:hover{background:#0e594b}
.quiet,.ghost{background:#edf4f1;color:#17352f}.quiet:hover,.ghost:hover{background:#dbe9e4}
.danger{background:#9a3125;color:#fff}.danger:hover{background:#81271e}
.compact{min-height:36px;padding:7px 10px}
.toast{position:fixed;right:18px;top:78px;z-index:40;margin:0;background:#e9f6f2;border:1px solid #b7dccf;border-radius:8px;padding:10px 12px;color:#17352f;box-shadow:0 14px 30px rgba(13,27,24,.12)}
.mail-canvas{min-height:0;display:grid;grid-template-columns:196px minmax(320px,420px) minmax(0,1fr);background:#fff;overflow:hidden}
.folder-column{border-right:1px solid #dce4e1;background:#f7faf8;padding:12px;display:grid;align-content:start;gap:12px;min-width:0}
.compose-wide{width:100%;border-radius:999px;background:#126b5a;color:#fff;box-shadow:0 8px 18px rgba(18,107,90,.18)}
.folder-group{display:grid;gap:2px}.folder-group button{border:0;background:transparent;color:#31413c;border-radius:999px;min-height:40px;padding:8px 11px;display:flex;align-items:center;justify-content:space-between;text-align:left}
.folder-group button:hover,.folder-group button.active{background:#e3f2ed;color:#0e594b}.folder-group em{font-style:normal;color:#61716c;font-size:12px}
.message-column{border-right:1px solid #dce4e1;display:grid;grid-template-rows:auto minmax(0,1fr) auto;min-width:0;background:#fff}
.column-bar{min-height:58px;border-bottom:1px solid #dce4e1;padding:9px 12px;display:flex;align-items:center;justify-content:space-between;gap:10px}
.column-bar div{display:grid;gap:2px}.column-bar strong{font-size:16px}.column-bar span,.pager span{font-size:12px;color:#61716c}
.message-list{overflow:auto;min-height:0}.message-list button{width:100%;min-height:104px;border:0;border-bottom:1px solid #edf1ef;border-radius:0;background:#fff;color:#151d1b;text-align:left;padding:12px 14px;display:grid;gap:5px}
.message-list button:hover,.message-list button.selected{background:#f2f8f5}.message-list button.unread b,.message-list button.unread .subject-line strong{font-weight:900}
.row-top,.subject-line,.message-tags{display:flex;gap:8px;align-items:center;min-width:0}.row-top{justify-content:space-between}.row-top b,.subject-line strong,.preview{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.row-top time,.preview{color:#61716c;font-size:12px}.subject-line i,.message-tags em{font-style:normal;font-size:11px;border-radius:999px;background:#e9f6f2;color:#126b5a;padding:2px 7px;flex:0 0 auto}.message-tags em{background:#eef4f1;color:#33413d}
.pager{border-top:1px solid #dce4e1;padding:8px;display:flex;align-items:center;justify-content:space-between;gap:8px}
.reader-column{background:#fbfdfc;display:grid;grid-template-rows:auto minmax(0,1fr);min-width:0;min-height:0}
.reader-toolbar{min-height:56px;border-bottom:1px solid #dce4e1;padding:8px 12px;display:flex;gap:8px;align-items:center;overflow-x:auto}
.mobile-back{display:none}
.reader-card{min-height:0;overflow:auto;background:#fff}.reader-head{padding:24px 28px 16px;border-bottom:1px solid #edf1ef;display:grid;gap:16px}
.reader-head h1{margin:0;font-size:24px;line-height:1.2;overflow-wrap:anywhere}.reader-head p{margin:6px 0 0;color:#61716c}
dl{margin:0;display:grid;gap:6px}dl div{display:grid;grid-template-columns:48px minmax(0,1fr);gap:12px}dt{color:#61716c;font-size:12px}dd{margin:0;overflow-wrap:anywhere}
.warning{margin:14px 28px 0;border:1px solid #e7c2a7;background:#fff7ed;border-radius:8px;padding:10px 12px;display:grid;gap:4px;color:#6d3517}.warning span{overflow-wrap:anywhere}
.message-body{padding:22px 28px 32px;line-height:1.58;max-width:86ch}.message-body :global(img){max-width:100%;height:auto}.message-body :global(a){color:#126b5a}
.attachments{border-top:1px solid #edf1ef;padding:14px 28px;display:flex;gap:10px;flex-wrap:wrap}.attachments strong{flex-basis:100%}
.empty-state,.reader-empty{display:grid;place-content:center;text-align:center;padding:28px;color:#61716c;min-height:260px}.reader-empty{min-height:100%}.empty-state strong,.reader-empty strong{color:#151d1b;font-size:18px}.empty-state p,.reader-empty p{max-width:42ch;line-height:1.55}
.compose-window{position:fixed;right:24px;bottom:24px;z-index:60;width:min(620px,calc(100vw - 48px));max-height:min(760px,calc(100dvh - 48px));background:#fff;border:1px solid #cbd8d4;border-radius:8px;box-shadow:0 24px 70px rgba(13,27,24,.28);display:grid;grid-template-rows:auto repeat(4,auto) minmax(180px,1fr) auto auto auto;overflow:auto}
.compose-title{min-height:48px;background:#0d1b18;color:#edf7f3;padding:8px 12px;display:flex;align-items:center;justify-content:space-between;gap:10px}.compose-window label,.file-field{display:grid;gap:5px;padding:8px 12px;border-bottom:1px solid #edf1ef}.compose-window label span{font-size:12px;font-weight:800;color:#61716c}.compose-window input,.compose-window textarea{border:0;border-radius:0;padding:4px 0}.body-field textarea{min-height:220px}.compose-window small{padding:0 12px;color:#61716c}.compose-actions{display:flex;gap:8px;padding:10px 12px;align-items:center}
.settings-drawer{position:fixed;right:0;top:0;bottom:0;z-index:55;width:min(440px,100vw);background:#fff;border-left:1px solid #cbd8d4;box-shadow:-18px 0 48px rgba(13,27,24,.18);overflow:auto;display:grid;align-content:start}
.settings-head{position:sticky;top:0;background:#fff;border-bottom:1px solid #dce4e1;min-height:58px;padding:10px 14px;display:flex;align-items:center;justify-content:space-between;z-index:1}.settings-drawer section{padding:14px;border-bottom:1px solid #edf1ef;display:grid;gap:9px}.settings-drawer h2{font-size:16px;margin:0}.settings-drawer textarea{min-height:110px}.inline-fields{display:grid;gap:8px}.check{display:flex;gap:8px;align-items:center}.check input{width:auto}.muted{color:#61716c}.settings-drawer code{background:#eef4f1;border-radius:5px;padding:3px 6px;overflow-wrap:anywhere}
:global(html[data-theme="dark"]) .mail-topbar,:global(html[data-theme="dark"]) .message-column,:global(html[data-theme="dark"]) .reader-card,:global(html[data-theme="dark"]) .compose-window,:global(html[data-theme="dark"]) .settings-drawer,:global(html[data-theme="dark"]) .settings-head{background:#131f1c;color:#eaf3ef;border-color:#2d403a}
:global(html[data-theme="dark"]) .mail-canvas,:global(html[data-theme="dark"]) .reader-column{background:#101715}
:global(html[data-theme="dark"]) .folder-column{background:#10201d;border-color:#2d403a}
:global(html[data-theme="dark"]) input,:global(html[data-theme="dark"]) select,:global(html[data-theme="dark"]) textarea,:global(html[data-theme="dark"]) .message-list button{background:#101715;color:#eaf3ef;border-color:#2d403a}
:global(html[data-theme="dark"]) .message-list button:hover,:global(html[data-theme="dark"]) .message-list button.selected,:global(html[data-theme="dark"]) .quiet,:global(html[data-theme="dark"]) .ghost{background:#20342f;color:#eaf3ef}
:global(html[data-theme="dark"]) .reader-head,:global(html[data-theme="dark"]) .reader-toolbar,:global(html[data-theme="dark"]) .column-bar,:global(html[data-theme="dark"]) .pager,:global(html[data-theme="dark"]) .attachments{border-color:#2d403a}
@media(max-width:1180px){.webmail-shell{grid-template-columns:200px minmax(0,1fr)}.platform-rail{padding:16px 14px}.mail-canvas{grid-template-columns:168px minmax(300px,380px) minmax(0,1fr)}}
@media(max-width:920px){:global(html),:global(body){overflow:auto}.webmail-shell{height:auto;min-height:100dvh;grid-template-columns:minmax(0,1fr)}.platform-rail{position:sticky;top:0;z-index:50;height:auto;display:flex;flex-direction:row;align-items:center;padding:10px 14px}.brand span:last-child{display:block;font-size:initial}nav{display:flex;margin-left:auto;overflow-x:auto}nav a{font-size:14px;width:auto;padding:9px 11px;background:rgba(255,255,255,.06);white-space:nowrap}.mail-app{height:calc(100dvh - 64px)}.mail-topbar{grid-template-columns:minmax(0,1fr) auto;grid-template-rows:auto auto}.mail-title{grid-column:1}.search{grid-column:1 / -1;grid-row:2}.top-actions{grid-column:2;grid-row:1}.top-actions select{display:none}.compose-wide{display:none}.mail-canvas{grid-template-columns:132px minmax(0,1fr)}.reader-column{grid-column:1 / -1;border-top:1px solid #dce4e1;min-height:420px}.message-body{max-width:none}}
@media(max-width:640px){.platform-rail{gap:10px}.brand small{display:none}.mail-app{height:calc(100dvh - 58px)}.mail-topbar{min-height:118px;padding:10px;gap:8px}.mail-title span{max-width:58vw}.top-actions{gap:6px}.top-actions .quiet{display:none}.mail-canvas{grid-template-columns:minmax(0,1fr);position:relative}.folder-column{border-right:0;border-bottom:1px solid #dce4e1;padding:8px;display:block;overflow-x:auto;white-space:nowrap}.folder-group{display:flex;gap:6px}.folder-group button{min-width:108px}.message-column{border-right:0;min-height:0}.reader-column{display:none;grid-column:auto;border-top:0}.mail-canvas.reading .folder-column,.mail-canvas.reading .message-column{display:none}.mail-canvas.reading .reader-column{display:grid;min-height:calc(100dvh - 176px)}.mobile-back{display:inline-flex}.reader-toolbar{position:sticky;top:0;background:#fff;z-index:2}.reader-head{padding:18px 16px 12px}.reader-head h1{font-size:20px}.message-body{padding:18px 16px 26px}.warning{margin:12px 16px 0}.attachments{padding:12px 16px}.compose-window{inset:0;width:auto;max-height:none;border-radius:0;border:0}.settings-drawer{width:100vw}.toast{left:12px;right:12px;top:auto;bottom:12px}}
@media(max-width:430px){.top-actions .primary{padding-inline:10px}.mail-title strong{font-size:14px}.search input{height:40px}.reader-toolbar{gap:6px}.reader-toolbar .quiet,.reader-toolbar .danger{min-height:38px;padding:7px 9px}.message-list button{min-height:112px}.compose-actions{display:grid}.compose-actions button{width:100%}}
</style>