/* Marketing Desk — the fixture. One record, one entry: every screen reads this file, and
   nothing is written twice to make two screens agree.

   What is stored and what is not:
   · Audience sizes are MATERIALISED counts — what the segment engine wrote at
     `evaluatedAt` — because that is what a segment count is in a real product.
     Each carries the rule in words and a five-member sample, so the figure is
     checkable beside the number as well as by the predicate it names.
   · Journey step counts are the journey's own run counts since it was
     published, and they close: entered = completed + at + failed + exited, on
     every step, with entered on step n equal to completed on step n-1. Every
     figure on the journeys screen is arithmetic over these.
   · JOURNEY_DELIVERY is the daily journey send log, 30 rows. Broadcast delivery
     is not duplicated into it — the day series in metrics.js adds each
     broadcast's own numbers on the day it went out, so the overview headline and
     the broadcasts table cannot disagree.
   · Every email that sends names a template in CONTENT, so the content library's
     usage column is derived from the journeys and broadcasts that point at it,
     never stored on the template.
   · Audiences overlap by design; their sizes do not sum to the workspace and no
     screen adds them up. */

const MD_NOW = new Date("2026-08-25T09:40:00Z").getTime();
const H = 3600e3;

const MD_WORKSPACE = {
  name: "Northwind Studio",
  contacts: 6142,
  unsubscribed: 214,
  sendingDomainId: "dom_1",
  replyTo: "hello@northwind.studio",
  timezone: "Europe/London",
};

const MD_SIGNED_IN = "mem_1";

/* The link a reset email carries. `sentAt` is what the set-new screen counts
   down against, using RESET_LINK_MINUTES from enums.js. */
const MD_RESET_LINK = { email: "dana@northwind.studio", sentAt: MD_NOW - 4 * 60e3 };

/* Six people, each with a face, a role, an entitlement and the thing they are
   responsible for. What they OWN is not stored here — it is the journeys,
   broadcasts and audiences that name them (personLoad in metrics.js), so a
   profile cannot claim work the lists disagree with. */
const MEMBERS = [
  { id: "mem_1", name: "Dana Whitlock", role: "Lifecycle marketing lead", teamRole: "admin", email: "dana@northwind.studio", avatar: "../../assets/img/avatar-f-17.jpg",
    focus: "Owns the trial and customer lifecycle — onboarding, welcome, renewals.", location: "London", timezone: "Europe/London", joinedAt: MD_NOW - 903 * DAY_MS, lastActiveAt: MD_NOW - 12 * 60e3 },
  { id: "mem_2", name: "Ivo Karlsen", role: "Marketing operations", teamRole: "editor", email: "ivo@northwind.studio", avatar: "../../assets/img/avatar-m-13.jpg",
    focus: "Keeps sending healthy: domains, suppression, the newsletter calendar.", location: "Oslo", timezone: "Europe/Oslo", joinedAt: MD_NOW - 611 * DAY_MS, lastActiveAt: MD_NOW - 3 * H },
  { id: "mem_3", name: "Priya Raman", role: "Campaign manager", teamRole: "editor", email: "priya@northwind.studio", avatar: "../../assets/img/avatar-f-13.jpg",
    focus: "Runs the one-off sends — pricing, launches, webinars.", location: "Bengaluru", timezone: "Asia/Kolkata", joinedAt: MD_NOW - 288 * DAY_MS, lastActiveAt: MD_NOW - 5 * H },
  { id: "mem_4", name: "Tomás Reyes", role: "Head of growth", teamRole: "owner", email: "tomas@northwind.studio", avatar: "../../assets/img/avatar-m-20.jpg",
    focus: "Signs off anything that touches price or the whole list.", location: "Madrid", timezone: "Europe/Madrid", joinedAt: MD_NOW - 1120 * DAY_MS, lastActiveAt: MD_NOW - 26 * H },
  { id: "mem_5", name: "Naomi Adeyemi", role: "Content designer", teamRole: "editor", email: "naomi@northwind.studio", avatar: "../../assets/img/avatar-f-9.jpg",
    focus: "Writes and keeps the template library — every email in Content.", location: "Manchester", timezone: "Europe/London", joinedAt: MD_NOW - 154 * DAY_MS, lastActiveAt: MD_NOW - 40 * 60e3 },
  { id: "mem_6", name: "Felix Braun", role: "Growth analyst", teamRole: "analyst", email: "felix@northwind.studio", avatar: "../../assets/img/avatar-m-15.jpg",
    focus: "Reads what sending did. Can see everything and change nothing.", location: "Berlin", timezone: "Europe/Berlin", joinedAt: MD_NOW - 96 * DAY_MS, lastActiveAt: MD_NOW - 2 * DAY_MS },
];

/* ── Sending domains ──────────────────────────────────────────────────────
   Three checks per domain, each one an enum value from DOMAIN_CHECKS. A domain
   is authenticated when all three pass — derived, never stored. */
const SENDING_DOMAINS = [
  { id: "dom_1", domain: "mail.northwind.studio", purpose: "Everything the product sends", spf: "pass", dkim: "pass", dmarc: "pass", checkedAt: MD_NOW - 3 * H },
  { id: "dom_2", domain: "news.northwind.studio", purpose: "Newsletter only, kept separate on purpose", spf: "pass", dkim: "pass", dmarc: "missing", checkedAt: MD_NOW - 26 * H },
  { id: "dom_3", domain: "northwind.studio", purpose: "The root domain — replies come back here", spf: "pass", dkim: "failing", dmarc: "missing", checkedAt: MD_NOW - 9 * DAY_MS },
];

/* ── Content ───────────────────────────────────────────────────────────────
   [id, name, subject, status, updatedById, days since it was last edited]. */
