Wave 8 · System sheets

States & notifications

The recipes every surface reuses for empty / loading / error, and the single map of what triggers which channel. Claude Code: apply these patterns, never invent new ones per page.

Empty states

rule: name what's missing, say why that's fine or what fills it, offer ONE action. Never a sad blank box.
{{ em.where }}
{{ em.ic }} {{ em.title }} {{ em.sub }}

Good empty states are honest ("no one has posted yet") — never fake urgency, never render placeholder data as if real.

Loading

skeletons mirror the real layout at real size — no spinners for page loads; spinners only for in-place actions.
List / table skeleton
In-place action (button)
Button keeps its size, label changes to the gerund, disabled until resolved. On success: label flips to "✓ Saved" for 1.5s, then back. On failure: see error recipe.

Errors

plain words, keep the user's work, always a retry. Never a code as the headline.
Inline (form / action failed)
Couldn't save your RSVP

Your picks are still here. It's on our end — try again in a moment.

Section failed (partial page)
Coming up couldn't load
The rest of the page is fine.
a failed panel NEVER takes the page down — render the dashed slot in place
Full page (route crashed)
🐾
Something broke on our end
It's logged and we'll fix it. Your data is safe.

Notification map

what triggers what, where. In-app is free; push is earned; email is for things that matter tomorrow; SMS is day-of only.
{{ nc }}
{{ nr.event }} {{ nr.who }} {{ nr.inapp }} {{ nr.push }} {{ nr.email }} {{ nr.sms }} {{ nr.note }}

● default on · ○ opt-in · — never. Every row maps to a toggle in Settings › Notifications (grouped, not one-per-row). Batching: in-app instant; push batched 15 min except day-of; email digested daily except auth + march-critical; SMS only Sept 11–26 for RSVPs who opted in.