/* global React, ScreenFrame, SummaryTitle, SummarySelectors, SetTotal, SquareStill, ScrubSequence, GiftLine, KeraiGlassHeader, KERAI_HEAD_H, KeraiMark, Display, Eyebrow, Body, PrimaryButton, themeTokens, useTheme, useReducedMotion, Sheet, QrCode, WhatsAppGlyph, TYPE, SERIF, typeName */
const { useState: useStateD, useEffect: useEffectD } = React;

/* ------------------------------------------------------------------ */
/* 07. PAYMENT AND COLLECTION                                          */
/* One quiet shell for every payment, dispensing and recovery state.   */
/* The screen follows the checkout controller; the kiosk itself never  */
/* handles card details — the card reader below the screen does.       */
/* ------------------------------------------------------------------ */
const KCD = window.KeraiCommerce;

function useCheckoutState(controller) {
  const [state, setState] = useStateD(() => controller.getState());
  useEffectD(() => {
    setState(controller.getState());
    return controller.subscribe(setState);
  }, [controller]);
  return state;
}

/* Indeterminate loader — a slow turning arc. */
function PayLoader({ reduced }) {
  const t = themeTokens(useTheme());
  return (
    <svg width="132" height="132" viewBox="0 0 132 132" aria-hidden="true"
         style={{ display: "block", animation: reduced ? "kerai-pay-pulse 2.4s ease-in-out infinite" : "kerai-pay-spin 1.6s linear infinite" }}>
      <circle cx="66" cy="66" r="58" fill="none" stroke={t.line} strokeWidth="3" />
      <circle cx="66" cy="66" r="58" fill="none" stroke={t.gold} strokeWidth="3" strokeLinecap="round"
              strokeDasharray={`${Math.PI * 116 * 0.24} ${Math.PI * 116}`} />
    </svg>
  );
}

/* Dispensing ring — fills from when the machine accepted the vend. It
   only completes when the machine is expected to be ready on time. */