const CONTENT = [
  ["cnt_welcome", "Welcome and set-up checklist", "Welcome to Marketing Desk — start here", "published", "mem_1", 34],
  ["cnt_import", "Import your contacts", "Bring your list over in two minutes", "published", "mem_1", 34],
  ["cnt_first_campaign", "Your first campaign, step by step", "Send your first campaign today", "published", "mem_3", 12],
  ["cnt_specialist", "Talk to a specialist", "Fifteen minutes with someone who has done this", "published", "mem_3", 61],
  ["cnt_nudge_template", "You haven’t sent yet — here’s a template", "One template, one send, ten minutes", "published", "mem_3", 4],
  ["cnt_three_templates", "Three templates that work", "The three we would start with", "published", "mem_3", 21],
  ["cnt_book_15", "Book fifteen minutes with us", "Want a hand setting this up?", "published", "mem_2", 47],
  ["cnt_welcome_aboard", "Welcome aboard", "You’re in — here’s what happens next", "published", "mem_1", 88],
  ["cnt_account_team", "Meet your account team", "The people looking after your account", "published", "mem_1", 88],
  ["cnt_how_teams", "How other teams use Marketing Desk", "Three ways teams like yours use this", "published", "mem_4", 74],
  ["cnt_exit_survey", "Anything we could have done?", "One question, if you have a minute", "published", "mem_4", 122],
  ["cnt_whats_new", "What’s new since you left", "A few things changed since you left", "published", "mem_4", 96],
  ["cnt_month_free", "A month on us", "A month on us, no strings", "published", "mem_4", 96],
  ["cnt_plan_limit", "You’re close to your plan limit", "You’re at 80% of your plan", "published", "mem_2", 9],
  ["cnt_scale_adds", "What Scale adds", "", "draft", "mem_2", 2],
  ["cnt_product_update", "August product update", "What shipped in August", "published", "mem_1", 12],
  ["cnt_shared_inboxes", "Feature: shared inboxes", "Shared inboxes are here", "published", "mem_3", 5],
  ["cnt_webinar", "Webinar invite: deliverability", "Deliverability, explained — Thursday", "published", "mem_2", 26],
  ["cnt_pricing", "Pricing update notice", "A change to your plan’s pricing", "published", "mem_3", 1],
  ["cnt_trial_ext", "Trial extension offer", "Another fortnight on your trial", "published", "mem_1", 2],
  ["cnt_sept_news", "September newsletter", "September at Northwind", "published", "mem_2", 3],
  ["cnt_case_study", "Case study: Northloop", "How Northloop tripled activation", "draft", "mem_1", 6],
  ["cnt_changelog", "Changelog digest, week 34", "Everything that changed this week", "published", "mem_2", 2],
  ["cnt_renewal", "Renewal reminders, August", "Your plan renews next month", "published", "mem_1", 1],
  ["cnt_referral", "Refer a friend", "Know someone who needs this?", "published", "mem_4", 141],
  ["cnt_old_webinar", "Webinar invite: segmentation (2025)", "Segmentation, explained", "archived", "mem_2", 402],
  ["cnt_old_welcome", "Welcome aboard (old wording)", "Welcome to Marketing Desk", "archived", "mem_1", 288],
].map((row) => ({
  id: row[0], name: row[1], subject: row[2], status: row[3], kind: "email",
  updatedById: row[4], updatedAt: MD_NOW - row[5] * DAY_MS,
}));

/* ── Audiences ─────────────────────────────────────────────────────────────
   `size` and `joined30` are what the engine computed at `evaluatedAt`. `rule`
   is the predicate in words, and `sample` is five of the people it matched. */
