# Marketing Desk — marketing automation

Twelve destinations in three labelled groups, in an **inset-pane sidebar shell**. Marketing automation for a
small SaaS workspace: what sending did, the automations that run continuously,
the one-off sends waiting on approval, the audiences everything goes to, the
templates they send, and whether any of it actually arrives.

Open `MarketingDesk.dc.html`. The floating button bottom-right lists every registered
screen and carries the theme toggle.

---

## The shell

The frame is the point of this template, so it is worth stating exactly:

- The whole window is **one tinted plane** (`--sidebar`), and that tint is the app
  background.
- The **sidebar sits directly on the tint** — no surface of its own, no border,
  nothing boxing it in.
- The content lives in **one rounded pane** on the plain page colour
  (`--background`), inset from all four edges, with a single hairline edge and
  **no shadow**.
- The insets are **deliberately uneven**: 10px top, 8px left and right, 14px
  bottom. Even insets read as a mistake at this scale; the heavier bottom makes
  the pane read as seated.
- **Only the pane scrolls.** The tint and the sidebar never move, so the account
  menu in the sidebar footer is always reachable.

Inside the pane the content is a **tile grid**. The frame stays flat — the pane
keeps one hairline and no shadow — and the tiles take the card surface, a
hairline and the smallest shadow in the system, which is the single axis of depth
this product spends. Two rules keep it from becoming a card grid of nothing: a
tile is a group of figures that belong together, and every figure inside one
still names the array it came from.

Spans are deliberately uneven — 7/5, 8/4, 5/4/3 — so no grid line runs the length
of a page, and every tile drops to full width below 900px. **Curves carry the
data as well as the boxes**: a donut for a decomposition whose slices sum to the
figure in its centre, an arc gauge for a rate against a stated ceiling, a
smoothed spark for a series, a pill meter for a proportion whose track is a
number the caption states. Straight bars are kept for one job — the day chart on
Overview, where the reader compares days against each other.

One CSS detail worth keeping: `.md-pane` sets `width:auto`. The design system's
`.o-sidebar-inset` is `width:100%`, and a percentage width leaves the inline
margins nothing to take from — the pane would hang its right edge past the window
and the gap would only appear on three sides.

### Responsiveness, declared once

| Width | Sidebar | Pane |
|---|---|---|
| ≥1280px | 240px, labelled | inset 10/8/14 |
| 1024–1280px | 60px icon rail, labels become tooltips | inset, full remaining width |
| 768–1024px | drawer over the pane; the trigger takes the pane header's leading slot | inset, **full** width — the sidebar's track leaves the grid, it does not shrink to zero |
| <768px | drawer | flush: no inset, no radius |

Below 900px the search field collapses to an icon button that opens the same
palette. Nothing else is lost on the way down.

### App-scope duties

| Duty | Where |
|---|---|
| Search | pane header, read-only field opening the ⌘K palette |
| Session and account | sidebar footer — workspace settings, team, sign out |
| Utility | pane header — notifications, theme |
| Page context | the page header band, first band inside the pane |

---

## The screens