function DispenseRing({ state, script, size = 280, strong = false, settled = false, hidden = false }) {
  const t = themeTokens(useTheme());
  const reduced = useReducedMotion();
  const [progress, setProgress] = useStateD(() => KCD.dispenseProgress(state, Date.now(), script) || 0);
  useEffectD(() => {
    let raf;
    const tick = () => {
      const p = KCD.dispenseProgress(state, Date.now(), script);
      if (p != null) setProgress(p);
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [state, script]);
  const R = size / 2 - 20;
  const C = 2 * Math.PI * R;
  const mid = size / 2;
  return (
    <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} role="progressbar" aria-valuemin={0} aria-valuemax={100}
         aria-valuenow={Math.round(progress * 100)} style={{ display: "block", transform: "rotate(-90deg)" }}>
      <circle cx={mid} cy={mid} r={R} fill="none" stroke={t.line} strokeWidth="3" />
      <circle cx={mid} cy={mid} r={R} fill="none" stroke={t.gold} strokeWidth="3" strokeLinecap="round"
              strokeDasharray={C}
              style={{
                strokeDashoffset: C * (1 - (settled ? 1 : progress)),
                transition: reduced ? "none" : "opacity 0.45s ease",
                opacity: hidden ? 0 : 1,
                animation: reduced || hidden ? "none" : strong
                  ? "kerai-ring-pulse-strong 1.5s ease-in-out infinite"
                  : settled ? "none" : "kerai-ring-pulse 2.8s ease-in-out infinite",
              }} />
    </svg>
  );
}

/* Points the customer at hardware below the screen: an arrow in a ring,
   easing gently downwards. */
function CardCue({ reduced }) {
  const t = themeTokens(useTheme());
  return (
    <div aria-hidden="true" style={{
      width: 132, height: 132, borderRadius: "50%", border: `1.5px solid ${t.gold}`,
      display: "flex", alignItems: "center", justifyContent: "center", color: t.gold,
    }}>
      <svg width="58" height="58" viewBox="0 0 24 24" fill="none"
           style={{ display: "block", animation: reduced ? "none" : "kerai-cue-bob 1.8s ease-in-out infinite" }}>
        <path d="M12 4.5 V19 M5.5 12.5 L12 19 L18.5 12.5" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
      </svg>
    </div>
  );
}

const PAY_BOX = window.KERAI_PAY_BOX || { top: 600, setTop: 400, setGap: 24 };

// One entry per piece. A set's pieces carry no price of their own; the
// set total is shown beneath them.
function purchasePieces(snap) {
  const stock = KCD.baseStock();
  // Gold colour and carat are already on the selectors, so the specs
  // beneath carry the other six of the eight short rows.
  const piece = (product, variant) => ({
    key: variant.sku,
    product,
    metals: KCD.metalOptions(product, stock),
    metal: variant.metal,
    carats: KCD.caratOptions(product, variant.metal, stock),
    caratId: variant.caratId,
    specs: KCD.shortSpecRows(product, variant).filter((r) => r.id !== "colour" && r.id !== "carat"),
  });
  if (snap.kind === "set") {
    return KCD.setParts(KCD.SET_BY_ID[snap.setId]).map(({ product, variant, still }) => ({
      ...piece(product, variant),
      priceMinor: variant.amountMinor,
      showPrice: true,
      label: typeName(product),
      still,
    }));
  }
  const variant = KCD.VARIANT_BY_SKU[snap.vendSku];
  return [{ ...piece(KCD.PRODUCT_BY_ID[snap.productId], variant), priceMinor: snap.amountMinor, showPrice: true }];
}

/* The purchase, boxed. Its right-hand side is the product page's own
   title and selectors with just the chosen options, so the block carries
   straight over; the square video then opens out to its left and the
   details draw together beside it. A set's pieces arrive with their
   square already showing the packshot (`still`), and the video fades in
   over it. */
// The box opens to make room, the rule draws across, then the rows drift
// in one after another.
const SPEC_EASE = "cubic-bezier(.22,.7,.2,1)";
function PurchaseSpecs({ rows, compact, reduced, delay }) {
  const t = themeTokens(useTheme());
  const [open, setOpen] = useStateD(reduced);
  useEffectD(() => {
    if (reduced) return;
    const id = setTimeout(() => setOpen(true), delay * 1000);
    return () => clearTimeout(id);
  }, []);
  const row = (i) => (reduced ? {} : {
    opacity: open ? 1 : 0,
    transform: open ? "translateY(0)" : "translateY(14px)",
    filter: open ? "blur(0)" : "blur(8px)",
    transition: `opacity 0.9s ease ${0.25 + i * 0.07}s, transform 1.1s ${SPEC_EASE} ${0.25 + i * 0.07}s, filter 0.9s ease ${0.25 + i * 0.07}s`,
  });
  const rule = (
    <div aria-hidden="true" style={{
      height: 1, background: t.line, transformOrigin: "left center",
      transform: open ? "scaleX(1)" : "scaleX(0)",
      transition: reduced ? "none" : `transform 1s ${SPEC_EASE} 0.1s`,
    }} />
  );
  const brief = { purity: (v) => v.replace(/ \(.*\)$/, ""), type: () => "Lab-grown", shape: (v) => v.replace(/,? Excellent( cut)?$/, "") };
  return (
    <div style={{
      display: "grid", gridTemplateRows: open ? "1fr" : "0fr",
      transition: reduced ? "none" : `grid-template-rows 0.9s ${SPEC_EASE}`,
    }}>
      <div style={{ minHeight: 0, overflow: "hidden" }}>
        <div style={{ paddingTop: compact ? 14 : 26 }}>
          {rule}
          {compact ? (
            <div style={{ ...TYPE.small, color: t.fgMuted, paddingTop: 12, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", ...row(0) }}>
              {rows.map((r) => (brief[r.id] ? brief[r.id](r.value) : r.value)).join("  ·  ")}
            </div>
          ) : (
            <dl style={{ margin: 0, paddingTop: 22, display: "grid", gridTemplateColumns: "1fr 1fr", columnGap: 40, rowGap: 18 }}>
              {rows.map((r, i) => (
                <div key={r.id} style={{ display: "flex", flexDirection: "column", gap: 4, ...row(i) }}>
                  <dt style={{ ...TYPE.small, color: t.fgMuted }}>{r.label}</dt>
                  <dd style={{ ...TYPE.body, fontSize: 26, margin: 0 }}>{r.value}</dd>
                </div>
              ))}
            </dl>
          )}
        </div>
      </div>
    </div>
  );
}

function PurchaseBox({ piece: p, reduced, fromPdp }) {
  const t = themeTokens(useTheme());
  const contentRef = React.useRef(null);
  const [side, setSide] = useStateD(0);
  const [lit, setLit] = useStateD(false);
  const open = !!p.still || lit;

  React.useLayoutEffect(() => {
    setSide(contentRef.current.offsetHeight);
    if (reduced) { setLit(true); return; }
    const id = requestAnimationFrame(() => requestAnimationFrame(() => setLit(true)));
    return () => cancelAnimationFrame(id);
  }, []);

  const ease = "cubic-bezier(.65,0,.35,1)";
  const setPiece = !!p.still;
  return (
    <div style={{ padding: 23, border: `1px solid ${t.line}` }}>
    <div style={{ display: "flex", gap: 36 }}>
      <div style={{
        position: "relative", flexShrink: 0, overflow: "hidden", background: t.bgDeep,
        width: open ? side : 0,
        marginRight: open ? 0 : -36,
        transition: reduced || p.still ? "none" : `width 1.1s ${ease} 0.1s, margin-right 1.1s ${ease} 0.1s`,
      }}>
        {setPiece ? (
          <SquareStill src={p.still} />
        ) : p.sequence ? (
          <div style={{
            position: "absolute", top: 0, left: "50%", width: side, height: "100%", transform: "translateX(-50%)",
            opacity: lit ? 1 : 0, transition: reduced ? "none" : "opacity 1s ease 0.45s",
          }}>
            <ScrubSequence sequence={p.sequence} reduced={reduced} showHint={false} autoTurn />
          </div>
        ) : p.product.video ? (
          <video src={p.product.video} autoPlay={!reduced} loop muted playsInline preload="auto" style={{
            position: "absolute", top: 0, left: "50%", width: side, height: "100%", objectFit: "cover",
            display: "block", transform: "translateX(-50%) scale(1.3)", transformOrigin: "center center",
            opacity: lit ? 1 : 0,
            transition: reduced ? "none" : "opacity 1s ease 0.45s",
          }} />
        ) : (
          <SquareStill src={KCD.packshot(p.product, p.metal, p.caratId).src}
                       style={{ opacity: lit ? 1 : 0, transition: reduced ? "none" : "opacity 1s ease 0.45s" }} />
        )}
      </div>
      <div ref={contentRef} style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column", gap: 20 }}>
        <SummaryTitle product={p.product} name={p.product.name} label={p.label} priceMinor={p.priceMinor} showPrice={p.showPrice} />
        <SummarySelectors metals={p.metals} metal={p.metal} carats={p.carats} caratId={p.caratId} onlySelected />
      </div>
    </div>
    <PurchaseSpecs rows={p.specs} compact={setPiece} reduced={reduced || !fromPdp} delay={0.6} />
    </div>
  );
}

/* One circle for every vend screen. The ring, the piece and the mark
   crossfade in place, so a change of state does not slide the screen. */
function VendCircle({ mode, state, script, reduced }) {
  const t = themeTokens(useTheme());
  const snap = state.snapshot;
  const parts = snap.kind === "set"
    ? KCD.setParts(KCD.SET_BY_ID[snap.setId])
    : [{ product: KCD.PRODUCT_BY_ID[snap.productId], variant: KCD.VARIANT_BY_SKU[snap.vendSku] }];
  const shots = parts.map((x) => KCD.packshot(x.product, x.variant.metal, x.variant.caratId).src);
  const piece = mode === "ring" || mode === "delayed";
  const fade = reduced ? "none" : "opacity 0.7s ease";
  const mark = (shown, children) => (
    <div style={{
      position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", color: t.gold,
      opacity: shown ? 1 : 0, transition: fade, pointerEvents: "none",
    }}>{children}</div>
  );
  return (
    <div style={{ position: "relative", width: 380, height: 380 }}>
      <div style={{ position: "absolute", inset: 0 }}>
        <DispenseRing state={state} script={script} size={380} strong={mode === "delayed"} settled={mode !== "ring"} hidden={mode === "alert"} />
      </div>
      <div style={{ position: "absolute", inset: 56, borderRadius: "50%", overflow: "hidden", background: t.bgDeep }}>
        <div style={{ position: "absolute", inset: 0, display: "flex", opacity: piece ? 1 : 0, transition: fade }}>
          {shots.map((s) => (
            <img key={s} src={s} alt="" draggable={false}
                 style={{ flex: 1, minWidth: 0, height: "100%", objectFit: "contain", padding: 18, display: "block" }} />
          ))}
        </div>
        {mark(mode === "tick", (
          <svg width="132" height="132" viewBox="0 0 24 24" fill="none" aria-hidden="true">
            <path d="M5 12.5 L10 17.5 L19 7" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
          </svg>
        ))}
        {mark(mode === "alert", (
          <svg width="132" height="132" viewBox="0 0 24 24" fill="none" aria-hidden="true">
            <path d="M12 4.5 V14" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
            <circle cx="12" cy="18.2" r="1.15" fill="currentColor" />
          </svg>
        ))}
      </div>
    </div>
  );
}

/* Headline that steps through a few lines, each rising in from a blur
   and lifting away as the next arrives. Holds on the last line, unless
   `loop` is set, in which case it keeps going. */
function CyclingTitle({ lines, size = 68, interval = 3300, reduced, loop = false, calm = false }) {
  const [idx, setIdx] = useStateD(0);
  const [leaving, setLeaving] = useStateD(-1);
  useEffectD(() => {
    if (reduced || (!loop && idx >= lines.length - 1)) return;
    const id = setTimeout(() => {
      setLeaving(idx);
      setIdx((idx + 1) % lines.length);
    }, interval);
    return () => clearTimeout(id);
  }, [idx, reduced, loop, lines.length]);
  return (
    <div style={{ position: "relative", width: 880, height: Math.ceil(size * 1.02) }}>
      {lines.map((line, k) => {
        const entering = k === idx;
        const exiting = k === leaving && k !== idx;
        if (!entering && !exiting) return null;
        return (
          <div key={line} aria-hidden={!entering}
               style={{ position: "absolute", inset: 0, display: "flex", alignItems: "flex-start", justifyContent: "center" }}>
            <Display
              size={size}
              style={{
                whiteSpace: "nowrap",
                animation: reduced ? "none" : entering
                  ? calm && leaving < 0
                    ? "kerai-title-fade 0.8s ease both"
                    : `kerai-line-in 1s cubic-bezier(.22,.7,.2,1) ${leaving < 0 ? 0 : 0.5}s both`
                  : "kerai-line-out 0.6s cubic-bezier(.5,0,.7,.4) forwards",
              }}
            >
              {line}
            </Display>
          </div>
        );
      })}
    </div>
  );
}

const DISPENSE_LINES = [
  "Your jewellery is on its way",
  "Your box is being prepared",
  "Almost ready for you",
];

function SupportSheet({ open, onClose, orderRef }) {
  const t = themeTokens(useTheme());
  return (
    <Sheet open={open} onClose={onClose} eyebrow="Help" title="Talk to us">
      <div style={{ display: "flex", alignItems: "center", gap: 48, paddingBottom: 16 }}>
        <QrCode text={KCD.MACHINE.whatsappUrl} size={320} label="QR code to message Kerai on WhatsApp" />
        <div style={{ display: "flex", flexDirection: "column", gap: 22 }}>
          <Body>Message us on WhatsApp about a piece, your order, a return or an exchange.</Body>
          <Body muted>Point your phone's camera at the code.</Body>
          {orderRef && <div style={{ ...TYPE.small, color: t.fgMuted }}>Your order reference is {orderRef}</div>}
        </div>
      </div>
    </Sheet>
  );
}

/* ------------------------------------------------------------------ */
/* Test outcomes — for demonstrations and acceptance testing only. A   */
/* faint control in the corner opens them; choosing one restarts this  */
/* payment and plays it through, card and all.                         */
/* ------------------------------------------------------------------ */
function OutcomeTrigger({ onOpen }) {
  const t = themeTokens(useTheme());
  return (
    <button onClick={onOpen} aria-label="Test outcomes" style={{
      position: "absolute", right: 16, bottom: 16, width: 64, height: 64, zIndex: 40, padding: 0,
      display: "flex", alignItems: "center", justifyContent: "center", gap: 5,
      background: "transparent", border: `1px solid ${t.line}`, borderRadius: "50%", color: t.fg, opacity: 0.35, cursor: "pointer",
    }}>
      {[0, 1, 2].map((i) => <span key={i} style={{ width: 5, height: 5, borderRadius: "50%", background: "currentColor" }} />)}
    </button>
  );
}

function OutcomeSheet({ open, onClose, activeId, onPick }) {
  const t = themeTokens(useTheme());
  const groups = [
    { id: "payment", title: "Payment" },
    { id: "vend", title: "Dispensing" },
  ];
  return (
    <Sheet open={open} onClose={onClose} eyebrow="Test outcomes" title="Play a payment through" closeLabel="Close">
      <Body muted style={{ marginBottom: 36 }}>Restarts this payment and runs the card for you. For demonstrations only.</Body>
      {groups.map((g) => (
        <div key={g.id} style={{ marginBottom: 40 }}>
          <Eyebrow style={{ color: t.gold, marginBottom: 18 }}>{g.title}</Eyebrow>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}>
            {KCD.OUTCOMES.filter((o) => o.group === g.id).map((o) => {
              const sel = o.id === activeId;
              return (
                <button key={o.id} onClick={() => onPick(o)} aria-pressed={sel} style={{
                  minHeight: 140, padding: "24px 28px", textAlign: "left", cursor: "pointer",
                  display: "flex", flexDirection: "column", justifyContent: "center", gap: 8,
                  background: sel ? t.fg : t.subtle, color: sel ? t.bg : t.fg, border: "none",
                }}>
                  <span style={{ fontFamily: SERIF, fontSize: 34, lineHeight: 1.15 }}>{o.title}</span>
                  <span style={{ ...TYPE.small, opacity: 0.72 }}>{o.detail}</span>
                </button>
              );
            })}
          </div>
        </div>
      ))}
    </Sheet>
  );
}

/* The receipt, handed to the guest's phone. */
function ReceiptQr({ snap }) {
  const [link] = useStateD(() => KCD.receiptLink(snap, Date.now()));
  return <QrCode text={link} size={CIRCLE} label="QR code for your receipt" />;
}

const DELAY_LINES = [
  "Taking a little longer",
  "Still on its way",
  "Nearly with you",
];

const CIRCLE = 380;
const TITLE_SIZE = 60;
// Matches the gap beneath the heading.
const PAY_GAP = 55;
// The heading and gift line sit this far above the purchase box.
const HEAD_ABOVE_BOX = 164;
const HEAD_H = 58;
const CIRCLE_GAP = 80;
const FAILURE_VIEWS = ["failure", "refunding", "refunded", "refund-pending", "auth-cancelled"];

function ScreenPay({ theme, controller, onLeave, outcomeId = null, onOutcome }) {
  const t = themeTokens(theme);
  const reduced = useReducedMotion();
  const state = useCheckoutState(controller);
  const screen = KCD.screenFor(state);
  const snap = state.snapshot;
  const logos = KCD.MACHINE.cardSchemes.filter((s) => s.enabled && s.asset);
  // PIN, card removal and an unknown result stay on the two payment
  // lines. The terminal below the screen carries those prompts.
  const raw = screen === "insert" || screen === "pin" ? "ready"
    : screen === "checking" ? "processing"
    : screen === "remove" ? state.phase
    : screen === "thanks" || (screen === "closed" && state.collected) ? "thanks"
    : screen;
  const view = FAILURE_VIEWS.includes(raw) ? "failure" : raw;
  const isSet = snap.kind === "set";
  const [sequence] = useStateD(() => {
    const s = window.__keraiPaySequence || null;
    window.__keraiPaySequence = null;
    return s;
  });
  const pieces = React.useMemo(
    () => purchasePieces(snap).map((p) => (sequence && !isSet ? { ...p, sequence } : p)),
    [snap, sequence]
  );
  const boxTop = isSet ? PAY_BOX.setTop : PAY_BOX.top;
  const [supportOpen, setSupportOpen] = useStateD(false);
  const [outcomesOpen, setOutcomesOpen] = useStateD(false);
  const [fromPdp] = useStateD(() => {
    const v = !!window.__keraiPayFromPdp;
    window.__keraiPayFromPdp = false;
    return v;
  });

  // What each state shows: the product tile while paying, or the circle
  // in its place once the machine is vending; the headline, supporting
  // lines, a cue beneath them, and the actions that are safe at that moment.
  let circle = null;
  let title = "";
  let lines = [];
  let cue = null;
  let actions = null;
  const leave = () => (onLeave ? onLeave() : controller.close());
  const support = (doneLabel) => (
    <div style={{ display: "flex", gap: 18 }}>
      <div style={{ flex: 1 }}>
        <PrimaryButton variant="ghost" onClick={() => setSupportOpen(true)}><WhatsAppGlyph />Talk to us</PrimaryButton>
      </div>
      <div style={{ flex: 1 }}>
        <PrimaryButton onClick={leave}>{doneLabel}</PrimaryButton>
      </div>
    </div>
  );
  const refLine = `Order reference ${snap.ref}`;

  switch (view) {
    case "ready":
      title = "Please insert your card below";
      // A set's two boxes leave room for the cue but not this line.
      lines = isSet ? [] : ["Follow the instructions on the payment device"];
      cue = <CardCue reduced={reduced} />;
      if (state.phase === "ready") {
        actions = <PrimaryButton variant="ghost" onClick={() => controller.cancel()}>Cancel</PrimaryButton>;
      }
      break;
    case "processing":
      title = "Processing your payment";
      cue = <PayLoader reduced={reduced} />;
      break;
    case "declined":
      circle = "alert";
      title = "Your payment didn't go through";
      lines = ["You haven't been charged"];
      actions = (
        <>
          <PrimaryButton onClick={() => controller.retry()}>Try another card</PrimaryButton>
          <PrimaryButton variant="ghost" onClick={() => controller.close()}>Cancel</PrimaryButton>
        </>
      );
      break;
    case "dispensing":
      circle = "ring";
      title = <CyclingTitle lines={DISPENSE_LINES} size={TITLE_SIZE} reduced={reduced} calm />;
      break;
    case "delayed":
      circle = "delayed";
      title = <CyclingTitle lines={DELAY_LINES} size={TITLE_SIZE} reduced={reduced} loop calm />;
      break;
    case "collect":
      circle = "tick";
      title = "Collect your item below";
      cue = <CardCue reduced={reduced} />;
      break;
    case "thanks":
      circle = "receipt";
      title = "Scan for your receipt";
      lines = ["Add your email on your phone and we'll send it to you.", "Unworn pieces can be returned or exchanged within 30 days."];
      actions = support("Done");
      break;
    case "failure":
      circle = "alert";
      title = `We couldn't dispense your ${isSet ? "set" : "piece"}`;
      if (state.phase === "refund-pending") {
        lines = ["Your refund hasn't gone through yet. Please message us and we'll sort it.", refLine];
      } else if (state.phase === "auth-cancelled") {
        lines = ["The payment has been cancelled, so you haven't been charged.", refLine];
      } else {
        lines = ["We're refunding you in full. It can take up to 5 working days to show.", refLine];
      }
      actions = support("Return home");
      break;
    default:
      break;
  }
  const showTile = !circle;
  const heading = {
    declined: "Let's try again",
    dispensing: "Payment approved",
    delayed: "Payment approved",
    collect: "Ready for you",
    thanks: "Thank you",
    failure: "Our apologies",
  }[view] || "Your purchase";

  // The circle keeps its last face while it fades away, so going back to
  // the tile (another card after a decline) doesn't flash a new state.
  const lastCircle = React.useRef(circle);
  if (circle) lastCircle.current = circle;

  // The headline sits as far below the tile as the heading sits above
  // it, and the circle is centred on the tile, so neither moves when the
  // tile gives way to the circle.
  const stackRef = React.useRef(null);
  const [stackH, setStackH] = useStateD(0);
  React.useLayoutEffect(() => {
    const el = stackRef.current;
    const measure = () => setStackH(el.offsetHeight);
    measure();
    const ro = new ResizeObserver(measure);
    ro.observe(el);
    return () => ro.disconnect();
  }, []);
  const [glide, setGlide] = useStateD(false);
  useEffectD(() => {
    const id = requestAnimationFrame(() => setGlide(true));
    return () => cancelAnimationFrame(id);
  }, []);
  // Once the tile gives way, the circle sits a fixed step under the
  // heading and the headline closes up beneath it, however tall the tile was.
  // A set's heading starts higher to fit two boxes; once vending, both
  // settle in the same place.
  const vendHeadTop = PAY_BOX.top - HEAD_ABOVE_BOX;
  const headTop = circle ? vendHeadTop : boxTop - HEAD_ABOVE_BOX;
  const circleTop = vendHeadTop + HEAD_H + CIRCLE_GAP;
  const payTextTop = boxTop + stackH + PAY_GAP;
  const textTop = circle ? Math.min(payTextTop, circleTop + CIRCLE + 50) : payTextTop;
  const swap = "cubic-bezier(.22,.7,.2,1)";

  return (
    <ScreenFrame theme={theme}>
      <style>{`
        @keyframes kerai-pay-spin { to { transform: rotate(360deg); } }
        @keyframes kerai-pay-pulse { 0%, 100% { opacity: 0.45; } 50% { opacity: 1; } }
        @keyframes kerai-ring-pulse { 0%, 100% { opacity: 0.45; } 50% { opacity: 1; } }
        @keyframes kerai-ring-pulse-strong {
          0%, 100% { opacity: 0.15; stroke-width: 3px; }
          50% { opacity: 1; stroke-width: 11px; }
        }
        @keyframes kerai-pay-in {
          0%   { opacity: 0; transform: translateY(18px); filter: blur(10px); }
          100% { opacity: 1; transform: translateY(0); filter: blur(0); }
        }
        @keyframes kerai-line-in {
          0%   { opacity: 0; transform: translateY(48px); filter: blur(16px); }
          60%  { filter: blur(0); }
          100% { opacity: 1; transform: translateY(0); filter: blur(0); }
        }
        @keyframes kerai-cue-bob { 0%, 100% { transform: translateY(-4px); } 50% { transform: translateY(6px); } }
        @keyframes kerai-pay-drop {
          0%   { opacity: 0; transform: translateY(-18px); filter: blur(10px); }
          100% { opacity: 1; transform: translateY(0); filter: blur(0); }
        }
        @keyframes kerai-line-out {
          0%   { opacity: 1; transform: translateY(0); filter: blur(0); }
          100% { opacity: 0; transform: translateY(-48px); filter: blur(14px); }
        }
        @keyframes kerai-title-fade {
          0%   { opacity: 0; filter: blur(8px); }
          100% { opacity: 1; filter: blur(0); }
        }
      `}</style>

      <KeraiGlassHeader theme={theme} />
      <div style={{ position: "absolute", top: KERAI_HEAD_H, left: 0, right: 0, bottom: 0, background: t.bg }} />

      <div style={{
        position: "absolute", top: headTop, left: 0, right: 0, textAlign: "center",
        transition: reduced || !glide ? "none" : `top 0.9s ${swap}`,
        animation: reduced ? "none" : "kerai-pay-drop 0.8s cubic-bezier(.22,.7,.2,1) 0.25s both",
      }}>
        <div key={heading} style={{ animation: reduced ? "none" : "kerai-title-fade 0.5s ease both" }}>
          <Display size={56}>{heading}</Display>
        </div>
        <GiftLine style={{
          marginTop: 22,
          opacity: heading === "Your purchase" ? 1 : 0,
          filter: heading === "Your purchase" ? "blur(0)" : "blur(8px)",
          transition: reduced ? "none" : "opacity 0.5s ease, filter 0.5s ease",
        }} />
      </div>

      <div ref={stackRef} aria-hidden={!showTile} style={{
        position: "absolute", top: boxTop, left: 48, right: 48,
        display: "flex", flexDirection: "column", gap: PAY_BOX.setGap,
        opacity: showTile ? 1 : 0,
        transform: showTile ? "none" : "translateY(-24px) scale(0.97)",
        filter: showTile ? "blur(0)" : "blur(10px)",
        pointerEvents: showTile ? "auto" : "none",
        transition: reduced ? "none" : showTile
          ? `opacity 0.7s ease 0.2s, transform 0.9s ${swap} 0.2s, filter 0.7s ease 0.2s`
          : `opacity 0.5s ease, transform 0.7s ${swap}, filter 0.5s ease`,
      }}>
        {pieces.map((piece) => <PurchaseBox key={piece.key} piece={piece} reduced={reduced} fromPdp={fromPdp} />)}
        {isSet && (
          <div style={{
            margin: `${36 - PAY_BOX.setGap}px 24px 0`,
            animation: reduced || !fromPdp ? "none" : "kerai-pay-in 0.9s cubic-bezier(.22,.7,.2,1) 0.5s both",
          }}>
            <SetTotal set={KCD.SET_BY_ID[snap.setId]} />
          </div>
        )}
      </div>

      <div aria-hidden={!circle} style={{
        position: "absolute", top: circleTop, left: (1080 - CIRCLE) / 2, width: CIRCLE, height: CIRCLE,
        opacity: circle ? 1 : 0,
        transform: circle ? "scale(1)" : "scale(0.92)",
        filter: circle ? "blur(0)" : "blur(8px)",
        transition: reduced ? "none" : circle
          ? `opacity 0.7s ease 0.2s, transform 0.9s ${swap} 0.2s, filter 0.7s ease 0.2s`
          : `opacity 0.5s ease, transform 0.7s ${swap}, filter 0.5s ease`,
      }}>
        {lastCircle.current === "receipt" ? (
          <ReceiptQr snap={snap} />
        ) : lastCircle.current && (
          <VendCircle mode={lastCircle.current} state={state} script={controller.script} reduced={reduced} />
        )}
      </div>

      <div
        role="status"
        aria-live="polite"
        style={{
          position: "absolute", top: textTop, left: 0, right: 0,
          display: "flex", flexDirection: "column", alignItems: "center", textAlign: "center",
          transition: reduced || !glide ? "none" : `top 0.9s ${swap}`,
          animation: reduced || !fromPdp ? "none" : "kerai-pay-in 0.9s cubic-bezier(.22,.7,.2,1) 0.7s both",
        }}
      >
        <div key={view} style={{
          display: "flex", flexDirection: "column", alignItems: "center",
          animation: reduced ? "none" : "kerai-title-fade 0.5s ease both",
        }}>
          {typeof title === "string"
            ? <Display size={TITLE_SIZE} style={{ maxWidth: 880, textWrap: "balance" }}>{title}</Display>
            : title}
          {lines.length > 0 && (
            <div style={{ marginTop: 36 }}>
              {lines.map((l) => <Body key={l} size={28} muted>{l}</Body>)}
            </div>
          )}
          {cue && <div style={{ marginTop: isSet ? 36 : 56 }}>{cue}</div>}
          {view === "ready" && logos.length > 0 && (
            <div style={{ marginTop: 44, display: "flex", gap: 28, alignItems: "center" }}>
              {logos.map((l) => <img key={l.id} src={l.asset} alt={l.name} style={{ height: 40, display: "block" }} />)}
            </div>
          )}
        </div>
      </div>

      {actions && (
        <div style={{
          position: "absolute", left: 72, right: 72, bottom: 72, display: "flex", flexDirection: "column", gap: 18,
        }}>
          {actions}
        </div>
      )}

      {onOutcome && <OutcomeTrigger onOpen={() => setOutcomesOpen(true)} />}
      <SupportSheet open={supportOpen} onClose={() => setSupportOpen(false)} orderRef={state.payment === "approved" ? snap.ref : null} />
      {onOutcome && (
        <OutcomeSheet open={outcomesOpen} onClose={() => setOutcomesOpen(false)} activeId={outcomeId}
                      onPick={(o) => { setOutcomesOpen(false); onOutcome(o); }} />
      )}
    </ScreenFrame>
  );
}

window.ScreenPay = ScreenPay;