const AUDIENCES = [
  {
    id: "aud_trial_active", name: "Trial, opened this week", kind: "segment", channel: "email",
    rule: "Lifecycle is Trial · opened something in the last 7 days",
    /* The same rule in the fields the contact sample carries, so a contact page
       can EVALUATE membership rather than quote a sentence. Where the two differ
       the audience says so in sampleRuleNote. */
    sampleRule: { lifecycle: ["trial"], engagement: "open7" },
    size: 412, joined30: 188, evaluatedAt: MD_NOW - 12 * 60e3,
    sample: [
      { name: "Ada Fenwick", email: "ada@northloop.io", note: "Trial · day 12" },
      { name: "Noor Haddad", email: "noor@levantine.io", note: "Trial · day 3" },
      { name: "Ravi Chandra", email: "ravi@svarna.in", note: "Trial · day 8" },
      { name: "Mira Sandoval", email: "mira@paloverde.mx", note: "Trial · day 6" },
      { name: "Idris Cole", email: "idris@fernhaus.co", note: "Trial · day 9" },
    ],
  },
  {
    id: "aud_trial_quiet", name: "Trial, gone quiet", kind: "segment", channel: "email",
    rule: "Lifecycle is Trial · nothing opened for 14 days",
    sampleRule: { lifecycle: ["trial"], engagement: "quiet14" },
    size: 268, joined30: 96, evaluatedAt: MD_NOW - 12 * 60e3,
    sample: [
      { name: "Petra Lindqvist", email: "petra@sundial.se", note: "Last opened 19 days ago" },
      { name: "Yuki Tanabe", email: "yuki@hanamiworks.jp", note: "Last opened 27 days ago" },
      { name: "Hana Novak", email: "hana@vltavaco.cz", note: "Never opened" },
      { name: "Marcus Oyelaran", email: "marcus@baobabhq.com", note: "Last opened 22 days ago" },
      { name: "Delphine Roy", email: "delphine@atelierroy.fr", note: "Last opened 15 days ago" },
    ],
  },
  {
    id: "aud_new_customers", name: "New customers", kind: "segment", channel: "email",
    rule: "Lifecycle is Customer · started paying in the last 30 days",
    sampleRule: { lifecycle: ["customer"], tenure: "new30" },
    size: 143, joined30: 143, evaluatedAt: MD_NOW - 12 * 60e3,
    sample: [
      { name: "Lucía Ferrer", email: "lucia@camino.es", note: "Pro · 24 days" },
      { name: "Anders Holt", email: "anders@nordvind.no", note: "Pro · 18 days" },
      { name: "Priya Sundaram", email: "priya@indigoline.in", note: "Starter · 12 days" },
      { name: "Grace Mutua", email: "grace@savannahsix.ke", note: "Pro · 27 days" },
      { name: "Aoife Byrne", email: "aoife@rathmines.ie", note: "Pro · 22 days" },
    ],
  },
  {
    id: "aud_pro", name: "Pro and Scale customers", kind: "segment", channel: "email",
    rule: "Lifecycle is Customer · plan is Pro or Scale",
    sampleRule: { lifecycle: ["customer"], plan: ["pro", "scale"] },
    size: 1284, joined30: 61, evaluatedAt: MD_NOW - 12 * 60e3,
    sample: [
      { name: "Fenna de Vries", email: "fenna@tulpstudio.nl", note: "Scale · 20 months" },
      { name: "Odile Marchetti", email: "odile@marchettigroup.it", note: "Pro · 13 months" },
      { name: "Wei Lin", email: "wei@harborandco.sg", note: "Scale · 14 months" },
      { name: "Otto Weiss", email: "otto@weissbau.de", note: "Scale · 16 months" },
      { name: "Talia Mizrahi", email: "talia@yaffo.co.il", note: "Pro · 5 months" },
    ],
  },
  {
    id: "aud_renewal_risk", name: "Renewal risk", kind: "segment", channel: "email",
    rule: "Lifecycle is Customer · renews within 60 days · nothing opened for 30 days",
    sampleRule: { lifecycle: ["customer"], engagement: "quiet14" },
    sampleRuleNote: "The sample carries no renewal date, so membership evaluated on a contact page reads the engagement half of the rule only — the engine's own count is the size beside it.",
    size: 96, joined30: 0, evaluatedAt: MD_NOW - 12 * 60e3,
    sample: [
      { name: "Nadia Bellini", email: "nadia@bellinicasa.it", note: "Renews in 41 days" },
      { name: "Jonas Krause", email: "jonas@krausewerk.de", note: "Renews in 22 days" },
      { name: "Charles Adeyemi", email: "charles@lagosloop.ng", note: "Renews in 55 days" },
      { name: "Renata Alves", email: "renata@alvesestudio.br", note: "Renews in 12 days" },
      { name: "Karl Jensen", email: "karl@jensenbros.dk", note: "Renews in 48 days" },
    ],
  },
  {
    id: "aud_newsletter", name: "Newsletter subscribers", kind: "list", channel: "email",
    rule: "Everyone who opted in to the newsletter, imported and kept by hand",
    size: 4610, joined30: 187, evaluatedAt: MD_NOW - 3 * H,
    sample: [
      { name: "Hugo Beaumont", email: "hugo@beaumontpress.fr", note: "Opted in 5 days ago" },
      { name: "Grace Mutua", email: "grace@savannahsix.ke", note: "Opted in 27 days ago" },
      { name: "Otto Weiss", email: "otto@weissbau.de", note: "Opted in 16 months ago" },
      { name: "Zoë Hartman", email: "zoe@hartmanmedia.com", note: "Opted in 9 days ago" },
      { name: "Leo Fabbri", email: "leo@fabbriapp.it", note: "Opted in 2 days ago" },
    ],
  },
];

/* ── Journeys ──────────────────────────────────────────────────────────────
   Steps close: entered = completed + at + failed + exited, and entered on a
   step equals completed on the one before it. `at` is who is sitting there
   right now, so enrolment is the sum of `at` and nothing else. Every email step
   names its template. */