| Route | Primary display | Why |
|---|---|---|
| `/` Overview | hero sending tile → rate gauges → day chart → four queues | the only screen that mixes journey and broadcast sending |
| `/calendar` | month grid and week ladder, one destination two views | what is dated, and what runs continuously beside it |
| `/approvals` | three queues, one act-ability predicate | yours, ones you submitted, ones waiting on somebody else |
| `/people`, `/people/:id` | tile per person → profile with what they own and have written | a team is faces and responsibility, not a seat count |
| `/contacts`, `/contacts/:id` | lifecycle donut + engagement gauge over a sortable table | the sample the workspace can actually see, stated as a sample |
| `/inbox` | thread tiles with notes, reactions and a composer | a mention is owed until the person named writes on that record |
| `/reports` | sparks, donut and gauges over per-audience and per-owner tables | the same window as Overview, decomposed |
| `/activity` | feed grouped by day, plus who-did-what donut | registered off the sidebar; reached from Inbox, Overview and each profile |
| `/journeys`, `/journeys/:id` | **list-detail with a step ladder** | a journey answers "where do people stop" — a ladder shows it, a row cannot |
| `/journeys/new` | staged builder | name, trigger, audience and a ladder you assemble, with a summary that moves as you build |
| `/journeys/:id/steps` | staged step editor | a draft's ladder, editable — its own screen because the record commits instantly |
| `/broadcasts` | figure row → delivery-by-day chart → sortable table | one-off sends are a queue; a table is the right answer |
| `/broadcasts/:id` | **the send funnel** | attempted → delivered → opened → clicked, every bar against the top of the funnel |
| `/audiences` | two column stacks, one block per audience | a count plus the rule it counts plus five real members |
| `/audiences/:id` | the members, and everything that sends to it | this is what an audience count anywhere else drills through to |
| `/audiences/new` | staged segment builder | rules with a preview that **runs** them over the contact sample |
| `/content` | sortable, filterable table of email templates | usage is derived from what points at the template |
| `/deliverability` | **a DNS matrix** over the failure-source table | three checks across three domains is a grid question |
| `/settings` | staged form | the one place the workspace itself is configured |
| `/settings/team` | staged table of roles | a role change moves the review badge in the sidebar |
| `/settings/channels` | instant; disconnect confirms | what the workspace is plugged into, and what has stopped working |
| `/settings/sending` | instant; a re-check is an action | domains and their three checks, with the matrix on Deliverability |
| `/settings/notifications` | instant, per switch, with undo | per person, not per workspace |
| `/settings/billing` | read, plus two dialogs | plan usage read off the workspace's own contact count |
| `/settings/api` | instant; revoke confirms | keys shown once, webhooks with their real state |
| `/me/profile` | staged form | saving changes your face and name everywhere at once |
| `/signin` | centred card, no shell | auth has no navigation owner |
| `/forgot-password` | request | enumeration-safe: the next screen is the same whatever is typed |
| `/forgot-password/sent` | sent confirmation | a screen, not a toast — address, expiry, resend with a real cooldown |
| `/reset-password` | set-new behind the token | one password field, reveal toggle, live checklist |
| `/reset-password/expired` | expired or already used | one action, and it is the one that gets the user moving |
| `/reset-password/done` | the outcome | states the session consequence and routes somewhere real |

Journeys is the destination that would most plausibly have been a table and is
not.

---

## Derivation

Every figure names the array it came from. The ones worth knowing:

- **Journey step counts close.** On every step `entered = completed + at + failed
  + exited`, and `entered` on a step equals `completed` on the one above it. So
  enrolment is `sum(steps.at)`, conversion is the last step's `completed` over the
  first step's `entered`, and each step's drop-off meter has that step's own
  `entered` as its track — the unfilled tail is whoever is still waiting there.
- **Stopping a journey moves people from `at` into `exited`**, so the arithmetic
  stays closed and every figure that reads it moves with the commit.
- **Delivery is one series, two sources.** `JOURNEY_DELIVERY` is the daily journey
  send log; each broadcast's own numbers are added on the day it went out. The
  overview headline, the overview chart and the broadcasts table therefore cannot
  disagree, and the headline's qualifier decomposes into exactly those two
  sources.
- **The funnel and the figures on a send report read one record.** Delivery rate
  is `delivered / sends`, open rate is `opened / delivered`; the funnel draws each
  against `sends` so the shape is the funnel, and each row states its rate against
  the row above.
- **Deliverability is not time-scoped and says so.** Send attempts are journey
  step `entered` plus broadcast `sends`; failures are journey step `failed` plus
  each **settled** partial broadcast's `sends − delivered`. A send still in flight
  is not partial — what it has not handed off yet is pending, not failed — so it
  never appears in the failure table at an invented rate. The table underneath is
  those failures decomposed by source, worst rate first, every row linking to the
  record it came from.
- **One failure threshold, compared at the precision it is displayed.**
  `isOverFailureThreshold` in `enums.js` is what the journey step badge, the
  deliverability tone and the footnote count all read, so a rate rendered as
  "2.0%" is never left untoned beside a toned one and the count always matches the
  rows a reader can see.
