A window snapped to half the screen (640-800 px) or a 1366x768 laptop
at 150 % (911 px) got the phone and tablet layout: bottom tab bar,
folders in a drawer, Settings as a phone list with a Back arrow. The
layout switched at the 64rem (1024 px) breakpoint.
Now the phone layout starts below 600 px. From 600 to 1024 px the app
is a compact desktop: app rail at the side, folder sidebar folded to
its icons (the existing collapsed state, opened per session with its
toggle), Settings with its two panes and a narrower page list. Below
840 px the split layout opens a message in the list's place, as the
focused layout does. This holds in the browser and the desktop app,
in the Pro shell (which now runs from 600 px) and under Mountain View.
- `hooks/use-media-query.ts`: classifyWidth, PHONE_BELOW 600,
FULL_DESKTOP 1024; the tablet class (`isTablet`) is gone, the store
has `isCompact` instead. Plugins still see "tablet" for it.
- `app/globals.css`: a `desk:` breakpoint at 37.5rem for the layout
classes, and a `tap:` variant for 44px rows on phones and touch
screens below 1024 px (was every window below 1024 px).
- Touch screens below 1024 px keep swiping list rows (was phones only).
- The boot skeletons, boot script, mail app, sidebar, reader, admin
shell and calendar, contacts and files follow the new widths.
Tests changed with the behaviour: the tablet cases in pro-gate,
mail-app-mv-composer-move, mail-app-compose-handoff,
mail-app-thread-expand-mark-read, mobile-tour-end and
thread-list-item-columns now run on a phone or a compact desktop, as
those widths no longer exist as a tablet. mail-app-short-window-tab-bar
is replaced by mail-app-compact-layout: a phone on its side is a
compact desktop with the rail at the side, so the short-window tab bar
rule is gone. Class and media-query assertions follow desk:/tap: and
37.5rem; the 32px-text test uses 1100 px, since 1440 px at 32px is now
a compact desktop.
The #1327 store test now serves its optional plugin with defaultOn false:
it checks that every plugin is listed when IndexedDB fails, and a plugin
the admin didn't force is on by default now.
Offline sat in the middle of Backup & reset, privacy settings were spread
over Reading, Composing and Message list, and the version card took room
from the page list in the sidebar's footer. The search matched letters
inside words ("lock" found Time format and Send Checks) and knew no other
words for a setting ("offline", "autoreply", "logout" found nothing).
- Offline is a page in the top group: Keep mail on this device, what is
kept, Remove everything from this device, and the Outbox as its own
section. Old `/settings/backup/offline` links and the offline notice
land there.
- Privacy holds remote images and trusted senders, read receipts, sender
pictures from the web, the desktop app's usage info and crash reports,
and a link to the push relay on Notifications. Reading, Composing and
Message list keep a link row; old links to those sections land on
Privacy.
- About is the last page and holds the version card; the footer keeps the
Advanced switch.
- The search matches whole words and word starts (substrings only in
Chinese, Japanese and Thai), and each row can carry synonyms in the
user's language. A page matches by its name and keywords or when one of
its rows has every word. Account gets a Sign out row so "logout" leads
somewhere.
- The index test now also fails when a page draws a section the index
doesn't know, and a new test fails on two indexed rows with the same
name. Tests that rendered remote content and sender pictures on Reading
and Message list render Privacy instead, since that's where the rows are.
The usability pass of 2026-11-04 found the General and Notifications
pages hard to read, and the desktop app showing browser-only rows.
- Time zone: a searchable list by city and offset, common zones
first, with today's city names (Kolkata, Kyiv, Ho Chi Minh City).
The stored value stays the IANA id the runtime knows.
- First day of the week: "First day of the week in calendars".
- Desktop app: no browser link handlers (the app has its own "Default
mail app" row), no usage info on General (it moves to Privacy),
time zone and app lock texts without the browser and tabs.
- Taskbar flashing and quiet hours move to Notifications.
- "Windows" is now "App windows".
- Notifications: "Sound for new mail", "Sound for reminders", a
"Sound" section with a "Tone" row, "Notifications when Bulwark is
closed" with "Turn on for this device", a short badge description.
- Link handlers: "Open email links in Bulwark" with "Use Bulwark for
email links", no "Technically" sentence.
- App lock: "The PIN stays on this device.", the protection text
behind "What does this protect?", "1 hour" and "4 hours", one line
in the PIN dialog instead of repeating its title.
- Usage info names "the app's version numbers", as the desktop's
first run does, instead of the Node version.
Test changes: the telemetry wording test looked for "Node version",
which the text no longer names; the search-anchor test now fills in
{appName} before comparing, as the search index does, because row
labels name the app.
The formatter cache test expected the numeric date for every older mail; earlier this year it now reads "Oct 24" unless a date order is picked in the region settings, so the test expects that for those dates.
A lone capitalised word in brackets now counts as a fill-in, so two
cases of ordinary text changed: the cross-language case uses lowercase
"[votre] [ваше]", and the broken word-list case uses "[Your name]", so
both still test what they were written for.
Where people may sign in to a server of their own (the custom endpoint
setting and the desktop app's standalone mode), the sign-in form asked
for a "JMAP Server" first, refused a host without https:// with the
browser's own bubble and talked about CORS. Now the email address comes
first and the server is looked up from its domain (JMAP well-known, SRV,
then the admin's default) through a new budgeted route. The "Mail
server" field opens only when nothing is found, takes a bare host, and
keeps the CORS hint behind "Having trouble?" (not in the app). Gmail,
Outlook.com and iCloud addresses get a plain note that these offer no
JMAP. The app's browser button stays the main one and asks for what it
needs under the field it needs.
Also on the page: the theme button is an icon named "Light or dark",
the heading is the app's name only when the admin gave one (else "Sign
in", without the subtitle repeating it), "I have a code from my
authenticator app", "Add account", and no 11 px text.
login-page-i18n.test.tsx now names the app for the subtitle test and
waits for the heading, since the default name no longer shows a subtitle,
and finds the theme button by its new name.
The policy-defaults test now expects the new build default of 5 seconds
for an invalid admin value, and the settings-search tests look for the
row under its new label "Undo send".
The Move lists (message menu, reader Move button and its phone panel,
selection toolbar, Move dialog) now share one model in lib/move-menu.ts.
In the desktop app the native copy of the menu lost the shared accounts'
headers and every folder's nesting, so Inbox, Sent, Spam and Trash
showed once per account with nothing to tell them apart.
- Only the message's own account at the top; other accounts a move can
reach are submenus under their names.
- System folders first (Inbox, Sent, Archive, Spam, Trash), then the
user's folders by name; subfolders nest, as submenus natively.
- The open folder shows greyed instead of missing.
- A Recent group, a filter field on long lists, and "Choose folder…"
for the dialog, which has the filter too.
Tests changed: the open folder is now shown disabled rather than left
out, the own account of a shared-mailbox message is a submenu, and the
selection toolbar's filter field has its own label.