const JOURNEYS = [
  {
    id: "jny_onboard", name: "Trial onboarding", status: "live", channel: "email",
    trigger: "A trial starts", audienceId: "aud_trial_active", ownerId: "mem_1",
    publishedAt: MD_NOW - 424 * DAY_MS, lastSendAt: MD_NOW - 3 * H,
    steps: [
      { id: "s1", kind: "email", contentId: "cnt_welcome", label: "Welcome and set-up checklist", entered: 4820, completed: 4712, at: 0, failed: 61, exited: 47 },
      { id: "s2", kind: "wait", label: "Wait 2 days", waitHours: 48, entered: 4712, completed: 4586, at: 118, failed: 0, exited: 8 },
      { id: "s3", kind: "email", contentId: "cnt_import", label: "Import your contacts", entered: 4586, completed: 4451, at: 0, failed: 39, exited: 96 },
      { id: "s4", kind: "branch", label: "Sent a campaign yet?", entered: 4451, completed: 4451, at: 0, failed: 0, exited: 0 },
      { id: "s5", kind: "email", contentId: "cnt_first_campaign", label: "Your first campaign, step by step", entered: 4451, completed: 4188, at: 142, failed: 27, exited: 94 },
      { id: "s6", kind: "email", contentId: "cnt_specialist", label: "Talk to a specialist", entered: 4188, completed: 3902, at: 96, failed: 18, exited: 172 },
    ],
  },
  {
    id: "jny_activation", name: "Nudge: no campaign yet", status: "live", channel: "email",
    trigger: "Trial day 3 with nothing sent", audienceId: "aud_trial_quiet", ownerId: "mem_3",
    publishedAt: MD_NOW - 96 * DAY_MS, lastSendAt: MD_NOW - 40 * 60e3,
    steps: [
      { id: "s1", kind: "email", contentId: "cnt_nudge_template", label: "You haven’t sent yet — here’s a template", entered: 1840, completed: 1602, at: 0, failed: 214, exited: 24 },
      { id: "s2", kind: "wait", label: "Wait 3 days", waitHours: 72, entered: 1602, completed: 1508, at: 88, failed: 0, exited: 6 },
      { id: "s3", kind: "email", contentId: "cnt_three_templates", label: "Three templates that work", entered: 1508, completed: 1402, at: 0, failed: 22, exited: 84 },
      { id: "s4", kind: "email", contentId: "cnt_book_15", label: "Book fifteen minutes with us", entered: 1402, completed: 1246, at: 61, failed: 14, exited: 81 },
    ],
  },
  {
    id: "jny_welcome", name: "New customer welcome", status: "live", channel: "email",
    trigger: "A trial converts to a paid plan", audienceId: "aud_new_customers", ownerId: "mem_1",
    publishedAt: MD_NOW - 212 * DAY_MS, lastSendAt: MD_NOW - 11 * DAY_MS,
    steps: [
      { id: "s1", kind: "email", contentId: "cnt_welcome_aboard", label: "Welcome aboard", entered: 962, completed: 948, at: 0, failed: 9, exited: 5 },
      { id: "s2", kind: "wait", label: "Wait 1 day", waitHours: 24, entered: 948, completed: 942, at: 0, failed: 0, exited: 6 },
      { id: "s3", kind: "email", contentId: "cnt_account_team", label: "Meet your account team", entered: 942, completed: 906, at: 0, failed: 7, exited: 29 },
      { id: "s4", kind: "email", contentId: "cnt_how_teams", label: "How other teams use Marketing Desk", entered: 906, completed: 869, at: 0, failed: 6, exited: 31 },
    ],
  },
  {
    id: "jny_winback", name: "Win-back after cancellation", status: "paused", channel: "email",
    trigger: "A subscription is cancelled", audienceId: "aud_renewal_risk", ownerId: "mem_4",
    publishedAt: MD_NOW - 158 * DAY_MS, lastSendAt: MD_NOW - 6 * DAY_MS,
    pausedAt: MD_NOW - 6 * DAY_MS, pausedBy: "Tomás Reyes",
    steps: [
      { id: "s1", kind: "email", contentId: "cnt_exit_survey", label: "Anything we could have done?", entered: 486, completed: 471, at: 0, failed: 8, exited: 7 },
      { id: "s2", kind: "wait", label: "Wait 7 days", waitHours: 168, entered: 471, completed: 402, at: 64, failed: 0, exited: 5 },
      { id: "s3", kind: "email", contentId: "cnt_whats_new", label: "What’s new since you left", entered: 402, completed: 368, at: 0, failed: 5, exited: 29 },
      { id: "s4", kind: "email", contentId: "cnt_month_free", label: "A month on us", entered: 368, completed: 291, at: 41, failed: 4, exited: 32 },
    ],
  },
  {
    id: "jny_upgrade", name: "Upgrade prompt at 80% of plan", status: "draft", channel: "email",
    trigger: "A customer passes 80% of their plan’s sending limit", audienceId: "aud_pro", ownerId: "mem_2",
    publishedAt: null, lastSendAt: null,
    steps: [
      { id: "s1", kind: "email", contentId: "cnt_plan_limit", label: "You’re close to your plan limit", entered: 0, completed: 0, at: 0, failed: 0, exited: 0 },
      { id: "s2", kind: "wait", label: "Wait 4 days", waitHours: 96, entered: 0, completed: 0, at: 0, failed: 0, exited: 0 },
      { id: "s3", kind: "email", contentId: "cnt_scale_adds", label: "What Scale adds", entered: 0, completed: 0, at: 0, failed: 0, exited: 0, ready: false },
    ],
  },
];

/* ── Broadcasts ────────────────────────────────────────────────────────────
   A sent broadcast carries what actually went out. An unsent one carries no
   numbers at all — its recipient count is read from its audience, once, by
   broadcastRecipients() in metrics.js. */
