/* global React, window */
/* =========================================================
   STUDY CHECKOUT — the screen where the approach stops being
   something that happened and becomes something you chose.

   It is one viewport and it does not scroll. Left, the audience:
   who this study is about, and which of the Digital Twin panels we
   already hold can answer for them. Right, the consequence: how the
   sample splits, when it lands, and the button.

   WHAT CHANGED, AND WHY. The previous build put two calipers here —
   Timeline and Confidence — and derived everything from them. They
   are gone. They asked the merchant to grade the study before they
   had looked at who was in it, and the interesting decision on this
   page was never "how many days"; it is "do the panels we already
   hold cover the people I care about, or do I have to go find them."
   So the audience IS the caliper now: sample size and the ready date
   are both derived from the markets you pick and whether anyone has
   to be recruited into them.

   THE BINDING RUNS BOTH WAYS.
     left  -> right : editing the audience re-scores every panel, and
                      re-derives the totals, the split and the date.
     right -> left  : the mode tabs change what the panels are FOR —
                      Digital Twins gives them the whole sample, New
                      audience stands them down, Mixed splits it.
     left  -> right : switching every panel off is itself a mode
                      change; there is nothing left for twins to do,
                      so the mode follows.
   No setter writes state directly. Every change goes through ckClamp,
   so an incoherent state is unreachable rather than reachable-and-
   then-warned-about.

   TONE: this is read by a merchant, not a researcher. The page leads
   with sentences and keeps the arithmetic underneath them. No
   percentage is ever the headline.

   FW.BACKTEST is deliberately not read here. Its paired/sameCall/
   meanGapPts values are a proposed schema awaiting measurement, not
   results, so no accuracy figure is asserted anywhere on this page.

   COPY RULE: "Digital Twins" is a proper noun and is capitalised
   everywhere it appears — labels, captions, tab names, chart legend.
   The recruited side is "New audience" — shoppers we go out and find
   for this study, as opposed to ones we have already asked. It used
   to read "Real people", which quietly conceded that the twins were
   not, in a product whose whole claim is that a twin is a compression
   of this brand's own past respondents.
   ========================================================= */
const { useState: ckS, useRef: ckR, useEffect: ckE } = React;

const ckFmt = n => Math.round(n).toLocaleString();

/* Dates are derived from the day the demo is opened, never authored. A demo
   that reads "Fri 28 Aug" in October tells the room the screen is a picture.
   `days` is the only truth here; the weekday and date are rendered from it. */
const ckWhen = days => {
  if (days < 1) return "in about 1 hour";
  const d = new Date();
  d.setDate(d.getDate() + Math.ceil(days));
  return d.toLocaleDateString("en-GB", { weekday: "short", day: "numeric", month: "short" });
};

/* Panels carry how long ago they were fielded, not the month they were
   fielded in, for the same reason the delivery date is derived: a panel
   stamped "Mar 2025" reads as recent in 2026 and as neglected in 2028,
   and the demo should not rot. Months-ago is the fact; the month label
   is rendered from it. */
const ckSurveyed = agoM => {
  const d = new Date();
  d.setMonth(d.getMonth() - agoM);
  return d.toLocaleDateString("en-GB", { month: "short", year: "numeric" });
};
const ckAgo = agoM => agoM < 1 ? "this month"
  : agoM === 1 ? "1 month ago"
  : agoM < 12 ? agoM + " months ago"
  : agoM < 24 ? "just over a year ago"
  : Math.round(agoM / 12) + " years ago";

/* ---------------------------------------------------------
   The audience vocabulary.

   Country, gender and age are the intake's own three — the same
   fields, the same options, in the same order as the Audience tab in
   the builder (explore-intake/ei4-app.jsx AUD_OPTS), because this
   screen is meant to reflect that answer back rather than open a
   second, subtly different one.

   The fourth row is new and is the one that earns the screen: the
   behavioural screen the study actually runs. It is what separates
   "women 25-34 in Germany" — which any panel company can field — from
   the group this brand keeps asking, which is the only reason a twin
   of them exists.
   --------------------------------------------------------- */