- **Content usage is derived, never stored.** A template's "what sends it" column
  reads the journeys and broadcasts that name it — which is also why archiving one
  that is still in use is refused with the count rather than hidden.
- **Audience sizes are materialised counts** — what the segment engine wrote at
  `evaluatedAt`. The record screen shows the rule in words and five real members,
  so the number is checkable beside itself as well as by the predicate it names.
- **`broadcastRecipients()` is one function with two branches**: a sent broadcast
  reports what actually went out, an unsent one reports its audience's size as it
  stands now. The table cell says which.
- **One act-ability predicate**: `BROADCAST_FLAGS.isApprovableBy` — in review,
  **not** submitted by the viewer, **and** the viewer's role entitled to approve.
  The nav badge, the overview section, the row button, the bulk bar and the team
  screen's own figure all read it, so changing a role on `/settings/team` moves
  all of them at once.

- **One password rule list.** `PASSWORD_RULES` in `enums.js` is what the live
  checklist renders and what gates the submit, so what the screen promises and
  what it enforces cannot drift. `RESET_LINK_MINUTES` is stated on the request
  screen, on the confirmation and on the expired screen, and the set-new screen
  names the clock time the link dies.

## Creating things

Six creation actions, each placed where the thing lives and sized to what it
needs:

| Action | Where | Shape |
|---|---|---|
| New journey | Journeys header | its own staged screen — a name, a trigger, an audience and a ladder is too much for a dialog |
| Edit a draft's steps | the journey record | its own staged screen, because the record itself is instant |
| New audience | Audiences header | its own staged screen, so the rule preview has room to be a real preview |
| New broadcast | Broadcasts header | staged dialog — three decisions |
| New template | Content header | staged dialog — two fields |
| Add sending domain | Deliverability header | staged dialog — and the new domain immediately moves the nav badge, because nothing is authenticated on the day it is added |
| Invite teammate | Team header | staged dialog — the invited person holds a role but cannot use it until they accept, so they are not counted as an approver |

Everything created is a real record written into the fixture, so the figures that
claim to derive from it actually move: a new journey appears in the list recessed
as a draft and its templates stop reading as unused; a new audience appears with
its estimate and is offered to the next broadcast; a new domain drops the
authenticated count and raises the deliverability badge.

**Bands are cards too.** The record screens (journeys, broadcasts, audiences,
content, deliverability, settings) group their content in `Band`, and a Band takes
the same surface, hairline and single shadow as a tile — and redefines
`--gutter-page` to the card gutter inside itself, so a full-bleed table or filter
bar lands on the card's measure instead of the page's. That is why a record screen
and a tiled screen read as one product rather than two.

**Two live previews.** The journey builder's summary is arithmetic over the steps
as written — how many emails, how many days from entry to the last send. The
audience builder runs the rules' own predicates over the forty-contact sample and
says exactly what it did: the matched count is exact for the sample, the size is
an extrapolation to the reachable list rounded to ten, and the saved audience
stays marked as estimated until the engine evaluates it in full.

**Sorting** is one hook, one header control and one comparator (`useSort`,
`SortHead`, `sortRows` in `MdParts.jsx`), so the broadcasts and content
tables behave identically. The comparator knows nothing about the domain; each
table passes an accessor map.

## Commit models

Nine screens are **instant**; `/settings` and `/settings/team` are **staged**, and
say so at the top of their file. Publish, pause, resume, approve, cancel a
schedule, retry, archive and restore apply immediately and raise an undo toast.
**Stopping a journey** is the one irreversible action and opens a confirm dialog
naming the consequence. Two things need a form — writing a new broadcast, sending
one back with a note — and both open a **staged dialog** from an instant screen.

A record with no transition anybody can take is **recessed, not disabled**: a
draft or stopped journey, a broadcast that is sending or sent, an archived
template. One rule, three entities.

---

## Files