const BROADCASTS = [
  {
    id: "bc_109", name: "Renewal reminders, August", status: "sending", channel: "email",
    audienceId: "aud_new_customers", contentId: "cnt_renewal", domainId: "dom_1",
    ownerId: "mem_1", submittedById: "mem_1", approvedById: "mem_4",
    scheduledAt: MD_NOW - 22 * 60e3, sentAt: MD_NOW - 22 * 60e3,
    sends: 143, delivered: 88, opened: 21, clicked: 4, unsubs: 0,
  },
  {
    id: "bc_108", name: "Changelog digest, week 34", status: "failed", channel: "email",
    audienceId: "aud_trial_quiet", contentId: "cnt_changelog", domainId: "dom_3",
    ownerId: "mem_2", submittedById: "mem_2", approvedById: "mem_1",
    scheduledAt: MD_NOW - 2 * DAY_MS, sentAt: MD_NOW - 2 * DAY_MS,
    sends: 268, delivered: 106, opened: 24, clicked: 3, unsubs: 0,
    failureReason: "The sending domain stopped resolving partway through — 162 messages were never handed off.",
  },
  {
    id: "bc_104", name: "Pricing update notice", status: "in_review", channel: "email",
    audienceId: "aud_pro", contentId: "cnt_pricing", domainId: "dom_1",
    ownerId: "mem_3", submittedById: "mem_3",
    submittedAt: MD_NOW - 5 * H, scheduledAt: MD_NOW + 30 * H,
    reviewNote: "Legal signed off on the wording this morning.",
  },
  {
    id: "bc_105", name: "Trial extension offer", status: "in_review", channel: "email",
    audienceId: "aud_trial_quiet", contentId: "cnt_trial_ext", domainId: "dom_1",
    ownerId: "mem_1", submittedById: "mem_1",
    submittedAt: MD_NOW - 26 * H, scheduledAt: MD_NOW + 4 * DAY_MS,
  },
  {
    id: "bc_106", name: "September newsletter", status: "scheduled", channel: "email",
    audienceId: "aud_newsletter", contentId: "cnt_sept_news", domainId: "dom_2",
    ownerId: "mem_2", submittedById: "mem_2", approvedById: "mem_4",
    scheduledAt: MD_NOW + 6 * DAY_MS,
  },
  {
    id: "bc_107", name: "Case study: Northloop", status: "draft", channel: "email",
    audienceId: "aud_pro", contentId: "cnt_case_study", domainId: "dom_1",
    ownerId: "mem_1", scheduledAt: null,
  },
  {
    id: "bc_102", name: "Feature: shared inboxes", status: "sent", channel: "email",
    audienceId: "aud_pro", contentId: "cnt_shared_inboxes", domainId: "dom_1",
    ownerId: "mem_3", submittedById: "mem_3", approvedById: "mem_1",
    scheduledAt: MD_NOW - 5 * DAY_MS, sentAt: MD_NOW - 5 * DAY_MS,
    sends: 1276, delivered: 1251, opened: 514, clicked: 148, unsubs: 4,
  },
  {
    id: "bc_101", name: "August product update", status: "sent", channel: "email",
    audienceId: "aud_newsletter", contentId: "cnt_product_update", domainId: "dom_2",
    ownerId: "mem_1", submittedById: "mem_1", approvedById: "mem_4",
    scheduledAt: MD_NOW - 12 * DAY_MS, sentAt: MD_NOW - 12 * DAY_MS,
    sends: 4592, delivered: 4488, opened: 1693, clicked: 402, unsubs: 17,
  },
  {
    id: "bc_103", name: "Webinar invite: deliverability", status: "sent", channel: "email",
    audienceId: "aud_newsletter", contentId: "cnt_webinar", domainId: "dom_2",
    ownerId: "mem_2", submittedById: "mem_2", approvedById: "mem_1",
    scheduledAt: MD_NOW - 26 * DAY_MS, sentAt: MD_NOW - 26 * DAY_MS,
    sends: 4510, delivered: 4402, opened: 1408, clicked: 291, unsubs: 22,
  },
];

/* ── Journey delivery, by day ──────────────────────────────────────────────
   Thirty rows, oldest first: [delivered, opened, clicked, unsubscribed]. Row i
   is (29 − i) days ago. Broadcast sends are deliberately not in here. */
const JOURNEY_DELIVERY = [
  [214, 71, 16, 1], [228, 78, 19, 0], [152, 47, 9, 0], [118, 34, 6, 0], [241, 84, 21, 1],
  [233, 79, 18, 0], [219, 73, 17, 1], [247, 88, 23, 1], [236, 81, 20, 0], [161, 51, 11, 0],
  [127, 38, 7, 0], [252, 91, 24, 1], [259, 94, 25, 0], [248, 87, 22, 1], [236, 82, 21, 0],
  [263, 97, 26, 2], [168, 54, 12, 0], [133, 41, 8, 0], [271, 101, 28, 1], [279, 106, 29, 1],
  [264, 96, 25, 0], [256, 92, 24, 1], [282, 108, 30, 1], [174, 56, 12, 0], [139, 43, 9, 0],
  [287, 111, 31, 1], [274, 103, 28, 1], [296, 116, 33, 2], [288, 111, 30, 1], [273, 104, 28, 1],
];

/* ── The contact sample ────────────────────────────────────────────────────
   Forty real contacts: the most recent additions to the workspace. This is a
   SAMPLE, not the contact list — the audience builder evaluates its rules over
   these forty and extrapolates to the reachable total, which is what a segment
   estimator does before the engine materialises the real count. Everybody named
   in an audience's `sample` above is in here with attributes that agree with
   what that sample says about them.
   [name, email, lifecycle, plan, days since they last opened (null = never),
    days since they joined] */
