// Partner — the record shop's own page at /partner.
//
// THE TOKEN LIVES IN THE URL FRAGMENT (/partner#k=…), never the query string. A
// fragment is never sent to a server, so the credential cannot reach Vercel's
// access logs, Supabase's logs, or a Referer header on any outbound click. It is
// also still bookmarkable, which matters: this link IS the login, and a
// shopkeeper will bookmark it.
//
// THREE STATES, ONE ACTION EACH. The same URL does three different jobs and
// conflating them is how it ends up as a wall of zeros beside a form:
//
//   no-order  → "Order your kit". No metrics at all — there is nothing to
//               measure, so nothing is measured.
//   awaiting  → "Here's where your kit is". The tracker carries the page through
//               the fortnight that would otherwise look like a dead programme.
//   live      → the dashboard proper, and the only state where a reorder makes
//               sense.
//
// Server state is the authority on which one. `state` comes from
// partner_dashboard() and is never derived here — a page that guesses its own
// state disagrees with the database the first time an edge case appears.
//
// Rendered by App.jsx for route "partner". Unlinked from the nav: the only way in
// is the emailed link.
(function () {
  const { useState, useEffect, useCallback, useRef } = React;

  const API = "/api/partner";

  // ---- money -------------------------------------------------------------
  const gbp = (pence) => "£" + (pence / 100).toFixed(2);

  const fmtDate = (iso) => {
    if (!iso) return "";
    const d = new Date(iso);
    if (isNaN(d)) return "";
    return d.toLocaleDateString("en-GB", { weekday: "short", day: "numeric", month: "short" });
  };

  const pct = (n, d) => (!d ? null : Math.round((n / d) * 100));

  // ---- the token ---------------------------------------------------------
  // Read once and held in memory for the life of the page. Two forms, and the
  // difference matters:
  //
  //   #k=…  the resting form. A fragment is never sent to a server, so a
  //         bookmark made from it leaks the credential to nothing — not our
  //         access log, not a Referer header on an outbound click.
  //
  //   ?k=…  the ARRIVING form, used by the welcome email only. Resend rewrites
  //         every <a href> for click tracking, and a redirect cannot be trusted
  //         to carry a fragment through — if it drops it, the shop's only way in
  //         simply does not work. A query parameter survives any redirect, so the
  //         email uses one and the page promotes it to a fragment on arrival.
  //
  // The promotion is the whole point: the token sits in a URL a server can see
  // for exactly one request, and never again.
  const TOKEN_RE = /^[A-Za-z0-9]{24,}$/;

  function readToken() {
    try {
      const h = (location.hash || "").replace(/^#/, "");
      const m = /(?:^|&)k=([A-Za-z0-9]+)/.exec(h);
      if (m && TOKEN_RE.test(m[1])) return m[1];

      const q = new URLSearchParams(location.search);
      const qk = q.get("k");
      if (qk && TOKEN_RE.test(qk)) {
        q.delete("k");
        const rest = q.toString();
        try {
          history.replaceState(null, "",
            location.pathname + (rest ? "?" + rest : "") + "#k=" + qk);
        } catch (e) { /* replaceState unavailable; the token still works */ }
        return qk;
      }
      return null;
    } catch (e) { return null; }
  }

  async function post(body, secret) {
    const r = await fetch(API, {
      method: "POST",
      headers: { "Content-Type": "application/json", ...(secret ? { "x-webhook-secret": secret } : {}) },
      body: JSON.stringify(body),
    });
    let json = null;
    try { json = await r.json(); } catch (e) { /* non-JSON error page */ }
    if (!r.ok) {
      const err = new Error((json && json.message) || (json && json.error) || "request_failed");
      err.code = json && json.error;
      err.status = r.status;
      throw err;
    }
    return json;
  }

  // =======================================================================
  // Small presentational pieces
  // =======================================================================

  function Eyebrow({ children }) {
    return <p className="pt-eyebrow">{children}</p>;
  }

  // The funnel. Rendered as an indented tree rather than four separate tiles
  // because each tier is a strict subset of the one above it — the indentation is
  // the claim that the numbers add up, and a row of equal tiles hides it.
  function Funnel({ f, money }) {
    const rows = [
      {
        depth: 0, label: "Scans", value: f.scans,
        note: f.scans
          ? [f.scans_card ? `${f.scans_card} from the card` : null,
             f.scans_flyer ? `${f.scans_flyer} from flyers` : null].filter(Boolean).join(", ")
          : "nobody has scanned your code yet",
      },
      {
        depth: 1, label: "Gave us an email", value: f.emails,
        note: f.scans ? (pct(f.emails, f.scans) != null ? `${pct(f.emails, f.scans)}% of scans` : "") : "",
      },
      {
        depth: 2, label: "Became collectors", value: f.collectors,
        note: f.collectors
          ? `${f.activated} ${f.activated === 1 ? "has" : "have"} added records — yours for good`
          : "",
      },
      {
        depth: 3, label: "Are paying", value: f.paying,
        note: "the subscription launches in Q4",
      },
    ];

    return (
      <div className="pt-funnel">
        {rows.map((r, i) => (
          <div key={r.label} className={"pt-frow pt-d" + r.depth}>
            <span className="pt-fname">
              {r.depth > 0 && <span className="pt-tick" aria-hidden="true">└</span>}
              {r.label}
            </span>
            <span className="pt-fcount">{r.value}</span>
            <span className="pt-fnote">{r.note}</span>
          </div>
        ))}
        <div className="pt-money">
          <div>
            <span className="pt-mlabel">Earned so far</span>
            <span className="pt-mbig">{gbp(money.earned_pence)}</span>
            <span className="pt-mnote">The subscription isn't live yet. Nothing to pay you, and nothing wrong.</span>
          </div>
          <div>
            <span className="pt-mlabel">At launch — an illustration</span>
            <span className="pt-mbig pt-mproj">{gbp(money.projected_pence)}<span className="pt-per">/mo</span></span>
            <span className="pt-mnote">
              {money.projected_pence
                ? `If all ${money.projected_pence / money.per_subscriber_pence} of your collectors subscribe, at ${gbp(money.per_subscriber_pence)} each.`
                : `${gbp(money.per_subscriber_pence)} a month for every collector who subscribes, for as long as they stay.`}
            </span>
            {money.breakdown && (
              <span className="pt-mnote">
                That's half of what actually reaches us: {gbp(money.breakdown.gross_pence)} less{" "}
                {money.breakdown.vat_pence ? `VAT and ` : ""}card fees leaves{" "}
                {gbp(money.breakdown.net_pence)}, and you take half.
              </span>
            )}
          </div>
        </div>
      </div>
    );
  }

  function MonthChart({ months }) {
    if (!months || !months.length) return null;
    const max = Math.max(...months.map(m => m.collectors), 1);
    return (
      <div className="pt-chart">
        <Eyebrow>Collectors by month</Eyebrow>
        <div className="pt-bars">
          {months.map(m => (
            <div className="pt-bar" key={m.month}>
              <div className="pt-barfill" style={{ height: Math.round((m.collectors / max) * 100) + "%" }}
                   title={`${m.collectors} in ${m.month}`} />
              <span className="pt-barlab">{m.month.slice(5)}</span>
            </div>
          ))}
        </div>
      </div>
    );
  }

  // The tracker. Four steps: three from the order's status, and a fourth from the
  // scan data — which is what makes it complete itself rather than sit there.
  function Tracker({ order, scans }) {
    if (!order) return null;
    const st = order.status;
    const posted = st === "posted";
    const printing = st === "printing";

    const steps = [
      {
        key: "placed", label: "Order received", done: true,
        body: `${order.qty.flyer_a} flyers of each design${order.qty.card ? ` and ${order.qty.card === 1 ? "a counter card" : order.qty.card + " counter cards"}` : ""}, to ${order.ship.town} ${order.ship.postcode}.`,
        when: fmtDate(order.placed_at),
      },
      {
        key: "printing", label: "At the printer", done: posted, now: printing,
        body: "Your QR codes are being made up and the flyers printed. Two to three working days.",
        when: posted ? fmtDate(order.printing_at) : printing ? "In progress" : "Next",
      },
      {
        key: "posted", label: "In the post", done: posted, now: posted && !scans,
        body: order.tracking
          ? `Tracked delivery — ${order.tracking}.`
          : "Tracked delivery. We'll email you when it goes.",
        when: posted ? fmtDate(order.posted_at) : "Soon",
      },
      {
        key: "scan", label: "Your first scan", done: scans > 0,
        body: scans > 0
          ? "It's working — the numbers below are live."
          : "Usually within a week or two of the card going out on the counter.",
        when: scans > 0 ? "Done" : "Waiting",
      },
    ];

    return (
      <div className="pt-panel">
        <Eyebrow>Your kit</Eyebrow>
        <ol className="pt-steps">
          {steps.map(s => (
            <li key={s.key} className={s.done ? "done" : s.now ? "now" : ""}>
              <div>
                <div className="pt-slabel">{s.label}</div>
                <div className="pt-sbody">{s.body}</div>
                <div className="pt-swhen">{s.when}</div>
              </div>
            </li>
          ))}
        </ol>
      </div>
    );
  }

  // The order form. Also the address-change form — same fields, different verb,
  // so there is one place where an address is validated and rendered.
  function AddressForm({ initial, busy, error, onSubmit, submitLabel, showNote }) {
    const [f, setF] = useState({
      name: (initial && initial.name) || "",
      line1: (initial && initial.line1) || "",
      line2: (initial && initial.line2) || "",
      town: (initial && initial.town) || "",
      postcode: (initial && initial.postcode) || "",
      phone: (initial && initial.phone) || "",
    });
    const [note, setNote] = useState("");
    const set = k => e => setF(prev => ({ ...prev, [k]: e.target.value }));

    const field = (k, label, opts = {}) => (
      <label className={"pt-field" + (opts.half ? " pt-half" : "")}>
        <span>{label}{opts.optional && <em> — optional</em>}</span>
        <input value={f[k]} onChange={set(k)} autoComplete={opts.autoComplete || "off"}
               inputMode={opts.inputMode} spellCheck="false" />
      </label>
    );

    return (
      <form className="pt-form" onSubmit={e => { e.preventDefault(); onSubmit(f, note); }}>
        <div className="pt-fields">
          {field("name", "Who's it for", { autoComplete: "name" })}
          {field("line1", "Address", { autoComplete: "address-line1" })}
          {field("line2", "Address, second line", { optional: true, autoComplete: "address-line2" })}
          {field("town", "Town", { half: true, autoComplete: "address-level2" })}
          {field("postcode", "Postcode", { half: true, autoComplete: "postal-code" })}
          {field("phone", "Phone", { optional: true, autoComplete: "tel", inputMode: "tel" })}
          <p className="pt-hint">The phone number goes to the courier and nowhere else. We never use it to call you.</p>
          {showNote && (
            <label className="pt-field">
              <span>Anything else?<em> — optional</em></span>
              <input value={note} onChange={e => setNote(e.target.value)}
                     placeholder="Two tills, so a second card would help" />
            </label>
          )}
        </div>
        {error && <p className="pt-err">{error}</p>}
        <button className="pt-cta" type="submit" disabled={busy}>
          {busy ? "One moment…" : submitLabel}
        </button>
      </form>
    );
  }

  function KitSummary({ qty }) {
    const rows = [
      qty.card ? [`${qty.card} × counter card`, "£0.00"] : null,
      qty.flyer_a ? [`${qty.flyer_a} × bag flyer A — typographic`, "£0.00"] : null,
      qty.flyer_b ? [`${qty.flyer_b} × bag flyer B — poster`, "£0.00"] : null,
      ["Postage, tracked", "£0.00"],
    ].filter(Boolean);
    return (
      <div className="pt-summary">
        <Eyebrow>Your starter kit</Eyebrow>
        {rows.map(([l, r]) => (
          <div className="pt-srow" key={l}><span>{l}</span><span>{r}</span></div>
        ))}
        <div className="pt-srow pt-total"><span>Total to pay</span><span>£0.00</span></div>
        <p className="pt-hint">
          We cover the print and the postage. There's no invoice, no card needed, and nothing to pay later.
        </p>
      </div>
    );
  }

  // The explainer. Its whole job is to stop a shop concluding the programme is
  // dead during the three weeks when, legitimately, nothing happens.
  function Explainer({ compact, perSubscriber }) {
    const [open, setOpen] = useState(!compact);
    const rate = gbp(perSubscriber || 0);
    const points = [
      ["Card by the till, flyers in bags.", "That's the whole job. The card does most of the work because it's there all day."],
      ["Nothing will happen on day one.", "Scans come first, and most people who scan don't sign up the same day — they look, then come back."],
      ["The numbers only move one way.", "Nobody gets taken off your count. A collector who came through your code stays yours."],
      ["£0 is the expected reading until Q4.", "The subscription isn't live yet. This page will show nothing earned until it is, and that isn't a fault."],
      [`Then it's ${rate} a month, per subscriber.`,
       `Half of what reaches us after VAT and card fees, for as long as they stay. Ten of your regulars is ${gbp((perSubscriber || 0) * 10)} a month, every month.`],
    ];
    return (
      <div className="pt-explain">
        {compact && (
          <button className="pt-disclose" onClick={() => setOpen(o => !o)} aria-expanded={open}>
            How this works {open ? "−" : "+"}
          </button>
        )}
        {open && (
          <>
            {!compact && <Eyebrow>What happens now</Eyebrow>}
            <dl className="pt-points">
              {points.map(([t, d]) => (
                <div key={t}><dt>{t}</dt><dd>{d}</dd></div>
              ))}
            </dl>
            <Eyebrow>Being straight with you about the numbers</Eyebrow>
            <ul className="pt-limits">
              <li>Scans counts <strong>devices</strong>, not people — a cleared phone counts twice, a shared one counts once.</li>
              <li>Someone who scans in the shop and finishes signing up on a different device may not reach you. We've reduced this, not eliminated it.</li>
              <li>If a customer scanned another shop's card first, they stay with that shop. The same rule protects you.</li>
              <li>We've no idea how many flyers you handed out. A low number may just mean the box is still under the counter.</li>
            </ul>
          </>
        )}
      </div>
    );
  }

  function Reorder({ reorder, onOrder, busy, error }) {
    const [open, setOpen] = useState(false);
    const [flyers, setFlyers] = useState(250);
    const [cards, setCards] = useState(1);

    if (!reorder) return null;

    if (!reorder.allowed) {
      if (reorder.reason === "order_open") {
        return <p className="pt-quiet">There's an order on its way — once it lands you'll be able to order more here.</p>;
      }
      if (reorder.reason === "too_soon") {
        return (
          <p className="pt-quiet">
            Your last kit was posted on {fmtDate(reorder.last_posted_at)}. If you've already run out,{" "}
            <a href="mailto:luca@thelead-in.com?subject=More%20flyers%20please">tell Luca</a> and we'll get more to you.
          </p>
        );
      }
      return null;
    }

    if (!open) {
      return <button className="pt-cta pt-ghost" onClick={() => setOpen(true)}>Order more flyers or cards</button>;
    }

    return (
      <div className="pt-panel">
        <Eyebrow>More print, free as always</Eyebrow>
        <div className="pt-choose">
          <label className="pt-field pt-half">
            <span>Flyers</span>
            <select value={flyers} onChange={e => setFlyers(Number(e.target.value))}>
              <option value={100}>100</option>
              <option value={250}>250</option>
              <option value={500}>500</option>
            </select>
          </label>
          <label className="pt-field pt-half">
            <span>Counter cards</span>
            <select value={cards} onChange={e => setCards(Number(e.target.value))}>
              <option value={0}>None, I'm fine</option>
              <option value={1}>1</option>
              <option value={2}>2</option>
              <option value={3}>3</option>
            </select>
          </label>
        </div>
        <p className="pt-hint">Split evenly across both flyer designs, and posted to the address we already have.</p>
        {error && <p className="pt-err">{error}</p>}
        <div className="pt-actions">
          <button className="pt-cta" disabled={busy} onClick={() => onOrder(flyers, cards)}>
            {busy ? "One moment…" : "Place free order"}
          </button>
          <button className="pt-link" onClick={() => setOpen(false)}>Cancel</button>
        </div>
      </div>
    );
  }

  // The hero for a shop that has not ordered yet.
  //
  // This page used to open with a paragraph and a form. It was accurate and it was
  // underwhelming: a shop arriving from an email about a revenue-share programme
  // got prose and an address field, with no sign of the dashboard they were
  // promised. So the ask is now unmissable and the product is visible behind it.
  function KitHero({ shop, qty, onOrder }) {
    return (
      <section className="pt-hero pt-rise">
        <Eyebrow>Step one, and the only one</Eyebrow>
        <h2 className="pt-herohead">
          Get <span className="pt-herocode">{shop.code}</span> into your customers' hands
        </h2>
        <p className="pt-herolede">
          A counter card for the till and {qty.flyer_a + qty.flyer_b} bag flyers, each one carrying
          your code. We print it, we post it, and it costs you nothing.
        </p>

        <div className="pt-herokit">
          <div><span className="pt-hkq">{qty.card}</span><span className="pt-hkl">counter card</span></div>
          <div><span className="pt-hkq">{qty.flyer_a}</span><span className="pt-hkl">flyers, design A</span></div>
          <div><span className="pt-hkq">{qty.flyer_b}</span><span className="pt-hkl">flyers, design B</span></div>
          <div className="pt-hkfree"><span className="pt-hkq">£0.00</span><span className="pt-hkl">to pay, ever</span></div>
        </div>

        <button className="pt-hero-cta" onClick={onOrder}>Order your free kit&nbsp;&rarr;</button>
        <p className="pt-heronote">
          Print and tracked postage on us. No card, no account, no password — we just need to know
          where to send it.
        </p>
      </section>
    );
  }

  // The dashboard, shown BEFORE there is anything in it.
  //
  // Deliberately not hidden. A shop that has been told it gets a dashboard should
  // be able to see what it will say, and a structure with honest zeros in it reads
  // as "not yet" where a blank page reads as "nothing here". Each row carries the
  // condition that makes it move, so a zero is an explanation rather than a
  // verdict.
  function DormantDashboard({ shop, money, waiting }) {
    const rows = [
      { depth: 0, label: "Scans", note: "every time someone scans your card or a flyer" },
      { depth: 1, label: "Gave us an email", note: "the ones who go on to hand over an address" },
      { depth: 2, label: "Became collectors", note: "tied to " + shop.code + " permanently, from that moment" },
      { depth: 3, label: "Are paying", note: "the subscription launches in Q4" },
    ];
    return (
      <section className="pt-dormant pt-rise" aria-label="Your dashboard, before the kit goes out">
        <div className="pt-dormhead">
          <Eyebrow>Your dashboard</Eyebrow>
          <span className="pt-waiting">{waiting || "waiting on your kit"}</span>
        </div>
        <div className="pt-funnel pt-funnel-dormant">
          {rows.map((r, i) => (
            <div key={r.label} className={"pt-frow pt-d" + r.depth} style={{ "--i": i }}>
              <span className="pt-fname">
                {r.depth > 0 && <span className="pt-tick" aria-hidden="true">└</span>}
                {r.label}
              </span>
              <span className="pt-fcount pt-zero">0</span>
              <span className="pt-fnote">{r.note}</span>
            </div>
          ))}
          <div className="pt-money">
            <div>
              <span className="pt-mlabel">Earned so far</span>
              <span className="pt-mbig pt-zero">{gbp(0)}</span>
              <span className="pt-mnote">Nothing yet, and nothing wrong — the subscription isn't live.</span>
            </div>
            <div>
              <span className="pt-mlabel">Per subscriber, once it is</span>
              <span className="pt-mbig pt-mproj">{gbp(money.per_subscriber_pence)}<span className="pt-per">/mo</span></span>
              <span className="pt-mnote">
                Every month they stay. Ten of your regulars is {gbp(money.per_subscriber_pence * 10)} a month.
              </span>
            </div>
          </div>
        </div>
        <p className="pt-dormfoot">
          These start moving within a week or two of the card going on your counter. Nothing here
          ever goes down.
        </p>
      </section>
    );
  }

  // =======================================================================
  // The page
  // =======================================================================
  function Partner() {
    const tokenRef = useRef(readToken());
    const formRef = useRef(null);
    const [data, setData] = useState(null);
    const [phase, setPhase] = useState("loading");
    const [busy, setBusy] = useState(false);
    const [err, setErr] = useState("");
    const [editingAddress, setEditingAddress] = useState(false);
    const [flash, setFlash] = useState("");

    const load = useCallback(async () => {
      if (!tokenRef.current) { setPhase("no-token"); return; }
      try {
        const out = await post({ action: "dashboard", token: tokenRef.current });
        setData(out.data);
        setPhase("ready");
      } catch (e) {
        setPhase(e.status === 404 || e.status === 401 ? "bad-token" : "error");
      }
    }, []);

    useEffect(() => { load(); }, [load]);

    const act = async (body, okMsg) => {
      setBusy(true); setErr("");
      try {
        const out = await post({ ...body, token: tokenRef.current });
        setData(out.data);
        setEditingAddress(false);
        if (okMsg) { setFlash(okMsg); setTimeout(() => setFlash(""), 6000); }
      } catch (e) {
        setErr(e.message || "Something went wrong. Try again in a moment?");
      } finally {
        setBusy(false);
      }
    };

    // ---- gates -----------------------------------------------------------
    if (phase === "loading") {
      return <div className="pt-wrap pt-centre"><p className="pt-quiet">Opening your dashboard…</p></div>;
    }

    if (phase === "no-token" || phase === "bad-token") {
      return (
        <div className="pt-wrap pt-centre">
          <h1 className="pt-h1">This link isn't working</h1>
          <p>
            {phase === "no-token"
              ? "Your dashboard opens from the link we emailed you — it needs the whole link, including everything after the #."
              : "This link has expired, or it's been reissued. That happens if we've had to replace it."}
          </p>
          <p className="pt-quiet">
            Email <a href="mailto:luca@thelead-in.com?subject=Dashboard%20link">luca@thelead-in.com</a> and we'll
            send you a fresh one the same day.
          </p>
        </div>
      );
    }

    if (phase === "error" || !data) {
      return (
        <div className="pt-wrap pt-centre">
          <h1 className="pt-h1">We can't load this right now</h1>
          <p>Something went wrong at our end, not yours. Try again in a minute.</p>
          <button className="pt-cta" onClick={() => { setPhase("loading"); load(); }}>Try again</button>
        </div>
      );
    }

    const { shop, state, address, funnel, order, money, months, reorder } = data;

    // ---- header ----------------------------------------------------------
    const header = (
      <header className="pt-top">
        <Eyebrow>The Lead-In · Ambassador Programme</Eyebrow>
        <h1 className="pt-h1">{shop.name}</h1>
        <p className="pt-code">
          Your code <strong>{shop.code}</strong>
          {shop.town ? <span className="pt-dim"> · {shop.town}</span> : null}
        </p>
      </header>
    );

    // ---- STATE A: no order yet -------------------------------------------
    // No metrics on this page at all. Four zeros beside an order form tells a
    // shop the programme is broken before it has begun.
    if (state === "no-order") {
      const STARTER = { card: 1, flyer_a: 125, flyer_b: 125 };
      // The hero CTA does not hide the form behind a click — a form that is not
      // there cannot be filled in by someone who scrolled past the button. It
      // scrolls to it and puts the cursor in the first field.
      const jumpToForm = () => {
        try {
          const el = formRef.current;
          if (!el) return;

          const reduce = window.matchMedia
            && window.matchMedia("(prefers-reduced-motion: reduce)").matches;

          // Smooth where it works, and CHECKED, because it does not always: in
          // some embedded browsers scrollIntoView({behavior:"smooth"}) silently
          // does nothing while the instant form scrolls fine. This CTA is the
          // whole point of the page, so it cannot quietly fail to go anywhere.
          const before = window.scrollY;
          el.scrollIntoView({ behavior: reduce ? "auto" : "smooth", block: "center" });
          if (!reduce) {
            setTimeout(() => {
              if (window.scrollY === before) el.scrollIntoView({ block: "center" });
            }, 350);
          }

          const first = el.querySelector("input");
          if (first) setTimeout(() => first.focus({ preventScroll: true }), reduce ? 0 : 500);
        } catch (e) { /* the form is directly below either way */ }
      };

      return (
        <div className="pt-wrap">
          {header}
          <KitHero shop={shop} qty={STARTER} onOrder={jumpToForm} />
          <DormantDashboard shop={shop} money={money} />
          <div className="pt-panel pt-rise" ref={formRef}>
            <Eyebrow>Where should we send it?</Eyebrow>
            <AddressForm
              initial={address}
              busy={busy}
              error={err}
              showNote
              submitLabel="Place free order"
              onSubmit={(ship, note) => act({ action: "order", ship, note }, "")}
            />
          </div>
          <Explainer perSubscriber={money.per_subscriber_pence} />
        </div>
      );
    }

    // ---- ORDERED: one dashboard, whether or not the numbers have arrived ----
    //
    // These used to be two different pages. A shop that had ordered but had no
    // scans yet got a tracker-led page with a dashed placeholder dashboard, and
    // only once a scan landed did it become the real thing. That meant the layout
    // it had been sold arrived twice — once as a preview, once for real — and the
    // moment of transition was a different page rather than a number changing.
    //
    // Now the order completing IS the switch. From then on this is the dashboard,
    // with honest zeros in it, and the first scan changes a figure rather than the
    // furniture. Two things still respond to progress:
    //
    //   the tracker   open while the parcel is in transit, folded once posted
    //   the explainer expanded until the first scan, folded after
    //
    // Both are the same components either way, so nothing can drift between the
    // empty view and the populated one.
    const posted = !!(order && order.status === "posted");
    const started = funnel.scans > 0;

    return (
      <div className="pt-wrap">
        {header}
        {flash && <p className="pt-flash">{flash}</p>}

        <Funnel f={funnel} money={money} />
        <MonthChart months={months} />
        {!months && funnel.collectors > 0 && (
          <p className="pt-quiet">
            The month-by-month chart appears once you've referred five collectors — below that it
            would say more about individual customers than we're willing to show anyone.
          </p>
        )}

        {order && (
          <details className="pt-fold" open={!posted}>
            <summary>
              {posted
                ? `Kit posted ${fmtDate(order.posted_at)}`
                : `Kit ${order.status === "printing" ? "at the printer" : "ordered"} — where it is`}
            </summary>
            <Tracker order={order} scans={funnel.scans} />
          </details>
        )}

        <Reorder reorder={reorder} busy={busy} error={err}
                 onOrder={(flyers, cards) => act({ action: "order", kind: "reorder", flyers, cards, ship: address }, "Ordered — we'll email you when it's posted.")} />

        {/* Expanded until the first scan: that is exactly when a shop needs to be
            told that nothing happening is normal. Folded once the numbers move,
            because by then the page is making the argument itself. */}
        <Explainer compact={started} perSubscriber={money.per_subscriber_pence} />

        {/* Address: theirs to see and change, and the one place this page holds
            personal data. Every change emails the shop, so a forwarded link
            cannot quietly redirect a parcel. */}
        <div className="pt-panel">
          <Eyebrow>Where your print goes</Eyebrow>
          {editingAddress ? (
            <AddressForm
              initial={address}
              busy={busy}
              error={err}
              submitLabel="Save address"
              onSubmit={(ship) => act({ action: "address", ship }, "Saved. We've emailed you a confirmation.")}
            />
          ) : (
            <>
              {address ? (
                <p className="pt-addr">
                  {[address.name, address.line1, address.line2, address.town, address.postcode]
                    .filter(Boolean).map((l, i) => (<span key={i}>{l}<br /></span>))}
                </p>
              ) : (
                // Reachable: a shop can have scans before it has ever ordered, so
                // this panel renders with nothing in it. An empty box above a
                // "Change this" link reads as a page that failed to load.
                <p className="pt-quiet">
                  We haven't got an address for you yet — add one and we'll know where to send your print.
                </p>
              )}
              <button className="pt-link" onClick={() => { setErr(""); setEditingAddress(true); }}>
                {address ? "Change this" : "Add an address"}
              </button>
            </>
          )}
        </div>

        <p className="pt-foot">
          Questions? You get a founder, not a helpdesk —{" "}
          <a href="mailto:luca@thelead-in.com">luca@thelead-in.com</a>. Keep this link to yourself:
          anyone who has it can see your dashboard.
        </p>
      </div>
    );
  }

  window.Partner = Partner;
})();