| File | Holds |
|---|---|
| `MarketingDesk.dc.html` | the Design Component; mounts `MdApp`, no tweaks |
| `nav.js` | the route table, its projections, and what deliberately is not a route |
| `enums.js` | four status enums with tone reasons, classification lists, roles and their entitlements, derived-flag predicates, transition sets |
| `fixtures.js` | audiences (with a machine-readable `sampleRule`), journeys, broadcasts, templates, sending domains, the daily journey send log, six members, the contact sample, notes, the activity feed, connected channels, keys, webhooks, billing and notification preferences |
| `metrics.js` | every figure on every screen |
| `MdShell.jsx` | the shell described above |
| `MdParts.jsx` | bands, figures, the day chart, meters, list furniture, the staged form bar, dialogs |
| `MdCards.jsx` | the card layer: tiles and the tile grid, stat tiles, donut, arc gauge, smoothed spark, pill meter, faces, notes, the composer, chips, key-values |
| `PeopleScreens.jsx` `InboxScreens.jsx` `ApprovalsScreen.jsx` `ContactsScreens.jsx` `CalendarScreen.jsx` `ReportsScreen.jsx` `SettingsMoreScreens.jsx` | the screens added with the card layer |
| `OverviewScreen.jsx` `JourneysScreen.jsx` `BroadcastsScreen.jsx` `BroadcastScreen.jsx` `AudiencesScreen.jsx` `AudienceScreen.jsx` `ContentScreen.jsx` `DeliverabilityScreen.jsx` `SettingsScreens.jsx` `AuthScreens.jsx` | the screens |
| `kit.css` | layout only; every value is a token |
| `lib/` | `contracts.js`, `ui-runtime.js`, `command-palette.js`, `template-menu.js` — byte-identical across the templates here |
| `boot.js`, `ds-base.js` | loading |

## Local additions, reported

Built local to this product because the design system has no equivalent:

- **`Tile`**, **`Tiles`**, **`StatTile`**, **`StatRow`** — the card grid described
  above, with uneven spans and one depth token.
- **`Donut`**, **`DonutKey`**, **`Gauge`**, **`Spark`**, **`PillMeter`** — the curved
  data shapes. Each takes its numbers already computed; none of them decides a tone.
- **`Face`**, **`FaceStack`**, **`Note`**, **`MentionText`**, **`Composer`**,
  **`Chip`**, **`KeyValues`** — the social layer's furniture. A mention token in a
  body is a member id resolved at render, never a written name.
- **`Band`** and **`Figures`** — the full-bleed band and hairline-divided figure
  row the record screens still use.
- **`DayChart`** — stacked daily bars, one tooltip per day carrying the numbers
  the bar is drawn from.
- **`Meter`** — a multi-part proportion whose track is a stated number. Used by
  the journey ladder, the send funnel and the audience blocks.
- **`StepRow`** (`JourneysScreen.jsx`) — one step of a journey.
- **`AudienceBlock`** (`AudiencesScreen.jsx`), **`CheckCell`**
  (`DeliverabilityScreen.jsx`).
- **`Transitions`**, **`RowActions`**, **`FilterBar`**, **`BulkBar`**,
  **`ExportMenu`**, **`FormBar`**, **`ConfirmDialog`**, **`FormDialog`**,
  **`RouteNotBuilt`**, **`PageHeader`** — the same shapes the other templates here
  carry.

## Known gaps

Deliberate, not oversights:

- **Auth is sign-in plus the full password reset flow.** Signup, MFA, OTP,
  verify-email, accept-invite and lockout are not built, so nothing links to
  them. The reset flow is all five screens §7.5 asks for rather than three,
  because a dead link with nowhere to go is the failure that section exists to
  prevent.
- **No contact directory**, so an audience's full membership is not reachable. The
  audience record says so in place of pretending: five real members beside the
  count, and the rule that produced it.
- **No template editor.** Content manages the library — status, usage, archiving —
  and the row does not pretend to open a composer that does not exist.
- **The daily send log holds thirty days**, so the period control offers 7, 14 and
  30 rather than a 90-day option with sixty empty days in it.