const SAMPLE_CONTACTS = [
  ["Ada Fenwick", "ada@northloop.io", "trial", "starter", 1, 12],
  ["Noor Haddad", "noor@levantine.io", "trial", "starter", 0, 3],
  ["Ravi Chandra", "ravi@svarna.in", "trial", "starter", 4, 8],
  ["Mira Sandoval", "mira@paloverde.mx", "trial", "free", 2, 6],
  ["Idris Cole", "idris@fernhaus.co", "trial", "starter", 3, 9],
  ["Tomas Brandt", "tomas@kiln.studio", "trial", "starter", 3, 18],
  ["Petra Lindqvist", "petra@sundial.se", "trial", "starter", 19, 26],
  ["Yuki Tanabe", "yuki@hanamiworks.jp", "trial", "starter", 27, 31],
  ["Hana Novak", "hana@vltavaco.cz", "trial", "free", null, 29],
  ["Marcus Oyelaran", "marcus@baobabhq.com", "trial", "free", 22, 24],
  ["Delphine Roy", "delphine@atelierroy.fr", "trial", "starter", 15, 16],
  ["Luc\u00eda Ferrer", "lucia@camino.es", "customer", "pro", 1, 24],
  ["Anders Holt", "anders@nordvind.no", "customer", "pro", 3, 18],
  ["Priya Sundaram", "priya@indigoline.in", "customer", "starter", 2, 12],
  ["Grace Mutua", "grace@savannahsix.ke", "customer", "pro", 6, 27],
  ["Aoife Byrne", "aoife@rathmines.ie", "customer", "pro", 4, 22],
  ["Fenna de Vries", "fenna@tulpstudio.nl", "customer", "scale", 1, 611],
  ["Odile Marchetti", "odile@marchettigroup.it", "customer", "pro", 1, 395],
  ["Wei Lin", "wei@harborandco.sg", "customer", "scale", 1, 425],
  ["Otto Weiss", "otto@weissbau.de", "customer", "scale", 2, 480],
  ["Talia Mizrahi", "talia@yaffo.co.il", "customer", "pro", 3, 150],
  ["Ingrid Solberg", "ingrid@fjordsix.no", "customer", "pro", 5, 198],
  ["Kwame Boateng", "kwame@accraworks.gh", "customer", "starter", 4, 210],
  ["Rafael Duarte", "rafael@copacabana.br", "customer", "starter", 8, 154],
  ["Bruno S\u00e1", "bruno@ribeirasa.pt", "customer", "starter", 11, 96],
  ["Nadia Bellini", "nadia@bellinicasa.it", "customer", "pro", 38, 341],
  ["Jonas Krause", "jonas@krausewerk.de", "customer", "starter", 61, 289],
  ["Charles Adeyemi", "charles@lagosloop.ng", "customer", "starter", 44, 265],
  ["Renata Alves", "renata@alvesestudio.br", "customer", "starter", 71, 520],
  ["Karl Jensen", "karl@jensenbros.dk", "customer", "pro", 88, 601],
  ["Hugo Beaumont", "hugo@beaumontpress.fr", "lead", "free", 2, 5],
  ["Zo\u00eb Hartman", "zoe@hartmanmedia.com", "lead", "free", 7, 9],
  ["Leo Fabbri", "leo@fabbriapp.it", "lead", "free", 1, 2],
  ["Amara Nwosu", "amara@nwosucreative.ng", "lead", "free", 12, 14],
  ["Signe Dahl", "signe@dahlogsonn.dk", "lead", "free", 18, 20],
  ["Milo S\u00f8rensen", "milo@kobenhavnco.dk", "lead", "free", null, 33],
  ["Ceyda Aksoy", "ceyda@bosphorus.tr", "lead", "free", 3, 7],
  ["Dev Patel", "dev@patelanddaughters.uk", "lead", "free", 29, 41],
  ["Sofia Ruiz", "sofia@ruizmadrid.es", "churned", "starter", 71, 388],
  ["Ethan Cole", "ethan@coleandsons.com", "churned", "pro", 88, 455],
].map((row, i) => ({
  id: "con_" + (i + 1),
  name: row[0], email: row[1], lifecycle: row[2], plan: row[3],
  lastOpenAt: row[4] === null ? null : MD_NOW - row[4] * DAY_MS,
  signupAt: MD_NOW - row[5] * DAY_MS,
}));

/* Authored against the records above: each one names a record and agrees with
   that record's numbers and clock at this timestamp. */
const NOTIFICATIONS = [
  {
    id: "n1", title: "Pricing update notice needs approval",
    body: "Priya Raman submitted it 5 hours ago and it sends in 30 hours.",
    at: MD_NOW - 5 * H, route: "/broadcasts/bc_104",
  },
  {
    id: "n2", title: "Nudge: no campaign yet is bouncing",
    body: "214 of the 1,840 who entered “You haven’t sent yet” failed to deliver — 11.6%.",
    at: MD_NOW - 9 * H, route: "/journeys/jny_activation",
  },
  {
    id: "n3", title: "DKIM stopped resolving on northwind.studio",
    body: "The root domain has failed its DKIM check since the changelog digest went out.",
    at: MD_NOW - 31 * H, route: "/deliverability",
  },
];


/* ── The social layer ─────────────────────────────────────────────────────
   Notes on records, in the order they were written. A note is NOT a status
   field: it has no lifecycle, so enums.js carries no comment status. Whether a
   mention is still OWED is derived — the person mentioned has written nothing on
   that record since (COMMENT_FLAGS.isAwaitingReply in enums.js), which is what
   the inbox counts, what the nav badge counts and what the note itself says.

   Notes live only on records that HAVE a screen — a journey, a broadcast, an
   audience — so a mention in the inbox always opens onto the thread it came
   from. Templates and domains carry none, because a note nobody can reach is a
   note nobody answers.

   A name is never written in a body: a mention is a member id (@mem_4),
   resolved through MEMBERS at render. Every note is dated after the thing it
   names and agrees with that record's status and numbers at MD_NOW. */
const C = (id, type, recordId, authorId, hoursAgo, body, extra) => Object.assign({
  id: "cmt_" + id, record: { type: type, id: recordId }, authorId,
  at: MD_NOW - hoursAgo * H, body,
  mentions: (body.match(/@mem_\d+/g) || []).map((m) => m.slice(1)),
  reactions: {},
}, extra || {});