const CK_COUNTRIES = ["United States", "Canada", "United Kingdom", "Germany", "France",
  "Spain", "Italy", "Japan", "China", "Australia", "Brazil", "Mexico"];
const CK_GENDERS = ["Women", "Men"];
const CK_AGES = ["18–24", "25–34", "35–44", "45–54", "55+"];
const CK_BEHAVIOURS = [
  { id: "trains", lbl: "Trains 3+ times a week" },
  { id: "outdoor", lbl: "Runs or trains outdoors" },
  { id: "bought12", lbl: "Bought performance apparel in 12 months" },
  { id: "athleisure", lbl: "Wears athleisure daily" },
  { id: "competitor", lbl: "Shops competitor brands" },
  { id: "family", lbl: "Buys for their family" }
];
const ckBehLbl = id => (CK_BEHAVIOURS.filter(b => b.id === id)[0] || { lbl: id }).lbl;

/* Which behavioural screen a study opens with follows the moment picked on
   home, so a pricing study does not open pre-screened on "runs outdoors".
   Keyed to window.EH.moments ids; anything unrecognised gets the default. */
const CK_MOMENT_BEH = {
  kickoff: ["trains", "bought12", "competitor"],
  design: ["trains", "outdoor", "bought12"],
  naming: ["bought12", "athleisure"],
  pricing: ["bought12", "competitor"],
  line: ["trains", "bought12", "athleisure"],
  marketing: ["athleisure", "competitor"],
  account: ["bought12", "family"]
};

/* ---------------------------------------------------------
   The panels we already hold.

   Each one is a real past study's respondents, compressed. `n` is how
   many twins the panel carries, `agoM` how long ago those people were
   originally surveyed, and the four attribute lists are what the panel
   is made of — which is what makes a similarity score computable
   rather than asserted.

   Six exist; three are shown. Which three is not authored — it is
   whichever three score highest against the audience on the left, so
   editing the audience genuinely re-orders the shelf.
   --------------------------------------------------------- */
const CK_PANELS = [
  { id: "gym", name: "Training & gym regulars", n: 3200, agoM: 5,
    countries: ["United States", "United Kingdom", "Germany"], genders: ["Women", "Men"],
    ages: ["18–24", "25–34", "35–44"], beh: ["trains", "bought12"],
    from: "Spring '26 training apparel range read" },
  { id: "run", name: "Run club runners", n: 2100, agoM: 11,
    countries: ["United States", "United Kingdom", "France"], genders: ["Women", "Men"],
    ages: ["25–34", "35–44", "45–54"], beh: ["outdoor", "trains", "bought12"],
    from: "FW '25 running franchise test" },
  { id: "athleisure", name: "Everyday athleisure wearers", n: 2650, agoM: 3,
    countries: ["United Kingdom", "France", "Germany"], genders: ["Women"],
    ages: ["25–34", "35–44"], beh: ["athleisure", "bought12"],
    from: "Spring '26 women's lifestyle read" },
  { id: "youth", name: "Youth athletes 18–24", n: 980, agoM: 8,
    countries: ["United States", "United Kingdom"], genders: ["Women", "Men"],
    ages: ["18–24"], beh: ["trains", "competitor"],
    from: "FW '25 youth team sport study" },
  { id: "parents", name: "Team sport parents", n: 1450, agoM: 19,
    countries: ["United States", "Canada"], genders: ["Women", "Men"],
    ages: ["35–44", "45–54"], beh: ["family", "bought12"],
    from: "FW '24 family sport assortment" },
  { id: "lapsed", name: "Lapsed brand buyers", n: 1120, agoM: 26,
    countries: ["United States", "United Kingdom"], genders: ["Women", "Men"],
    ages: ["25–34", "35–44", "45–54"], beh: ["competitor"],
    from: "FW '24 win-back study" }
];

