diff --git a/README.md b/README.md
index 0af0432..23df551 100644
--- a/README.md
+++ b/README.md
@@ -6,11 +6,38 @@ that lets you pick any mailbox (folder) and instantly see:
- **Unread messages per month** — bucketed by calendar month, sorted
chronologically, and drawn as a bar chart.
- **Senders by frequency** — every sender in the mailbox ranked by how many
- messages they sent, with their unread count alongside.
+ messages they sent, with their unread count alongside. Senders with a single
+ message are collapsed behind a toggle to keep the list scannable.
+- **Threads by size** — messages grouped by (normalised) subject, largest
+ conversations first.
It also shows summary cards for total messages, total unread, and the number of
unique senders.
+Click any **month** (bar or table row), **sender name**, or **thread** to open
+an in-page reader listing exactly those messages, **grouped into threads**. Skim
+message bodies with the keyboard and triage without leaving the report:
+
+- ↑/↓ (or j/k) — move between rows
+- ←/→ — collapse/expand the selected thread
+- Del/Backspace — move to Trash
+- A — archive
+- M — move to another mailbox (a filterable folder list pops up)
+- Esc — close the reader (or the move popup)
+
+When a **thread header** is selected, Del/A/M
+apply to the **whole thread**; when an individual message is selected they apply
+to just that message.
+
+The chart and tables update live as you delete, archive, or move. Skimming a
+message does **not** mark it as read, so your unread counts stay meaningful.
+Threads are approximated from subjects (Thunderbird's message headers don't
+expose reply references without fetching each message in full).
+
+The report stays in sync with the folder while it's open: new mail, moves,
+archiving, deletions, and read/unread changes made anywhere in Thunderbird are
+reflected in the stats and tables (and in the reader, if open) automatically.
+
## How it works
Click the **⚡ Mailbox Analytics** toolbar button. A tab opens with a mailbox
@@ -36,8 +63,12 @@ the root of the zip) and rename it to `thunderstruck.xpi`.
## Permissions
- `accountsRead` — to enumerate accounts and folders for the picker.
-- `messagesRead` — to read message headers (author, date, read state) in the
- selected folder. Message bodies are never accessed.
+- `messagesRead` — to read message headers (author, date, read state) for the
+ report, and message bodies on demand when you open one in the reader.
+- `messagesMove` — to archive a message from the reader (A).
+- `messagesDelete` — to move a message to Trash from the reader (Del).
+- `tabs` — to open the analytics page in a tab and reuse it instead of opening
+ a new one on every click.
## Notes & compatibility
diff --git a/analytics.css b/analytics.css
index 3ebd81a..849a226 100644
--- a/analytics.css
+++ b/analytics.css
@@ -186,3 +186,322 @@ canvas {
color: var(--muted);
font-size: 0.82rem;
}
+
+/* Clickable rows / cells that open the reader. */
+.data-table tr.clickable,
+.data-table td.clickable {
+ cursor: pointer;
+}
+
+.data-table tr.clickable:hover,
+.data-table td.clickable:hover {
+ background: var(--accent-soft);
+}
+
+.data-table td.clickable > div:first-child {
+ color: var(--accent);
+}
+
+.data-table td.clickable {
+ color: var(--accent);
+}
+
+/* Expand/collapse toggle row (e.g. one-message senders). */
+.data-table tr.toggle-row td {
+ color: var(--accent);
+ cursor: pointer;
+ font-size: 0.85rem;
+ user-select: none;
+}
+
+.data-table tr.toggle-row:hover td {
+ background: var(--accent-soft);
+}
+
+/* --- Reader overlay ------------------------------------------------------ */
+
+.reader {
+ position: fixed;
+ inset: 0;
+ z-index: 20;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 2vh 2vw;
+ background: rgba(0, 0, 0, 0.45);
+}
+
+.reader[hidden] {
+ display: none;
+}
+
+.reader-panel {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ width: min(1100px, 100%);
+ height: min(92vh, 100%);
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
+}
+
+.reader-header {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 0.75rem 1rem;
+ border-bottom: 1px solid var(--border);
+}
+
+.reader-header h2 {
+ margin: 0;
+ font-size: 1.05rem;
+ flex: 1 1 auto;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.reader-status {
+ color: var(--muted);
+ font-size: 0.85rem;
+ flex: 0 0 auto;
+}
+
+.reader-close {
+ flex: 0 0 auto;
+ padding: 0.3rem 0.6rem;
+ background: transparent;
+ color: var(--muted);
+ font-size: 1rem;
+}
+
+.reader-close:hover {
+ color: var(--fg);
+}
+
+.reader-body {
+ flex: 1 1 auto;
+ display: grid;
+ grid-template-columns: minmax(240px, 340px) 1fr;
+ min-height: 0;
+}
+
+.reader-list {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ overflow-y: auto;
+ border-right: 1px solid var(--border);
+ outline: none;
+}
+
+.reader-item {
+ padding: 0.5rem 0.75rem;
+ border-bottom: 1px solid var(--border);
+ cursor: pointer;
+}
+
+.reader-item.selected {
+ background: var(--accent-soft);
+}
+
+.reader-item .reader-subject {
+ font-weight: 600;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.reader-item.unread .reader-subject::before {
+ content: "";
+ display: inline-block;
+ width: 7px;
+ height: 7px;
+ margin-right: 0.4rem;
+ border-radius: 50%;
+ background: var(--unread);
+ vertical-align: middle;
+}
+
+.reader-item .reader-meta {
+ color: var(--muted);
+ font-size: 0.8rem;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* Thread header rows and their child messages. */
+.reader-item.thread-row .reader-line {
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+}
+
+.reader-item .twisty {
+ flex: 0 0 auto;
+ width: 1em;
+ color: var(--muted);
+ cursor: pointer;
+ user-select: none;
+}
+
+.reader-item.thread-row .reader-subject {
+ flex: 1 1 auto;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.reader-item .thread-count {
+ flex: 0 0 auto;
+ background: var(--accent-soft);
+ color: var(--accent);
+ border-radius: 999px;
+ padding: 0 0.5rem;
+ font-size: 0.75rem;
+ font-weight: 600;
+}
+
+.reader-item.child {
+ padding-left: 1.75rem;
+}
+
+.reader-item.child .reader-subject {
+ font-weight: 400;
+}
+
+.preview-note {
+ display: inline-block;
+ margin-bottom: 0.4rem;
+ padding: 0.1rem 0.5rem;
+ border-radius: 999px;
+ background: var(--accent-soft);
+ color: var(--accent);
+ font-size: 0.78rem;
+ font-weight: 600;
+}
+
+/* Move-to-mailbox popup, layered over the reader panel. */
+.move-popup {
+ position: absolute;
+ inset: 0;
+ z-index: 5;
+ display: flex;
+ align-items: flex-start;
+ justify-content: center;
+ padding-top: 12vh;
+ background: rgba(0, 0, 0, 0.35);
+}
+
+.move-popup[hidden] {
+ display: none;
+}
+
+.move-panel {
+ width: min(520px, 92%);
+ max-height: 70%;
+ display: flex;
+ flex-direction: column;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ overflow: hidden;
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4);
+}
+
+#move-filter {
+ border: none;
+ border-bottom: 1px solid var(--border);
+ padding: 0.7rem 0.9rem;
+ font-size: 0.95rem;
+ background: var(--bg);
+ color: var(--fg);
+ outline: none;
+}
+
+.move-list {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ overflow-y: auto;
+}
+
+.move-item {
+ padding: 0.45rem 0.9rem;
+ cursor: pointer;
+ font-size: 0.9rem;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.move-item.selected,
+.move-item:hover {
+ background: var(--accent-soft);
+}
+
+.reader-preview {
+ overflow-y: auto;
+ padding: 1rem 1.25rem;
+ min-width: 0;
+}
+
+.preview-head {
+ border-bottom: 1px solid var(--border);
+ padding-bottom: 0.6rem;
+ margin-bottom: 0.8rem;
+}
+
+.preview-head h3 {
+ margin: 0 0 0.3rem;
+ font-size: 1.1rem;
+}
+
+.preview-from {
+ font-size: 0.9rem;
+}
+
+.preview-when {
+ color: var(--muted);
+ font-size: 0.82rem;
+}
+
+.preview-body {
+ margin: 0;
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+ font-size: 0.85rem;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+.reader-help {
+ padding: 0.5rem 1rem;
+ border-top: 1px solid var(--border);
+ color: var(--muted);
+ font-size: 0.8rem;
+}
+
+.reader-help kbd {
+ font-family: inherit;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ padding: 0.05rem 0.35rem;
+ margin: 0 0.1rem;
+}
+
+@media (max-width: 640px) {
+ .reader-body {
+ grid-template-columns: 1fr;
+ grid-template-rows: 40% 60%;
+ }
+ .reader-list {
+ border-right: none;
+ border-bottom: 1px solid var(--border);
+ }
+}
diff --git a/analytics.html b/analytics.html
index 4f7bbd0..b4ee121 100644
--- a/analytics.html
+++ b/analytics.html
@@ -38,6 +38,18 @@
+ Messages grouped by subject, largest conversations first. Click one to open it. Counts every message in the mailbox grouped by sender, most frequent first.Threads by size
+
+
+
+
+
+
+ # Thread Messages Unread Senders by frequency