const COMMENTS = [
  /* bc_104 — Pricing update notice, in review, sends in 30 hours. */
  C(1, "broadcast", "bc_104", "mem_3", 5, "Submitted. Copy is signed off by legal, and it goes to Pro and Scale only — 1,284 people. @mem_1 can you approve before tomorrow lunchtime, it sends in 30 hours.", { reactions: { watching: ["mem_4"] } }),
  C(2, "broadcast", "bc_104", "mem_4", 4, "Price change to the whole paying base is mine to sign off, not @mem_1's. Leave it with me — but @mem_5, the second paragraph still says “from October” and the change lands in November.", { reactions: { agree: ["mem_3"] } }),
  C(3, "broadcast", "bc_104", "mem_5", 3, "Fixed in the template — cnt_pricing, edited just now. It reads “from 1 November” in both places.", { reactions: { agree: ["mem_4", "mem_3"] } }),
  /* bc_108 — Changelog digest, failed two days ago on the root domain. */
  C(4, "broadcast", "bc_108", "mem_2", 46, "This one failed against northwind.studio, not mail. DKIM has been failing on the root domain for nine days and nobody noticed because nothing else sends from it.", { reactions: { agree: ["mem_1"] } }),
  C(5, "broadcast", "bc_108", "mem_1", 44, "@mem_2 do we retry the 1,120 it did not reach, or fold them into next week's digest? I would fold them in — two digests in three days reads like a mistake.", { reactions: { agree: ["mem_2"] } }),
  C(6, "broadcast", "bc_108", "mem_2", 43, "Fold them in. I have moved the digest to mail.northwind.studio so it cannot happen again, and left the root domain in Deliverability for @mem_6 to watch.", {}),
  /* jny_activation — the live journey that is over the failure threshold. */
  C(7, "journey", "jny_activation", "mem_6", 30, "Step 2 of this journey is failing 11.6% of what enters it — that is the worst rate anywhere in the workspace, and it is all one audience. @mem_3 worth a look before we scale the nudge.", { reactions: { watching: ["mem_1", "mem_3"] } }),
  C(8, "journey", "jny_activation", "mem_3", 28, "Looked. The bounces are trial signups with typo'd domains — they never confirmed. Suppression will not catch them because they were never delivered to.", { reactions: { agree: ["mem_6"] } }),
  C(9, "journey", "jny_activation", "mem_1", 9, "Then the fix is at signup, not here. @mem_4 I would like to keep the journey live while product adds the confirmation step — it converts, it just wastes sends.", {}),
  /* jny_winback — paused by Tomás six days ago. */
  C(10, "journey", "jny_winback", "mem_4", 140, "Paused. The offer in step 3 is 30% for three months and we have not agreed that discount for this quarter. Nothing sends until it comes out.", { reactions: { agree: ["mem_1", "mem_3"] } }),
  C(11, "journey", "jny_winback", "mem_1", 120, "Understood. @mem_5 can you rewrite step 3 without a discount — same length, same offer of a call?", {}),
  /* aud_renewal_risk — the audience nothing currently sends to. */
  C(12, "audience", "aud_renewal_risk", "mem_1", 74, "96 people, and the only journey pointed at it is paused. Effectively nobody is talking to the customers most likely to leave.", { reactions: { agree: ["mem_4", "mem_6"] } }),
  C(13, "audience", "aud_renewal_risk", "mem_6", 50, "Worth saying the rule is strict: renews within 60 days AND nothing opened for 30. Loosen the open window to 14 days and it is nearer 300. @mem_1 want me to build that as a second segment?", {}),
  /* aud_newsletter — the hand-kept list. */
  C(14, "audience", "aud_newsletter", "mem_2", 200, "Imported 187 new subscribers this month from the conference sign-up sheet. All opted in on paper; I have the scans.", { reactions: { agree: ["mem_1"] } }),
  /* bc_105 — Trial extension offer, in review, Dana submitted it herself. */
  C(15, "broadcast", "bc_105", "mem_1", 26, "Submitted — I cannot approve my own, so this needs @mem_4. It goes to the quiet trials, 268 people, four days out.", { reactions: { watching: ["mem_4"] } }),
  C(16, "broadcast", "bc_106", "mem_5", 20, "September newsletter is written and scheduled. @mem_2 it is going out on news.northwind.studio, which still has no DMARC record — is that a problem for a 4,610-person send?", {}),
];

/* ── The activity feed ────────────────────────────────────────────────────
   What people did, one entry per act, newest first. Every entry names a record
   that exists and a person who is in MEMBERS, and agrees with that record's
   current state: an approval entry exists only where the record carries an
   approver, a pause entry only where the journey is paused. The feed is read,
   never written to by a screen — the toasts are what a commit raises. */
const V = (id, actorId, hoursAgo, verb, type, recordId, note) => ({
  id: "act_" + id, actorId, at: MD_NOW - hoursAgo * H, verb,
  record: type ? { type: type, id: recordId } : null, note: note || null,
});

const ACTIVITY = [
  V(1, "mem_5", 3, "edited", "content", "cnt_pricing", "Second paragraph now reads “from 1 November”"),
  V(2, "mem_3", 5, "submitted", "broadcast", "bc_104", "For review before it sends"),
  V(3, "mem_1", 9, "commented", "journey", "jny_activation", null),
  V(4, "mem_6", 30, "commented", "journey", "jny_activation", "Flagged the 11.6% failure rate"),
  V(5, "mem_1", 26, "submitted", "broadcast", "bc_105", "Needs somebody else to approve"),
  V(6, "mem_2", 43, "moved", "broadcast", "bc_108", "Now sends from mail.northwind.studio"),
  V(7, "mem_5", 20, "scheduled", "broadcast", "bc_106", "6 days from now, 4,610 subscribers"),
  V(8, "mem_4", 47, "approved", "broadcast", "bc_106", null),
  V(9, "mem_2", 50, "created", "broadcast", "bc_106", null),
  V(10, "mem_1", 74, "commented", "audience", "aud_renewal_risk", null),
  V(11, "mem_4", 140, "paused", "journey", "jny_winback", "Discount in step 3 is not agreed for this quarter"),
  V(12, "mem_1", 120, "commented", "journey", "jny_winback", null),
  V(13, "mem_4", 122, "approved", "broadcast", "bc_109", null),
  V(14, "mem_1", 123, "submitted", "broadcast", "bc_109", null),
  V(15, "mem_2", 200, "grew", "audience", "aud_newsletter", "187 subscribers imported from the conference sheet"),
  V(16, "mem_1", 264, "published", "journey", "jny_welcome", null),
  V(17, "mem_2", 300, "created", "journey", "jny_upgrade", "Still a draft"),
  V(18, "mem_6", 48, "exported", "broadcast", "bc_102", "Send report, CSV"),
];