/* ---------------------------------------------------------
   Similarity.

   The badge is derived, never authored, because an authored badge is
   a picture: it would sit at "High" while the merchant added Japan to
   the markets, which is the exact moment it needs to move.

   Each dimension scores as "how much of what you asked for does this
   panel cover" — the denominator is the SELECTION, not the panel. A
   panel that covers your three markets and nine others is a perfect
   market match; the extra markets are not a penalty, they are just
   people this study will not read.

   Weights say what a merchant would say out loud: markets first
   (a twin of a German shopper cannot answer for Japan), then the age
   bands and the behavioural screen, then gender, which in this
   category is the least discriminating of the four.

   Recency is deliberately NOT in the score. It is shown on every card
   and named in the hover, but a 26-month-old panel of exactly the
   right people is still a panel of exactly the right people, and
   folding age-of-data into a "similarity" number would conflate two
   questions the merchant should be allowed to weigh separately. */
const CK_W = { markets: 0.35, ages: 0.25, beh: 0.25, gender: 0.15 };

const ckOverlap = (has, wants) => {
  const hit = wants.filter(x => has.indexOf(x) >= 0).length;
  return { hit: hit, of: wants.length, r: wants.length ? hit / wants.length : 1 };
};
/* "all 3 markets" / "2 of 3 markets" / "no market overlap" — a phrase a
   merchant can repeat, not a ratio they have to read. */
const ckPhrase = (o, one, many) =>
  o.of === 0 ? "no " + many + " set"
  : o.hit === 0 ? "no " + one + " overlap"
  : o.hit === o.of ? (o.of === 1 ? "the one " + one : "all " + o.of + " " + many)
  : o.hit + " of " + o.of + " " + many;

function ckSim(p, sel) {
  const mk = ckOverlap(p.countries, sel.countries);
  const ag = ckOverlap(p.ages, sel.ages);
  const bh = ckOverlap(p.beh, sel.beh);
  const gd = ckOverlap(p.genders, sel.genders);
  const score = mk.r * CK_W.markets + ag.r * CK_W.ages + bh.r * CK_W.beh + gd.r * CK_W.gender;
  const band = score >= 0.72 ? "High" : score >= 0.42 ? "Medium" : "Low";
  const why = [ckPhrase(mk, "market", "markets"), ckPhrase(ag, "age band", "age bands"),
    ckPhrase(bh, "behavior", "behaviors"), ckPhrase(gd, "gender", "genders")]
    .join(" · ") + ". Surveyed " + ckAgo(p.agoM) + ", on your " + p.from + ".";
  /* The markets this panel simply cannot speak for. Named, because it is
     the one gap that costs either money or credibility depending on which
     mode you are in. */
  const gaps = sel.countries.filter(c => p.countries.indexOf(c) < 0);
  return { score: score, band: band, why: why, gaps: gaps, mk: mk, ag: ag, bh: bh, gd: gd };
}

/* Top three, best first. Ties break on panel size — more twins is more
   sample, so between two equally relevant panels the bigger one is the
   better offer. */
function ckShelf(sel) {
  return CK_PANELS.map(p => Object.assign({ p: p }, ckSim(p, sel)))
    .sort((a, b) => (b.score - a.score) || (b.p.n - a.p.n))
    .slice(0, 3);
}

/* ---------------------------------------------------------
   Who does the answering. Three shapes, not a dial: this is a choice
   between kinds of evidence, and a slider between them would imply
   intermediate values that do not exist.
   --------------------------------------------------------- */
const CK_MODES = [
  { id: "twins", lbl: "Digital Twins", share: 100,
    line: "Answered entirely from panels you already hold. Nothing to recruit, so nothing to wait for." },
  { id: "mixed", lbl: "Mixed", share: 55,
    line: "Digital Twins do the volume and new shoppers check the calls that cost money. Most studies land here." },
  { id: "people", lbl: "New audience", share: 0,
    line: "Every answer comes from a shopper we go out and find. Slowest and dearest, and nobody can argue with it." }
];
const ckMode = id => CK_MODES.filter(m => m.id === id)[0];

/* Sample is per market, not per study: two markets is twice the study,
   and a merchant who adds Japan should watch the number move rather
   than discover later that Japan was carved out of the same 600. */
const CK_PER_MARKET = 200;

