diff --git a/PLAY_DATA_SAFETY.md b/PLAY_DATA_SAFETY.md
index ca50a70..89b0767 100644
--- a/PLAY_DATA_SAFETY.md
+++ b/PLAY_DATA_SAFETY.md
@@ -32,6 +32,7 @@ Where the console offers free text about security practices, two things are wort
| App info and performance | Other app data | No | No | Not collected by us. Stored on the device only. |
| Messages | Other in-app messages | No | No | Not collected by us. Declare the relay hop in the console’s free-text security section if it asks. |
| Messages | Other user-generated content | No | No | Not collected by us. |
+| Messages | Other in-app messages | No | No | Not collected by us. Stored on the device only. |
| Device or other IDs | Device or other IDs | No | No | Not collected. |
## Each answer, and why it is the truthful one
@@ -83,12 +84,23 @@ Push is off until you enable it. When you do, the app mints a random, unguessabl
**Messages → Other user-generated content.** Not collected by us.
-Forum posts, Team activity, character and shard information, notification preferences: all of it is a live read or write against the deployment. Nothing is cached for offline use and nothing is duplicated anywhere else — the app with no signal is an app with no content, which is a limitation and also an accurate description of where the data lives.
+Forum posts, Team activity, character and shard information, notification preferences: all of it is a live read or write against the deployment. Apart from the notification snapshot described in the next entry, nothing is cached for offline use and nothing is duplicated anywhere else — the app with no signal is an app with almost no content, which is a limitation and also an accurate description of where the data lives.
- **Why that answer:** Content is written to the community’s own installation. We have no copy, no access and no way to obtain one.
- **Retention:** Held by the deployment, under its operator’s policy
- **Read from:** `PLAN.md §9 section 2`
+### A snapshot of your notifications, so the inbox opens without a signal
+
+**Messages → Other in-app messages.** Not collected by us. Stored on the device only.
+
+The app keeps the most recent notifications it has already fetched — at most thirty, and only the first page — on the device, so opening the inbox shows you what you had rather than a spinner. It is a copy of what the deployment already sent you and it is refreshed from there; nothing is written here that was not read from your own account. It is scoped to the account that fetched it, so a second person signing in on the same phone is never shown the first one’s messages.
+
+- **Why that answer:** The snapshot is written on the phone from data the deployment had already delivered. It is not uploaded anywhere, and no server we operate is on either end of it.
+- **Retention:** Until you sign out, or the thirty are pushed out by newer ones
+- **In detail:** Signing out deletes the snapshot outright. It lives in the app’s ordinary preference store rather than the encrypted one — sign-in tokens are the thing that store is for — which is worth stating plainly: on a device where someone has root, these are readable, and they are notification bodies rather than credentials.
+- **Read from:** `core/inbox/DataStoreInboxCache.kt, data/repository/AuthRepository.kt`
+
### No analytics, no crash reporting, no advertising
**Device or other IDs → Device or other IDs.** Not collected.
@@ -116,4 +128,4 @@ Not part of the Data Safety form — that form is about the app — but a review
- **Your browser’s user-agent string, truncated** — With the row; blanked on removal.
- **The web server’s access log** — Short-term operational retention, then rotated away.
-Last generated from data dated 2026-08-24. Regenerate with `npm run play:datasafety` after any change to what the app stores.
+Last generated from data dated 2026-09-01. Regenerate with `npm run play:datasafety` after any change to what the app stores.
diff --git a/src/config/sidebar.mjs b/src/config/sidebar.mjs
index 27d069d..e91818e 100644
--- a/src/config/sidebar.mjs
+++ b/src/config/sidebar.mjs
@@ -38,6 +38,7 @@ export const docsSidebar = [
{ label: 'Teams', slug: 'docs/administration/teams' },
{ label: 'Moderation', slug: 'docs/administration/moderation' },
{ label: 'Notifications and email', slug: 'docs/administration/notifications-and-email' },
+ { label: 'Engagement rules', slug: 'docs/administration/engagement-rules' },
{ label: 'Message templates', slug: 'docs/administration/message-templates' },
{ label: 'Managing modules', slug: 'docs/administration/managing-modules' },
{ label: 'The shard connection', slug: 'docs/administration/the-shard-connection' },
@@ -114,6 +115,7 @@ export const plannedSidebar = {
'Teams',
'Moderation',
'Notifications and email',
+ 'Engagement rules',
'Message templates',
'Managing modules',
'The shard connection',
diff --git a/src/content/docs/docs/administration/engagement-rules.mdx b/src/content/docs/docs/administration/engagement-rules.mdx
new file mode 100644
index 0000000..f03dd33
--- /dev/null
+++ b/src/content/docs/docs/administration/engagement-rules.mdx
@@ -0,0 +1,175 @@
+---
+title: Engagement rules
+description: Decide what your site mails and shows people — the rule editor, saved audiences, the trigger catalog and the send log that answers "did they actually get it".
+---
+
+import { Aside } from '@astrojs/starlight/components';
+
+[Notifications and email](/docs/administration/notifications-and-email/) is about where a
+message goes. [Message templates](/docs/administration/message-templates/) is about what it
+says. This page is the part in between: **what makes one get sent at all.**
+
+A **rule** is four decisions — *when* (a trigger), *to whom* (an audience), *by what*
+(channels), and *how often* (timing). **Admin → Engagement → Rules.**
+
+## Nothing sends until you turn it on
+
+Every rule arrives switched **off**. That is true of the ones you make, and it is true of
+the ones your modules ship with them: install a game module and you get a shelf of ready
+rules, all dark, none of them mailing anybody. Turning one on is a deliberate, separate
+act.
+
+The same caution runs through the rest of the screen. Every rule carries a **hard ceiling
+on sends per hour** — you cannot save one without a number — because the failure mode of an
+automated mailer is not a wrong message, it is ten thousand of them at four in the morning.
+
+
+
+## What a rule is made of
+
+**The trigger** is the event that fires it: a house falling into disrepair, a post being
+published, a login failing. Pick it from what is registered — see
+[the catalog](#the-trigger-catalog) below. A rule's trigger is **fixed once the rule
+exists**: its cooldowns, its pending messages and its whole send history are about one
+event, so changing it would silently be a different rule wearing the same name. Make a new
+one instead.
+
+**The audience** is who hears about it. Some are built in — the person the event is about,
+everyone subscribed to it, staff. Others come from your modules and are named in their own
+vocabulary. You can also point a rule at a **saved audience** you composed yourself; see
+[Audiences](#audiences).
+
+**The channels** are how it reaches them: on the site, by email, by push. A rule can name
+more than one, and each channel picks its own template — the same event can be a sentence
+in the inbox and a properly laid-out letter in the mail.
+
+**The timing** is the part worth reading twice.
+
+- A **delay** holds the message before it goes, so a situation that resolves itself never
+ produces a message at all.
+- **Cancel on** names the events that call it back. A warning that a house is about to
+ collapse waits fifteen minutes and is cancelled outright if the owner turns up and
+ repairs it — nobody is told their house was in danger after it stopped being in danger.
+- A **cooldown** is the "not again for a while" limit, counted **per person, per subject
+ and per channel**. Per subject, so a cooldown about one house says nothing about another.
+ Per channel, so "one a day about this house" means one email *and* one inbox item, which
+ is what an operator setting that limit means.
+
+## Audiences
+
+**Admin → Engagement → Audiences** is where you build a named set of people out of the ones
+your modules declare — *members of this Team*, *the sitting governors* — and combine them:
+all of these, any of these, none of these.
+
+One rule governs the whole screen: **composition narrows and never widens.**
+
+- The ceiling of a saved audience is **derived** from the tightest thing in it, never
+ chosen. That is true of "any of" too, where the intuitive answer — the widest of the two —
+ is the wrong one. A ceiling says what an expression is *allowed* to reach, not what it
+ happens to resolve to today.
+- **"None of" is only offered inside an "all of" group.** Alone it would have to mean
+ "everybody except these", which is a broadcast built out of a short list, and it is not
+ offered anywhere it would mean that.
+- Two audiences with no relationship between them — staff and "the person this is about",
+ say — have no honest combined ceiling, so the save is refused rather than guessing which
+ side to take.
+
+Before you save a rule, the editor shows you a **reach preview**: a number, never a list of
+names. It will also tell you when a number is a floor rather than an answer, and when an
+audience resolves to nobody at all and why.
+
+## The ceiling, and why a rule will not offer the audience you expected
+
+Every trigger declares the **widest audience a rule may ever give it**. It is the security
+boundary of the whole system, and it is set in code by whoever declared the event, not in
+the admin panel. Staff-only events cannot be widened into public ones by anybody, including
+you.
+
+Seven values, and they are a **tree, not a ladder**:
+
+| Ceiling | Who that is |
+| --- | --- |
+| `everyone` | Everyone, including signed-out visitors |
+| `authenticated` | Any signed-in user |
+| `subscribers` | Signed-in users subscribed to this event |
+| `members` | Members of a module-declared list |
+| `staff` | Staff only — admins, editors and moderators |
+| `admin` | Administrators only |
+| `owner` | Only the user the event is about |
+
+
+
+So `staff` does not permit `owner`, `members` does not permit `subscribers`, and the editor
+simply does not offer you the audiences the trigger forbids. The one exception proves the
+rule: `admin` sits under `staff`, because every administrator really is staff.
+
+## The trigger catalog
+
+**Admin → Engagement → Triggers** lists every event a rule can be built on, and it is
+read-only on purpose — **there is no table behind it**. A trigger is declared in code, by
+the site or by an installed module, so what you are looking at is whatever registered on
+this boot. Uninstall a module and its triggers stop appearing; nothing was deleted.
+
+Two things it shows that are invisible everywhere else:
+
+- **The variables** each event carries, with an example of each. This is the list a template
+ is allowed to reference — when a message comes out with a hole in it, this is the screen
+ that says why.
+- **The ceiling**, so when the rule editor offers you a narrower set of audiences than you
+ expected, you can see the number it is obeying.
+
+### Dormant rules
+
+A rule can be switched on and still be unable to fire — most often because the module that
+declared its trigger, or the audience it points at, is no longer installed. Those are
+badged **dormant** in the list, with the reason, because "this rule cannot fire" is a
+different fact from "this rule is off" and you need both. The on/off switch keeps working
+on a dormant rule, deliberately: a rule whose module has gone is exactly the rule you most
+want to be able to stop.
+
+## The send log
+
+**Admin → Engagement → Send Log** answers one question: *did that person get that message,
+and if not, why not?* Every attempt is a row — when, what fired it, which user, which
+channel, and the result. Filter by result to go straight to what failed.
+
+| Result | What it means |
+| --- | --- |
+| **Sent** | Handed to the channel successfully |
+| **Failed** | The attempt errored — the reason is on the row, not hidden in a tooltip |
+| **Not sent** | Suppressed before it was attempted: unsubscribed, unverified, or on the [suppression list](/docs/administration/troubleshooting/) |
+| **Bounced** | The receiving server rejected it after the fact |
+| **Marked as spam** | The recipient reported it |
+
+Test sends from the template editor land here too, labelled as such, so you can confirm
+your own test arrived before turning a rule on for real.
+
+
+
+## What a game module brings
+
+A module declares its own triggers and its own audiences, in its own vocabulary, and it may
+ship rules and message bodies to go with them. The Ultima Online module ships a large family
+of them — houses falling to ruin, vendors running out of gold, a governor being seated, a
+guild's fortunes — written in the voice of an in-world office rather than a system alert.
+
+All of them arrive **disabled**, like every other seeded rule. Read the list in
+**Admin → Engagement → Triggers**, turn on the ones your shard should send, and check the
+send log the first time each one fires.
diff --git a/src/content/docs/docs/administration/message-templates.mdx b/src/content/docs/docs/administration/message-templates.mdx
index 88b9c39..05112bd 100644
--- a/src/content/docs/docs/administration/message-templates.mdx
+++ b/src/content/docs/docs/administration/message-templates.mdx
@@ -129,4 +129,5 @@ rejected sender address, a bad password, a relay that will not accept your domai
The log does not store anybody's email address. It keeps a one-way fingerprint instead, so
that a bounce can be matched back to a delivery without the log itself becoming a second
-copy of your members' addresses.
+copy of your members' addresses. The rest of that screen — and the rules that decide a
+message is sent at all — is [Engagement rules](/docs/administration/engagement-rules/).
diff --git a/src/content/docs/docs/administration/notifications-and-email.mdx b/src/content/docs/docs/administration/notifications-and-email.mdx
index 6a081df..7adb945 100644
--- a/src/content/docs/docs/administration/notifications-and-email.mdx
+++ b/src/content/docs/docs/administration/notifications-and-email.mdx
@@ -9,6 +9,9 @@ Four separate delivery paths, each optional, each off until you configure it. A
configures none of them still works — it just never reaches anyone who is not looking at
it.
+This page is about the paths themselves. What decides that a particular message gets sent
+down one of them is a rule — see [Engagement rules](/docs/administration/engagement-rules/).
+
## Email
**Admin → Settings → Email delivery.** The site sends contact-form messages, invitations,
@@ -154,6 +157,10 @@ member sets their own. The defaults are not symmetrical, and the asymmetry is de
- **Muting a Team silences all three for that Team**, whatever the grid says, without
touching any of their other Teams.
+The operator's side of the same question — which events exist, and how wide an audience each
+one may ever be given — is [Engagement rules](/docs/administration/engagement-rules/). When
+a message went nowhere and you want to know why, the send log there is the screen that says.
+