/* ── What the settings screens configure ──────────────────────────────────
   Connected channels, keys, plan and personal preferences are records like any
   other: a screen reads them, a staged save writes them back, and the figures
   that quote them read the same array. Nothing here is a second copy of
   something already in MD_WORKSPACE. */
const CONNECTED_CHANNELS = [
  { id: "chn_ses", name: "Amazon SES", kind: "Email delivery", state: "connected", detail: "eu-west-1 · 240,000 a day", connectedAt: MD_NOW - 903 * DAY_MS, byId: "mem_4" },
  { id: "chn_segment", name: "Segment", kind: "Contact source", state: "connected", detail: "Writes lifecycle and plan onto every contact", connectedAt: MD_NOW - 611 * DAY_MS, byId: "mem_2" },
  { id: "chn_stripe", name: "Stripe", kind: "Contact source", state: "connected", detail: "Plan changes and cancellations trigger two journeys", connectedAt: MD_NOW - 480 * DAY_MS, byId: "mem_4" },
  { id: "chn_slack", name: "Slack", kind: "Notifications", state: "connected", detail: "#marketing — approvals and failed sends", connectedAt: MD_NOW - 154 * DAY_MS, byId: "mem_1" },
  { id: "chn_sheets", name: "Google Sheets", kind: "Export", state: "needs_reauth", detail: "The token expired 4 days ago; exports have stopped", connectedAt: MD_NOW - 288 * DAY_MS, byId: "mem_2" },
  { id: "chn_hubspot", name: "HubSpot", kind: "Contact source", state: "available", detail: "Not connected — contacts would arrive with lifecycle already set", connectedAt: null, byId: null },
];

const API_KEYS = [
  { id: "key_prod", label: "Production — website signup", prefix: "nw_live_8f2c", createdAt: MD_NOW - 611 * DAY_MS, byId: "mem_2", lastUsedAt: MD_NOW - 8 * 60e3, scope: "write" },
  { id: "key_report", label: "Looker reporting", prefix: "nw_read_41ab", createdAt: MD_NOW - 288 * DAY_MS, byId: "mem_6", lastUsedAt: MD_NOW - 4 * H, scope: "read" },
  { id: "key_old", label: "Old Zapier zap", prefix: "nw_live_0d77", createdAt: MD_NOW - 903 * DAY_MS, byId: "mem_4", lastUsedAt: MD_NOW - 214 * DAY_MS, scope: "write" },
];

const WEBHOOKS = [
  { id: "hook_1", url: "https://api.northwind.studio/hooks/marketing", events: ["send.failed", "contact.unsubscribed"], state: "healthy", lastAt: MD_NOW - 22 * 60e3 },
  { id: "hook_2", url: "https://hooks.slack.com/services/T0/B0/xxxx", events: ["broadcast.submitted"], state: "failing", lastAt: MD_NOW - 5 * H, detail: "403 from the endpoint on the last 6 attempts" },
];

/* The plan quotes one number for contacts and one for sends; the contacts figure
   is MD_WORKSPACE.contacts, so the usage meter cannot disagree with the workspace. */
const BILLING = {
  plan: "Scale", priceMinor: 74900, currency: "USD", interval: "month",
  renewsAt: MD_NOW + 19 * DAY_MS, seatsIncluded: 8,
  contactLimit: 10000, sendLimit: 250000, sendsThisPeriod: 168420,
  periodStartAt: MD_NOW - 11 * DAY_MS,
  invoices: [
    { id: "inv_2026_08", at: MD_NOW - 11 * DAY_MS, amountMinor: 74900, state: "paid" },
    { id: "inv_2026_07", at: MD_NOW - 42 * DAY_MS, amountMinor: 74900, state: "paid" },
    { id: "inv_2026_06", at: MD_NOW - 72 * DAY_MS, amountMinor: 74900, state: "paid" },
  ],
  card: { brand: "Visa", last4: "4242", expiry: "07/29" },
};

/* Per-person, not per-workspace: what Dana is told about, and how. */
const NOTIFICATION_PREFS = {
  memberId: "mem_1",
  rows: [
    { id: "approval_needed", label: "A send needs my approval", email: true, slack: true, inApp: true },
    { id: "send_failed", label: "A send fails or partially fails", email: true, slack: true, inApp: true },
    { id: "mention", label: "Somebody mentions me on a record", email: false, slack: true, inApp: true },
    { id: "journey_threshold", label: "A journey step passes the failure threshold", email: false, slack: true, inApp: true },
    { id: "domain_check", label: "A domain stops authenticating", email: true, slack: false, inApp: true },
    { id: "weekly_digest", label: "Weekly summary of what sent", email: true, slack: false, inApp: false },
  ],
  quietHours: { from: "19:00", to: "08:00" },
};

Object.assign(window, {
  MD_NOW, MD_WORKSPACE, MD_SIGNED_IN, MD_RESET_LINK, MEMBERS, SENDING_DOMAINS, CONTENT,
  AUDIENCES, JOURNEYS, BROADCASTS, JOURNEY_DELIVERY, SAMPLE_CONTACTS, NOTIFICATIONS,
  COMMENTS, ACTIVITY, CONNECTED_CHANNELS, API_KEYS, WEBHOOKS, BILLING, NOTIFICATION_PREFS,
});