/* ---------------------------------------------------------
   One function derives the whole right-hand side, and the date with it.

   Recruiting is the only thing on this page that genuinely takes time:
   two days to stand a panel up, and two on top if any market has no twin
   coverage at all — that is a group nobody has asked before, and finding
   them is slower than topping up a group we already know how to reach.

   Markets field in parallel, so a covered market you add costs sample and
   not time; only a market we have never asked in moves the date. Analysis
   is one day after fielding closes, at every depth — that gap is the whole
   end-to-end argument, so it is a constant here rather than a queue.
   --------------------------------------------------------- */
function ckQuote(st) {
  const sel = { countries: st.countries, genders: st.genders, ages: st.ages, beh: st.beh };
  const mode = ckMode(st.mode);
  const shelf = ckShelf(sel);
  const on = shelf.filter(x => st.off.indexOf(x.p.id) < 0);

  const total = st.countries.length * CK_PER_MARKET;
  const pool = on.reduce((a, x) => a + x.p.n, 0);
  /* The mode asks for a share; the panels can only supply what they hold.
     min() is a safety rail rather than a live constraint at these sizes,
     but the arithmetic should not be able to promise twins that do not
     exist. */
  const twins = on.length ? Math.min(Math.round(total * mode.share / 100), pool) : 0;
  const humans = Math.max(0, total - twins);

  /* Twins are drawn from the picked panels in proportion to their size —
     the largest panel carries the most of the study, which is both what a
     sampler would do and what the rail needs in order to show a breakdown
     that adds up to the total. Remainder lands on the last row so the
     column sums exactly rather than to within a rounding error. */
  let left = twins;
  const rows = on.map((x, i) => {
    const share = i === on.length - 1 ? left : (pool ? Math.round(twins * x.p.n / pool) : 0);
    left -= share;
    return { id: x.p.id, name: x.p.name, n: Math.max(0, share), sim: x };
  });

  /* Markets no picked panel covers. In Mixed and New audience they are the
     reason the date slips; in Digital Twins they are a hole in the read,
     because there is no one to recruit and no twin who can stand in. */
  const gaps = st.countries.filter(c => !on.some(x => x.p.countries.indexOf(c) >= 0));
  const field = humans > 0 ? 2 + (gaps.length ? 2 : 0) : 0;
  const days = field ? field + 1 : 0;

  return { sel: sel, mode: mode, shelf: shelf, on: on, rows: rows, gaps: gaps,
    total: total, twins: twins, humans: humans, field: field, days: days };
}

/* Every cross-panel invariant, in one place, applied to every change.

   There is exactly one state this page can reach that describes a study
   nobody can run: a twins-led mix with no panels in it. Rather than
   disable the panel toggle — a control you can press that refuses is
   broken, not instructive — the mode follows the toggle out. Which is
   the same rule the mode tabs get in reverse: pick Digital Twins with
   everything switched off and the panels come back on, because the pick
   itself is never the thing that gets undone. */
function ckClamp(st, touched) {
  const next = Object.assign({}, st);
  const sel = { countries: next.countries, genders: next.genders, ages: next.ages, beh: next.beh };
  const shelfIds = ckShelf(sel).map(x => x.p.id);
  const anyOn = shelfIds.some(id => next.off.indexOf(id) < 0);

  if (touched === "mode") {
    if (next.mode !== "people" && !anyOn) next.off = next.off.filter(id => shelfIds.indexOf(id) < 0);
  } else if (!anyOn && next.mode !== "people") {
    next.mode = "people";
  }
  return next;
}

/* ---------------------------------------------------------
   Audience rows.

   Every row shows the SELECTION and nothing else. An earlier pass laid
   the whole option set out as chips and lit the chosen ones green —
   which is what a picker looks like, not what an audience looks like.
   On a checkout the merchant is reading back who this study is about;
   twelve countries with three lit is a catalogue they have to parse to
   find their own answer in, and it buried the panels below it under two
   rows of markets nobody picked.

   So: chosen values are tags, and everything else lives behind one
   "add" control per row. Same grammar as the builder's Audience tab
   (explore-intake/ei4-app.jsx AddMenu), which reached the same
   conclusion about country for the same reason — the difference is
   that here it applies to all four rows, because on this screen even
   five age bands is four more facts than the answer needs.

   The row stays editable rather than going read-only: the left half of
   the two-way binding is the whole reason the panels below re-score,
   and a read-out has no lever.
   --------------------------------------------------------- */
function CKField({ k, sub, sel, all, lblOf, addLbl, onAdd, onDrop }) {
  const [open, setOpen] = ckS(false);
  const wrap = ckR(null);
  /* The menu closes on any click outside itself. Without this it stays open
     behind the next thing you press, and on a screen where the next thing
     you press is usually a panel toggle that reads as the page ignoring you. */
  ckE(() => {
    if (!open) return;
    const away = e => { if (wrap.current && !wrap.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", away);
    return () => document.removeEventListener("mousedown", away);
  }, [open]);
  const lbl = v => lblOf ? lblOf(v) : v;
  const rest = all.filter(v => sel.indexOf(v) < 0);
  return (
    <div className="ck-af">
      <div className="ck-af__l">{k}{sub && <span>{sub}</span>}</div>
      <div className="ck-af__c">
        {sel.map(v => (
          <span className="ck-tag" key={v}>{lbl(v)}
            <button type="button" onClick={() => onDrop(v)} aria-label={"Remove " + lbl(v)}>
              <i className="ti ti-x" />
            </button>
          </span>
        ))}
        {/* An empty row is a real state — it is how the study gets blocked —
            so it says so rather than rendering as a gap someone reads past. */}
        {!sel.length && <span className="ck-tag ck-tag--none">None set</span>}
        {rest.length ? (
          <div className="ck-add" ref={wrap}>
            {/* Icon-only: the label still exists, it just moved to aria-label
                and to the row's own heading — "+ Add market" beside a row
                already headed "Country" was saying market twice. */}
            <button type="button" className={"ck-add__b" + (open ? " is-on" : "")}
              aria-expanded={open} aria-label={addLbl} title={addLbl}
              onClick={() => setOpen(o => !o)}>
              <i className="ti ti-plus" />
            </button>
            {open && (
              <div className="ck-pop">
                {rest.map(v => (
                  <button type="button" className="ck-pop__o" key={v}
                    onClick={() => { onAdd(v); setOpen(false); }}>{lbl(v)}</button>
                ))}
              </div>
            )}
          </div>
        ) : null}
      </div>
    </div>
  );
}

/* ---------------------------------------------------------
   A panel. The card states four things in the order a merchant asks
   them: who is in it, how many, how relevant, and how old.
   --------------------------------------------------------- */
function CKPanel({ x, n, on, dim, onTog }) {
  const p = x.p;
  const band = x.band.toLowerCase();
  return (
    <div className={"ck-pan" + (on ? "" : " is-off") + (dim ? " is-dim" : "")}>
      <button type="button" className="ck-pan__tick" aria-pressed={on}
        aria-label={(on ? "Leave out " : "Use ") + p.name}
        onClick={() => onTog(p.id)}>
        <i className={"ti ti-" + (on ? "check" : "plus")} />
      </button>
      <div className="ck-pan__b">
        <div className="ck-pan__hd">
          <span className="ck-pan__nm">{p.name}</span>
          {/* The reason lives in the badge rather than beside it. On the card
              it would be a third line of small grey type on every row, read
              once and then never again; on hover it is there for exactly the
              person who just asked "why is that one High?" — which in a demo
              is somebody in the room, out loud. */}
          <span className={"ck-badge ck-badge--" + band} tabIndex={0}
            aria-label={x.band + " similarity. " + x.why}>
            {x.band}<i className="ti ti-info-circle" />
            <span className="ck-badge__tip" role="tooltip">
              <b>{x.band} similarity</b>{x.why}
            </span>
          </span>
        </div>
        <div className="ck-pan__meta">
          <span><b>{ckFmt(p.n)}</b> Digital Twins</span>
          <span className="ck-pan__dot" />
          <span>Surveyed {ckSurveyed(p.agoM)}<em> · {ckAgo(p.agoM)}</em></span>
        </div>
      </div>
      <div className="ck-pan__n">{on && n > 0 ? <React.Fragment><b>{ckFmt(n)}</b><em>answering</em></React.Fragment>
        : <em>{dim ? "standing by" : "not used"}</em>}</div>
    </div>
  );
}

/* ---------------------------------------------------------
   The screen
   --------------------------------------------------------- */
function StudyCheckout({ name, busy, moment, aud, tier, onCancel, onConfirm }) {
  /* Country, gender and age are seeded from the builder's Audience tab when
     it hands them over, so the two screens are answering with one set of
     facts rather than two that happen to agree. The behavioural screen has
     no counterpart upstream yet, so it follows the moment. */
  /* Seeds are normalised to the master order, not taken in whatever order they
     arrive from upstream. Adding a value rebuilds the row in master order, so a
     seed that is not already in it would have the untouched values visibly
     reshuffle the first time you added anything — an edit to one market
     appearing to move two others. Values the master list does not carry are
     dropped rather than rendered as tags with nothing behind them. */
  const seed = (all, given, fallback) => {
    const want = given && given.length ? given : fallback;
    const keep = all.filter(v => want.indexOf(v) >= 0);
    return keep.length ? keep : all.filter(v => fallback.indexOf(v) >= 0);
  };
  /* The mode tab follows the dock's own answer in on arrival. "quick" is the
     one tier that is twin-only by construction (AT_MIX.quick = 1 — a same-day
     deadline or a Directional bar can only be answered from panels you
     already hold, nobody gets recruited in half an hour), so checkout should not
     open on Mixed and make the merchant re-tell it what they just told the
     chat. Any other tier — or no tier at all, when checkout is reached
     without going through the dock — keeps the previous default. */
  const [st, setSt] = ckS(() => ({
    countries: seed(CK_COUNTRIES, aud && aud.countries,
      ["United States", "United Kingdom", "Germany"]).slice(0, 5),
    genders: seed(CK_GENDERS, aud && aud.genders, ["Women", "Men"]),
    ages: seed(CK_AGES, aud && aud.ages, ["18–24", "25–34", "35–44"]),
    beh: CK_MOMENT_BEH[moment] || ["trains", "bought12"],
    mode: tier === "quick" ? "twins" : "mixed",
    off: []
  }));
  /* `patch` may be an object or a function of the previous state, and every
     toggle on this page uses the function form. It has to: a toggle derived
     from the `st` in the render closure computes its new array from a value
     that is already stale the moment a second click lands in the same tick,
     and React 18 batches those — so two quick chips in a row would silently
     resolve to one. Every write goes through ckClamp on the way out, so the
     invariants hold for the function form too. */
  const set = (patch, touched) => setSt(s =>
    ckClamp(Object.assign({}, s, typeof patch === "function" ? patch(s) : patch), touched));
  /* Adding rebuilds the list from the master order rather than appending, so
     a market added late does not land after one added early — the row reads
     the same however it was assembled. Both go through the functional setter
     for the batching reason above. */
  const add = (k, all) => v => set(s => ({ [k]: all.filter(x => s[k].indexOf(x) >= 0 || x === v) }));
  const drop = (k) => v => set(s => ({ [k]: s[k].filter(x => x !== v) }));

  const q = ckQuote(st);
  const twinPct = q.total ? Math.round(q.twins / q.total * 100) : 0;
  /* An audience with no market, no gender or no age band is not a narrow
     study, it is an empty one — and the total says 0, so the button has to
     say why rather than sending nothing to field. */
  const missing = !st.countries.length ? "a market" : !st.genders.length ? "a gender"
    : !st.ages.length ? "an age band" : null;
  /* Two dates, not one. "Fielding closes" then "results ready" the next day is
     the claim this page exists to make; collapsing them back into a single
     number hides the only interval a competitor cannot match. */
  const ready = q.days < 1 ? "In about 30 minutes" : ckWhen(q.days);
  const fieldsAt = q.field ? ckWhen(q.field) : null;
  const dim = st.mode === "people";

  /* Clicking a stood-down panel is a real choice with the same standing as
     a tab: it turns the panel on AND brings the mode back to Mixed, rather
     than silently doing half of what the click asked for. */
  const togPanel = (id) => {
    if (dim) return set(s => ({ mode: "mixed", off: s.off.filter(x => x !== id) }), "mode");
    set(s => ({ off: s.off.indexOf(id) >= 0 ? s.off.filter(x => x !== id) : s.off.concat(id) }));
  };

  return (
    <div className="ck-page" data-screen-label="Study checkout">
      <div className="ck-bar">
        <span className="ck-bar__logo"><img className="ua-mark" src="brand/ua-mark.svg" alt="Under Armour" /></span>
        <span className="ck-bar__k">Review &amp; launch</span>
        <span className="ck-bar__nm">{name}</span>
        <span className="ck-bar__sp" />
        <button className="ck-bar__x" onClick={busy ? null : onCancel} disabled={busy} aria-label="Back to the study">
          <i className="ti ti-x" />
        </button>
      </div>

      <div className="ck-wrap">
        {/* ---- left: who this study is about ---- */}
        <div className="ck-main">

          <section className="ck-card">
            <div className="ck-card__hd">
              <h2>New audience to recruit</h2>
              <span className="ck-card__n">{ckFmt(q.total)} respondents</span>
            </div>
            <div className="ck-aud">
              <CKField k="Country" addLbl="Add market"
                sel={st.countries} all={CK_COUNTRIES}
                onAdd={add("countries", CK_COUNTRIES)} onDrop={drop("countries")} />
              <CKField k="Gender" addLbl="Add"
                sel={st.genders} all={CK_GENDERS}
                onAdd={add("genders", CK_GENDERS)} onDrop={drop("genders")} />
              <CKField k="Age" addLbl="Add band"
                sel={st.ages} all={CK_AGES}
                onAdd={add("ages", CK_AGES)} onDrop={drop("ages")} />
              <CKField k="Other" addLbl="Add screen"
                sel={st.beh} all={CK_BEHAVIOURS.map(b => b.id)} lblOf={ckBehLbl}
                onAdd={add("beh", CK_BEHAVIOURS.map(b => b.id))} onDrop={drop("beh")} />
            </div>
          </section>

          {/* The panels. Which three appear, and in what order, is derived
              from the card above — this is the half of the binding that
              makes editing the audience worth doing on this screen rather
              than back in the builder. */}
          <section className={"ck-card" + (dim ? " is-dim" : "")}>
            <div className="ck-card__hd">
              <h2>Digital Twins from your historical testing</h2>
              <span className="ck-card__n">{dim ? "Standing by" : ckFmt(q.twins) + " of " + ckFmt(q.total)}</span>
            </div>
            <p className="ck-card__lede">
              {dim
                ? "This study asks new shoppers instead. Your panels are still here — pick one to bring them back in."
                : "The three closest matches to the audience above, out of " + CK_PANELS.length + " you hold."}
            </p>
            <div className="ck-pans">
              {q.shelf.map(x => {
                const row = q.rows.filter(r => r.id === x.p.id)[0];
                return <CKPanel key={x.p.id} x={x} n={row ? row.n : 0} dim={dim}
                  on={!dim && st.off.indexOf(x.p.id) < 0} onTog={togPanel} />;
              })}
            </div>
            {/* Coverage gaps are shown in every mode, including New audience,
                because a gap is now the ONLY thing that moves the date — covered
                markets field in parallel, so the two extra days belong to a
                market nobody has ever asked. What changes is which fact leads: with
                the panels stood down, "no panel covers Japan" is beside the
                point and "we have never asked there" is the whole of it. */}
            {q.gaps.length ? (
              <div className="ck-warn ck-pans__flag">
                <i className="ti ti-alert-triangle" />
                {dim
                  ? <span>You have never asked shoppers in {q.gaps.join(" or ")}, so finding them
                      adds a couple of days to the date.</span>
                  : <span>No panel you hold covers {q.gaps.join(" or ")}.{" "}
                      {q.humans > 0
                        ? "We have never asked there, so finding those shoppers adds a couple of days."
                        : "On Digital Twins alone that market goes unanswered — switch to Mixed to recruit into it."}</span>}
              </div>
            ) : null}
          </section>
        </div>

        {/* ---- right: the consequence ---- */}
        <aside className="ck-side">
          <div className="ck-sum">
            <div className="ck-seg" role="tablist" aria-label="Who answers">
              {CK_MODES.map(m => (
                <button key={m.id} role="tab" aria-selected={m.id === st.mode}
                  className={m.id === st.mode ? "is-on" : ""}
                  onClick={() => set({ mode: m.id }, "mode")}>{m.lbl}</button>
              ))}
            </div>
            <p className="ck-segline">{q.mode.line}</p>

            <div className="ck-sum__k">Who we&rsquo;ll ask <em>{ckFmt(q.total)} people</em></div>
            <div className="ck-grps">
              {q.rows.filter(r => r.n > 0).map(r => (
                <div className="ck-grp" key={r.id}>
                  <span className="ck-grp__nm" title={r.name}>{r.name}</span>
                  <span className={"ck-dot ck-dot--" + r.sim.band.toLowerCase()} title={r.sim.band + " similarity"} />
                  <span className="ck-grp__n">{ckFmt(r.n)}</span>
                </div>
              ))}
              {q.humans > 0 && (
                <div className="ck-grp" key="new">
                  <span className="ck-grp__nm">New audience<em>{st.countries.length
                    ? st.countries.length + (st.countries.length === 1 ? " market" : " markets") : "no market yet"}</em></span>
                  <span className="ck-grp__n">{ckFmt(q.humans)}</span>
                </div>
              )}
              {!q.rows.some(r => r.n > 0) && q.humans === 0 && (
                <div className="ck-grp ck-grp--empty">Nothing selected yet.</div>
              )}
            </div>

            {/* explore-intake's own split bar, hatch and legend. Same fact
                as the intake rail, so it is the same picture and there is
                one definition of it. */}
            <div className="ck-split">
              <div className="i4-split">
                <i className="i4-split__t" style={{ width: twinPct + "%" }} />
                <i className="i4-split__h" style={{ width: (100 - twinPct) + "%" }} />
              </div>
              <div className="i4-legend">
                <span><i className="i4-key i4-key--t" />Digital Twins<b>{ckFmt(q.twins)}</b></span>
                <span><i className="i4-key i4-key--h" />New audience<b>{ckFmt(q.humans)}</b></span>
              </div>
            </div>

            <div className="ck-sum__rows">
              {fieldsAt && <div className="ck-sum__r"><span>Fielding closes</span><b>{fieldsAt}</b></div>}
              <div className="ck-sum__r"><span>Results ready</span><b>{ready}</b></div>
              <div className="ck-sum__note">{fieldsAt
                ? "Your pages land the day after fielding closes. No analyst queue."
                : "Answered from panels you already hold, so your pages land today."}</div>
            </div>

            <button className="ck-go" disabled={busy || !!missing}
              onClick={() => onConfirm && onConfirm({
                total: q.total, name: q.mode.lbl, back: ready,
                who: q.twins && q.humans
                  ? ckFmt(q.twins) + " Digital Twins, " + ckFmt(q.humans) + " new shoppers"
                  : q.humans ? ckFmt(q.humans) + " new shoppers"
                  : ckFmt(q.twins) + " Digital Twins"
              })}>
              {busy ? <React.Fragment><span className="sf-spin" />Sending it out…</React.Fragment>
                : missing ? "Add " + missing + " to continue"
                : <React.Fragment>Launch study<i className="ti ti-arrow-right" /></React.Fragment>}
            </button>
            <button className="ck-back" onClick={onCancel} disabled={busy}>Keep editing</button>
            <div className="ck-sum__fine">Nothing goes to field until you launch.</div>
          </div>
        </aside>
      </div>
    </div>
  );
}

Object.assign(window, { StudyCheckout: StudyCheckout, ckQuote: ckQuote, ckSim: ckSim,
  CK_PANELS: CK_PANELS, CK_BEHAVIOURS: CK_BEHAVIOURS });
