/* global React, ScreenFrame, Display, Eyebrow, Body, PrimaryButton, Hairline, HeroImage, themeTokens, useTheme, useReducedMotion, PALETTE, KeraiMark, TYPE, SERIF, Choice, Sheet, QrCode */
const { useState: useStateB, useEffect: useEffectB, useRef: useRefB, useLayoutEffect: useLayoutEffectB } = React;

const KC = window.KeraiCommerce;
const GRID_EASE = "cubic-bezier(.22,.7,.2,1)";
const SIDE_B = 72;

/* ------------------------------------------------------------------ */
/* Names                                                               */
/* ------------------------------------------------------------------ */
const productLabel = (product) => {
  const id = product.label || product.box;
  return id.charAt(0).toUpperCase() + id.slice(1);
};
const itemType = (product) => productLabel(product).toLowerCase();
const TYPE_NAME = { bracelets: "Bracelet", earrings: "Earrings", pendants: "Pendant" };
const typeName = (product) => TYPE_NAME[itemType(product)] || productLabel(product);
const NOUN = { bracelets: "bracelet", earrings: "earrings", pendants: "pendant" };
const pieceNoun = (product) => NOUN[itemType(product)] || "piece";
// "Round Halo earrings", "Emerald pendant".
const pieceTitle = (product) => `${product.name} ${pieceNoun(product)}`;
const setTypeName = (parts) => parts.map(({ product }) => pieceNoun(product)).join(" + ");

const lowestPrice = (item, stock) => {
  const prices = KC.MACHINE.metals.map((m) => KC.fromPrice(item, m, stock)).filter((x) => x != null);
  return prices.length ? Math.min(...prices) : null;
};

/* ------------------------------------------------------------------ */
/* Grid card                                                           */
/* Square image; beneath it the type and gold colours, then the name   */
/* and price. The slot code matches the label in the cabinet. A piece  */
/* with nothing left stays in its place, greyed, and can't be opened.  */
/* ------------------------------------------------------------------ */
function GridCard({ theme, index, settle, onClick, soldOut = false, ariaLabel, media, code = null, name, type, price, metals = [], activeMetal = null }) {
  const t = themeTokens(theme);
  const [pressed, setPressed] = useStateB(false);
  return (
    <div style={{
      opacity: settle ? 1 : 0,
      animation: settle ? "none" : `kerai-grid-in 0.9s ${GRID_EASE} ${0.12 + Math.min(index, 8) * 0.06}s forwards`,
    }}>
      <button
        onClick={soldOut ? undefined : onClick}
        disabled={soldOut}
        onPointerDown={() => !soldOut && setPressed(true)}
        onPointerUp={() => setPressed(false)}
        onPointerLeave={() => setPressed(false)}
        onPointerCancel={() => setPressed(false)}
        aria-label={ariaLabel}
        style={{
          display: "block", width: "100%", padding: 0, border: "none",
          background: t.bgSoft, color: t.fg, textAlign: "left",
          cursor: soldOut ? "default" : "pointer",
          transform: pressed ? "scale(0.985)" : "scale(1)",
          transition: "transform 0.25s " + GRID_EASE,
        }}
      >
        <div style={{ position: "relative", width: "100%", aspectRatio: "1 / 1", background: t.bgDeep, overflow: "hidden" }}>
          <div style={{ position: "absolute", inset: 0, opacity: soldOut ? 0.32 : 1, filter: soldOut ? "grayscale(1)" : "none" }}>{media}</div>
          {code && (
            <span aria-hidden="true" style={{
              position: "absolute", top: 20, left: 20, minWidth: 72, height: 72, padding: "0 12px", boxSizing: "border-box",
              display: "flex", alignItems: "center", justifyContent: "center",
              background: t.bg, color: t.fg,
              fontFamily: SERIF, fontStyle: "italic", fontSize: 32, lineHeight: 1,
            }}>
              {code}
            </span>
          )}
          {soldOut && (
            <span style={{
              position: "absolute", left: "50%", top: "50%", transform: "translate(-50%, -50%)",
              padding: "18px 28px", background: t.bg, color: t.fg, ...TYPE.label, whiteSpace: "nowrap",
            }}>
              Sold out
            </span>
          )}
        </div>
        <div style={{ padding: "26px 28px 30px", opacity: soldOut ? 0.5 : 1 }}>
          <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 16 }}>
            <span style={{ ...TYPE.label, color: t.gold, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
              {type}
            </span>
            <div style={{ display: "flex", gap: 10, flexShrink: 0 }} aria-hidden="true">
              {metals.map((m) => (
                <span key={m.id} style={{
                  width: 22, height: 22, borderRadius: "50%", background: m.swatch,
                  boxShadow: m.id === activeMetal
                    ? `inset 0 0 0 1px ${t.swatchEdge}, 0 0 0 3px ${t.bgSoft}, 0 0 0 4.5px ${t.fg}`
                    : `inset 0 0 0 1px ${t.swatchEdge}`,
                  opacity: m.available === false ? 0.35 : 1,
                }} />
              ))}
            </div>
          </div>
          <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 16, marginTop: 14, minHeight: 44 }}>
            <Display size={38} as="h3" style={{ flex: 1, minWidth: 0, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
              {name}
            </Display>
            {price && (
              <span style={{ flexShrink: 0, whiteSpace: "nowrap" }}>
                {price.from && <span style={{ ...TYPE.small, color: t.fgMuted, marginRight: 10 }}>from</span>}
                <span style={{ fontFamily: SERIF, fontSize: 32 }}>{price.text}</span>
              </span>
            )}
          </div>
        </div>
      </button>
    </div>
  );
}

const coverShot = (src) => (
  <img src={src} alt="" draggable={false} style={{
    position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "contain",
    display: "block", userSelect: "none", WebkitUserDrag: "none",
  }} />
);

function ProductTile({ item, index, metal, stock, theme, onPick, settle }) {
  const from = metal ? KC.fromPrice(item, metal, stock) : lowestPrice(item, stock);
  const soldOut = from == null;
  const metals = KC.metalOptions(item, stock);
  const shown = metal || (metals.find((m) => m.available) || metals[0] || {}).id;
  return (
    <GridCard
      theme={theme} index={index} settle={settle}
      soldOut={soldOut}
      onClick={() => onPick(item, shown)}
      ariaLabel={soldOut ? `${pieceTitle(item)}, sold out` : `${pieceTitle(item)}, from ${KC.formatMoney(from)}`}
      media={coverShot(KC.packshot(item, shown, null).src)}
      code={KC.slotCode(item.id)}
      name={item.name}
      type={typeName(item)}
      price={soldOut ? null : { text: KC.formatMoney(from), from: item.carats.length > 1 }}
      metals={metals}
      activeMetal={shown}
    />
  );
}

function SetTile({ set, index, stock, theme, onPick, settle }) {
  const soldOut = !KC.isAvailable(set.sku, stock);
  const parts = KC.setParts(set);
  const metals = KC.MACHINE.metals
    .map((id) => {
      const v = KC.SETS.find((x) => x.name === set.name && x.metal === id);
      return v && { ...KC.METALS[id], available: KC.isAvailable(v.sku, stock) };
    })
    .filter(Boolean);
  return (
    <GridCard
      theme={theme} index={index} settle={settle}
      soldOut={soldOut}
      onClick={() => onPick(set)}
      ariaLabel={soldOut ? `${set.name}, sold out` : `${set.name}, ${KC.formatMoney(set.priceMinor)}`}
      media={parts.map(({ variant, still }, i) => (
        <img key={variant.sku} src={still} alt="" draggable={false} style={{
          position: "absolute", top: 0, bottom: 0, left: `${(i * 100) / parts.length}%`, width: `${100 / parts.length}%`, height: "100%",
          objectFit: "contain", display: "block", userSelect: "none", WebkitUserDrag: "none",
        }} />
      ))}
      name={set.name}
      type={setTypeName(parts)}
      price={soldOut ? null : { text: KC.formatMoney(set.priceMinor) }}
      metals={metals}
      activeMetal={set.metal}
    />
  );
}

/* ------------------------------------------------------------------ */
/* Header and footer glass                                             */
/* ------------------------------------------------------------------ */
const KERAI_HEAD_H = 230;
const GLASS_BLUR = "blur(22px) saturate(1.1)";

function KeraiGlassHeader({ theme, onLogo, edge = 0 }) {
  const t = themeTokens(theme);
  return (
    <div style={{
      position: "absolute", top: 0, left: 0, right: 0, height: KERAI_HEAD_H,
      display: "flex", alignItems: "center", justifyContent: "center",
      background: t.glass, backdropFilter: GLASS_BLUR, WebkitBackdropFilter: GLASS_BLUR, zIndex: 10,
    }}>
      <div onClick={onLogo} style={{ cursor: onLogo ? "pointer" : "default" }}>
        <KeraiMark size={80} color={t.fg} />
      </div>
      <div aria-hidden="true" style={{
        position: "absolute", left: 0, right: 0, bottom: 0, height: 1, background: t.line,
        opacity: edge, transition: "opacity 0.25s linear", pointerEvents: "none",
      }} />
    </div>
  );
}

/* Square control the height of a choice. */
function SquareButton({ label, onClick, pressed, children }) {
  const t = themeTokens(useTheme());
  return (
    <button
      onClick={onClick}
      aria-label={label}
      aria-pressed={pressed}
      style={{
        width: 88, height: 88, flexShrink: 0, padding: 0,
        display: "flex", alignItems: "center", justifyContent: "center",
        background: t.subtle,
        color: t.fg,
        border: "none", cursor: "pointer",
        boxShadow: pressed ? `inset 0 0 0 2px ${PALETTE.orange}` : "inset 0 0 0 2px transparent",
        transition: "box-shadow 0.3s",
      }}
    >
      {children}
    </button>
  );
}

const BackArrow = (
  <svg width="30" height="30" viewBox="0 0 24 24" fill="none" aria-hidden="true">
    <path d="M19 12 H5 M11 6 L5 12 L11 18" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
  </svg>
);

function WheelchairIcon({ size = 32 }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <circle cx="11" cy="3.6" r="1.7" fill="currentColor" />
      <path d="M11 6.5 V12.5 H16 L18.5 18.5 H20.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
      <path d="M11 9.5 H15" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
      <path d="M8 10.2 A5.6 5.6 0 1 0 15.2 17.6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
    </svg>
  );
}

/* Grid and footer ease off to the left before the next screen arrives;
   coming back from a product, the grid eases in from the left. */
function useGridExit(gridRef, extraRefs = []) {
  const leavingRef = useRefB(false);
  const enterFromLeft = useRefB(window.__keraiEnterFrom === "left");

  useLayoutEffectB(() => {
    window.__keraiEnterFrom = null;
    const grid = gridRef.current;
    if (!enterFromLeft.current || !grid) return;
    grid.animate(
      [
        { transform: "translateX(-140px)", opacity: 0, filter: "blur(14px)" },
        { transform: "translateX(0)", opacity: 1, filter: "blur(0px)" },
      ],
      { duration: 980, easing: GRID_EASE, fill: "both" }
    );
  }, []);

  const leave = async (then) => {
    if (leavingRef.current) return;
    leavingRef.current = true;
    const els = [gridRef, ...extraRefs].map((r) => r.current).filter(Boolean);
    const pending = els.map((el) => {
      el.style.animation = "none";
      el.style.transform = "translateY(0)";
      return el.animate(
        [
          { transform: "translateX(0)", opacity: 1, filter: "blur(0px)" },
          { transform: "translateX(-160px)", opacity: 0, filter: "blur(14px)" },
        ],
        { duration: 820, easing: GRID_EASE, fill: "forwards" }
      ).finished;
    });
    try { await Promise.all(pending); } catch {}
    then();
  };

  return { leave, settle: enterFromLeft.current };
}

const GRID_KEYFRAMES = `
  @keyframes kerai-grid-in {
    0%   { opacity: 0; transform: translateY(56px); filter: blur(14px); }
    55%  { filter: blur(0); }
    100% { opacity: 1; transform: translateY(0);    filter: blur(0); }
  }
  @keyframes kerai-shelf-up { to { transform: translateY(0); } }
`;

/* ------------------------------------------------------------------ */
/* 03. BROWSE — every piece and set, filtered from the shelf below     */
/* Three rows of choices, all within reach: which box, what kind of    */
/* piece, which gold colour. The reach button lowers the grid for a    */
/* seated guest.                                                       */
/* ------------------------------------------------------------------ */
const CATEGORIES = [
  { id: "all", name: "All" },
  { id: "bracelets", name: "Bracelets" },
  { id: "earrings", name: "Earrings" },
  { id: "pendants", name: "Pendants" },
  { id: "sets", name: "Sets" },
];
const SHELF_ROW = 88;
const SHELF_GAP = 14;
const SHELF_H = 28 + SHELF_ROW + 72;
const REACH_TOP = 760;

const CloseIcon = (
  <svg width="30" height="30" viewBox="0 0 24 24" fill="none" aria-hidden="true">
    <path d="M6 6 L18 18 M18 6 L6 18" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
  </svg>
);
const FiltersIcon = (
  <svg width="30" height="30" viewBox="0 0 24 24" fill="none" aria-hidden="true">
    <path d="M4 7 H20 M4 17 H20" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
    <circle cx="9" cy="7" r="2.4" fill="currentColor" />
    <circle cx="15" cy="17" r="2.4" fill="currentColor" />
  </svg>
);

// One tile per set; with no gold colour chosen, whichever is in stock.
function setsFor(metal, box, stock) {
  const byName = {};
  KC.SETS.forEach((s) => {
    if (metal && s.metal !== metal) return;
    if (box && !KC.setParts(s).some(({ product }) => KC.boxOf(product.id) === box)) return;
    const cur = byName[s.name];
    if (!cur || (!KC.isAvailable(cur.sku, stock) && KC.isAvailable(s.sku, stock))) byName[s.name] = s;
  });
  return Object.values(byName);
}

function Swatch({ swatch, size = 26 }) {
  const t = themeTokens(useTheme());
  return (
    <span aria-hidden="true" style={{
      width: size, height: size, borderRadius: "50%", flexShrink: 0,
      background: swatch, boxShadow: `inset 0 0 0 1px ${t.swatchEdge}`,
    }} />
  );
}

function ScreenBrowse({ theme, box = null, category = "all", metal = null, stock, onChangeBox, onChangeCategory, onChangeMetal, onPick, onPickSet, onBack, onHome, onLogo }) {
  const t = themeTokens(theme);
  const gridRef = useRefB(null);
  const shelfRef = useRefB(null);
  const { leave, settle } = useGridExit(gridRef, [shelfRef]);
  const [reach, setReach] = useStateB(!!window.__keraiReach);
  useEffectB(() => { window.__keraiReach = reach; }, [reach]);
  const [filtersOpen, setFiltersOpen] = useStateB(false);
  const activeFilters = (box ? 1 : 0) + (category !== "all" ? 1 : 0) + (metal ? 1 : 0);

  const [edge, setEdge] = useStateB(0);
  const [footEdge, setFootEdge] = useStateB(0);
  const measureEdges = (el) => {
    const below = el.scrollHeight - el.clientHeight - el.scrollTop;
    setEdge(Math.max(0, Math.min(1, (el.scrollTop - 12) / 30)));
    setFootEdge(Math.max(0, Math.min(1, (below - 24) / 30)));
  };
  useEffectB(() => {
    const el = gridRef.current;
    if (!el) return;
    el.scrollTop = 0;
    measureEdges(el);
  }, [box, category, metal, reach]);

  const products = category === "sets" ? [] : KC.PRODUCTS
    .filter((p) => (!metal || p.metals.includes(metal))
      && (category === "all" || itemType(p) === category)
      && (!box || KC.boxOf(p.id) === box))
    .sort((a, b) => (KC.shelfOrder(a.id) < KC.shelfOrder(b.id) ? -1 : 1));
  const sets = category === "all" || category === "sets" ? setsFor(metal, box, stock) : [];
  const entries = [
    ...products.map((p) => ({ key: p.id, product: p })),
    ...sets.map((s) => ({ key: s.id, set: s })),
  ];

  const boxInfo = box ? KC.BOX_BY_ID[box] : null;
  const catName = (CATEGORIES.find((c) => c.id === category) || CATEGORIES[0]).name;
  const heading = boxInfo ? `Box ${box}` : category === "sets" ? "Our sets" : "All products";
  const subheading = boxInfo ? boxInfo.name : `${entries.length} ${entries.length === 1 ? "piece" : "pieces"}`;
  const emptyLine = `No ${category === "all" ? "pieces" : catName.toLowerCase()}${box ? ` in Box ${box}` : ""}${metal ? ` in ${KC.METALS[metal].name.toLowerCase()}` : ""}.`;

  const reachEase = `0.8s ${GRID_EASE}`;
  const row = { display: "flex", gap: SHELF_GAP, height: SHELF_ROW };
  const even = { flex: "1 1 0", padding: "0 10px" };

  return (
    <ScreenFrame theme={theme} onBack={onBack} onHome={onHome}>
      <div
        ref={gridRef}
        onScroll={(e) => measureEdges(e.currentTarget)}
        style={{
          position: "absolute", top: reach ? REACH_TOP : 0, left: 0, right: 0, bottom: 0,
          overflowY: "auto", overflowX: "hidden", WebkitOverflowScrolling: "touch", scrollbarWidth: "none",
          padding: `${reach ? 32 : KERAI_HEAD_H + 8}px ${SIDE_B}px ${SHELF_H + 28}px`,
          boxSizing: "border-box",
          transition: `top ${reachEase}, padding-top ${reachEase}`,
        }}
      >
        <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 24, marginBottom: 32 }}>
          <Display size={56} as="h2">{heading}</Display>
          <span style={{ ...TYPE.small, color: t.fgMuted, whiteSpace: "nowrap" }}>{subheading}</span>
        </div>
        {entries.length ? (
          <div style={{ display: "grid", gridTemplateColumns: "minmax(0, 1fr) minmax(0, 1fr)", gap: 28 }}>
            {entries.map((e, i) => e.set
              ? <SetTile key={e.key} set={e.set} index={i} stock={stock} theme={theme} settle={settle} onPick={(s) => leave(() => onPickSet(s))} />
              : <ProductTile key={e.key} item={e.product} index={i} metal={metal} stock={stock} theme={theme} settle={settle} onPick={(p, m) => leave(() => onPick(p, m))} />
            )}
          </div>
        ) : (
          <Body muted style={{ paddingTop: 40 }}>{emptyLine}</Body>
        )}
      </div>

      <div aria-hidden="true" style={{
        position: "absolute", top: REACH_TOP, left: 0, right: 0, height: 1, background: t.line,
        opacity: reach ? edge : 0, transition: "opacity 0.25s linear", pointerEvents: "none",
      }} />

      <KeraiGlassHeader theme={theme} onLogo={onLogo} edge={reach ? 0 : edge} />

      {/* Filters rise from behind the footer and dim everything above it. */}
      <div onClick={() => setFiltersOpen(false)} aria-hidden="true" style={{
        position: "absolute", inset: 0, zIndex: 11, background: "rgba(12,8,6,0.66)",
        opacity: filtersOpen ? 1 : 0, pointerEvents: filtersOpen ? "auto" : "none",
        transition: `opacity 0.5s ease`,
      }} />
      <div role="dialog" aria-label="Filters" aria-hidden={!filtersOpen} style={{
        position: "absolute", left: 0, right: 0, bottom: SHELF_H, zIndex: 12,
        padding: `36px ${SIDE_B}px 40px`, boxSizing: "border-box",
        background: t.bg, borderTop: `1px solid ${t.line}`,
        display: "flex", flexDirection: "column", gap: 28,
        transform: filtersOpen ? "translateY(0)" : `translateY(calc(100% + ${SHELF_H}px))`,
        visibility: filtersOpen ? "visible" : "hidden",
        transition: filtersOpen
          ? `transform 0.6s ${GRID_EASE}, visibility 0s`
          : `transform 0.5s ${GRID_EASE}, visibility 0s 0.5s`,
      }}>
        <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
          <Display size={48} as="h2">Filters</Display>
          <SquareButton label="Close filters" onClick={() => setFiltersOpen(false)}>{CloseIcon}</SquareButton>
        </div>
        {[
          ["Box", (
            <>
              <Choice selected={!box} onClick={() => onChangeBox(null)} style={even}>All boxes</Choice>
              {KC.BOXES.map((b) => (
                <Choice key={b.id} selected={box === b.id} onClick={() => onChangeBox(b.id)} style={even}>Box {b.id}</Choice>
              ))}
            </>
          )],
          ["Type", CATEGORIES.map((c) => (
            <Choice key={c.id} selected={category === c.id} onClick={() => onChangeCategory(c.id)} style={even}>{c.name}</Choice>
          ))],
          ["Gold colour", (
            <>
              <Choice selected={!metal} onClick={() => onChangeMetal(null)} style={even}>All gold colours</Choice>
              {KC.MACHINE.metals.map((id) => (
                <Choice key={id} selected={metal === id} onClick={() => onChangeMetal(id)} style={even}>
                  <Swatch swatch={KC.METALS[id].swatch} />
                  {KC.METALS[id].name}
                </Choice>
              ))}
            </>
          )],
        ].map(([label, choices]) => (
          <div key={label} role="group" aria-label={label}>
            <Eyebrow style={{ color: t.fgMuted, marginBottom: 14 }}>{label}</Eyebrow>
            <div style={row}>{choices}</div>
          </div>
        ))}
      </div>

      <div
        ref={shelfRef}
        style={{
          position: "absolute", bottom: 0, left: 0, right: 0, height: SHELF_H,
          padding: `28px ${SIDE_B}px 72px`, boxSizing: "border-box",
          background: filtersOpen ? t.bg : t.glassDense,
          backdropFilter: "blur(30px) saturate(1.1)", WebkitBackdropFilter: "blur(30px) saturate(1.1)",
          zIndex: 13, display: "flex", gap: SHELF_GAP,
          transform: "translateY(100%)",
          animation: `kerai-shelf-up 0.7s ${GRID_EASE} 0.15s forwards`,
          transition: "background 0.4s ease",
        }}
      >
        <div aria-hidden="true" style={{
          position: "absolute", top: 0, left: 0, right: 0, height: 1, background: t.line,
          opacity: filtersOpen ? 1 : footEdge, transition: "opacity 0.25s linear", pointerEvents: "none",
        }} />
        <SquareButton label="Back" onClick={onBack}>{BackArrow}</SquareButton>
        <div style={{ flex: 1 }} />
        <button
          onClick={() => setFiltersOpen((o) => !o)}
          aria-expanded={filtersOpen}
          style={{
            height: SHELF_ROW, padding: "0 44px", flexShrink: 0,
            display: "inline-flex", alignItems: "center", gap: 16,
            background: t.fg, color: t.bg, border: "none", cursor: "pointer",
            ...TYPE.label,
          }}
        >
          {FiltersIcon}
          {activeFilters ? `Filters · ${activeFilters}` : "Filters"}
        </button>
        <SquareButton label={reach ? "Raise the products" : "Lower the products within reach"} pressed={reach} onClick={() => setReach((r) => !r)}>
          <WheelchairIcon />
        </SquareButton>
      </div>

      <style>{GRID_KEYFRAMES}</style>
    </ScreenFrame>
  );
}

/* ------------------------------------------------------------------ */
/* 04. PRODUCT DETAIL                                                  */
/* Photography; the name and price; gold colour and diamond size; the  */
/* details bar, which opens everything else; then the gift line and    */
/* the way to pay.                                                     */
/* ------------------------------------------------------------------ */
// Where the purchase box sits on the payment screen, in kiosk pixels.
const KERAI_PAY_BOX = { top: 560, setTop: 400, setGap: 16 };
window.KERAI_PAY_BOX = KERAI_PAY_BOX;
const DETAIL_EASE_IN = "cubic-bezier(.22,.7,.2,1)";
const DETAIL_EASE_OUT = "cubic-bezier(.5,0,.7,.4)";
const FOLD = "cubic-bezier(.65,0,.35,1)";
const slideIn = (delay) => `kerai-slide-in 0.95s ${DETAIL_EASE_IN} ${delay}s both`;
const DETAIL_KEYFRAMES = `
  @keyframes kerai-slide-in {
    0%   { opacity: 0; transform: translateX(48px); filter: blur(12px); }
    55%  { filter: blur(0); }
    100% { opacity: 1; transform: translateX(0); filter: blur(0); }
  }
  @keyframes kerai-media-in { from { opacity: 0; } to { opacity: 1; } }
`;

function MediaTile({ children, style = {}, dark }) {
  const t = themeTokens(useTheme());
  return (
    <div style={{ position: "relative", overflow: "hidden", background: dark ? t.bgDeep : t.bgSoft, ...style }}>
      {children}
    </div>
  );
}

const coverImg = (src, position = "center", fit = "cover", pad = 0) => (
  <img
    key={src}
    src={src}
    alt=""
    draggable={false}
    style={{
      position: "absolute", inset: pad, width: `calc(100% - ${pad * 2}px)`, height: `calc(100% - ${pad * 2}px)`,
      objectFit: fit, objectPosition: position, display: "block", userSelect: "none",
      animation: "kerai-media-in 0.5s ease both",
    }}
  />
);

/* Choice used by the gold colour and diamond size selectors. */
function OptionButton({ selected, disabled, onClick, label, sublabel, glyph, compact }) {
  const t = themeTokens(useTheme());
  return (
    <button
      onClick={disabled ? undefined : onClick}
      disabled={disabled}
      aria-pressed={selected}
      aria-label={disabled ? `${label}, sold out` : label}
      style={{
        background: "transparent", border: "none", padding: 0,
        cursor: disabled || !onClick ? "default" : "pointer",
        display: "flex", flexDirection: "column", alignItems: "center", gap: 12,
        opacity: disabled ? 0.4 : 1,
      }}
    >
      <span style={{ height: 72, display: "flex", alignItems: "flex-end" }}>{glyph}</span>
      <span style={{ ...TYPE.small, color: selected ? t.fg : t.fgMuted, fontWeight: selected ? 500 : 300, whiteSpace: "nowrap" }}>
        {label}
      </span>
      {!compact && (
        <span style={{ ...TYPE.small, height: 34, color: t.fgMuted }}>{disabled ? "Sold out" : sublabel || ""}</span>
      )}
    </button>
  );
}

/* Title and price. The product page, set page and payment screen share
   it so the block can carry straight across to payment. */
function SummaryTitle({ product, name, label, priceMinor, showPrice = true }) {
  const t = themeTokens(useTheme());
  return (
    <div>
      <Eyebrow style={{ color: t.gold, marginBottom: 12 }}>{label || productLabel(product)}</Eyebrow>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-end", gap: 24 }}>
        <Display size={64} style={{ minWidth: 0, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{name || product.name}</Display>
        {showPrice && (
          <Display size={48} style={{ flexShrink: 0, whiteSpace: "nowrap" }}>{priceMinor == null ? "Sold out" : KC.formatMoney(priceMinor)}</Display>
        )}
      </div>
      <div style={{ height: 1, background: t.line, marginTop: 22 }} />
    </div>
  );
}

function SummarySelectors({ metals, metal, carats, caratId, onChangeMetal, onChangeCarat, collapse = false, onlySelected = false }) {
  const t = themeTokens(useTheme());
  // Each option sits in a slot that can fold to nothing on the way to
  // payment, so the chosen ones close up without a gap.
  const slot = (sel, side, space) => {
    const folded = collapse && !sel;
    return {
      flexShrink: 0,
      maxWidth: folded ? 0 : 200,
      [side]: folded ? 0 : space,
      opacity: folded ? 0 : 1,
      overflow: folded ? "hidden" : "visible",
      transition: `max-width 0.55s ${FOLD}, padding 0.55s ${FOLD}, opacity 0.3s ease`,
    };
  };
  const compact = onlySelected;
  return (
    <div style={{ display: "flex", alignItems: "stretch", gap: 40 }}>
      <div style={{ flex: 1, minWidth: 0 }}>
        <Eyebrow style={{ color: t.fgMuted, marginBottom: 14, whiteSpace: "nowrap" }}>Gold colour</Eyebrow>
        <div style={{ display: "flex" }}>
          {metals.map((m) => {
            const sel = m.id === metal;
            if (onlySelected && !sel) return null;
            return (
              <div key={m.id} style={slot(sel, "paddingRight", 36)}>
                <OptionButton
                  compact={compact}
                  selected={sel}
                  disabled={!onlySelected && !m.available}
                  onClick={onChangeMetal ? () => onChangeMetal(m.id) : undefined}
                  label={m.name}
                  glyph={<span style={{
                    width: 56, height: 56, borderRadius: "50%", background: m.swatch,
                    outline: sel ? `2px solid ${t.gold}` : "2px solid transparent",
                    outlineOffset: 5, transition: "outline-color 0.3s",
                  }} />}
                />
              </div>
            );
          })}
        </div>
      </div>
      <div style={{ width: 1, background: t.line, alignSelf: "stretch" }} />
      <div style={{ flex: 1, minWidth: 0 }}>
        <Eyebrow style={{ color: t.fgMuted, marginBottom: 14, textAlign: "right", whiteSpace: "nowrap" }}>Diamond size</Eyebrow>
        <div style={{ display: "flex", alignItems: "flex-end", justifyContent: "flex-end" }}>
          {carats.map((c, i) => {
            const sel = caratId === c.id;
            if (onlySelected && !sel) return null;
            const dia = 34 + i * 16;
            const col = sel ? t.gold : t.fgMuted;
            return (
              <div key={c.id} style={slot(sel, "paddingLeft", 30)}>
                <OptionButton
                  compact={compact}
                  selected={sel}
                  disabled={!onlySelected && !c.available}
                  onClick={onChangeCarat ? () => onChangeCarat(c.id) : undefined}
                  label={c.label}
                  glyph={
                    <svg width={dia} height={dia} viewBox="0 0 24 24" fill="none" aria-hidden="true">
                      <path d="M6 3 L18 3 L22 9 L12 22 L2 9 Z" stroke={col} strokeWidth="1.2" strokeLinejoin="round" fill={sel ? PALETTE.white : "none"} />
                      <path d="M2 9 L22 9 M6 3 L9 9 L12 22 M18 3 L15 9 L12 22 M9 9 L15 9" stroke={sel ? "rgba(40,28,22,0.35)" : col} strokeWidth="0.8" strokeLinejoin="round" opacity="0.6" />
                    </svg>
                  }
                />
              </div>
            );
          })}
        </div>
      </div>
    </div>
  );
}

/* Hand-off to payment: the surrounding pieces fade away while the
   summary blocks gain their boxes and rise to where the payment screen
   draws them. */
async function riseToPayment({ wrap, boxes, fades, targetTop }) {
  const frame = wrap.closest(".screen-frame");
  if (!frame) return;
  const fading = fades.filter(Boolean).map((el) => {
    el.getAnimations().forEach((a) => a.cancel());
    return el.animate(
      [{ opacity: 1, filter: "blur(0px)" }, { opacity: 0, filter: "blur(12px)" }],
      { duration: 560, easing: "ease", fill: "forwards" }
    ).finished;
  });
  const frameRect = frame.getBoundingClientRect();
  const scale = frameRect.width / 1080;
  const top = (wrap.getBoundingClientRect().top - frameRect.top) / scale;
  const boxing = boxes.filter(Boolean).map((el) => el.animate(
    [{ opacity: 0 }, { opacity: 1 }],
    { duration: 500, delay: 200, easing: "ease", fill: "forwards" }
  ).finished);
  const rise = wrap.animate(
    [{ transform: "translateY(0)" }, { transform: `translateY(${targetTop + 24 - top}px)` }],
    { duration: 950, delay: 380, easing: FOLD, fill: "forwards" }
  ).finished;
  try { await Promise.all([...fading, ...boxing, rise]); } catch {}
}

function SquareStill({ src, style }) {
  return (
    <img src={src} alt="" draggable={false} style={{
      position: "absolute", inset: 0, width: "100%", height: "100%",
      objectFit: "contain", padding: 24, boxSizing: "border-box", display: "block", ...style,
    }} />
  );
}

/* One piece of a set, laid out as the payment screen's purchase box
   draws it: square packshot, then title and the chosen options. */
function SetPieceRow({ product, variant, still, stock, boxRef, style }) {
  const t = themeTokens(useTheme());
  const contentRef = useRefB(null);
  const [side, setSide] = useStateB(0);
  useLayoutEffectB(() => {
    const el = contentRef.current;
    setSide(el.offsetHeight);
    const ro = new ResizeObserver(() => setSide(el.offsetHeight));
    ro.observe(el);
    return () => ro.disconnect();
  }, []);
  return (
    <div style={{ position: "relative", display: "flex", gap: 36, isolation: "isolate", ...style }}>
      <div ref={boxRef} aria-hidden="true" style={{ position: "absolute", inset: -24, zIndex: -1, opacity: 0, border: `1px solid ${t.line}` }} />
      <div style={{ position: "relative", flexShrink: 0, width: side, overflow: "hidden", background: t.bgDeep }}>
        <SquareStill src={still} />
      </div>
      <div ref={contentRef} style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column", gap: 20 }}>
        <SummaryTitle product={product} name={product.name} label={typeName(product)} priceMinor={variant.amountMinor} />
        <SummarySelectors
          metals={KC.metalOptions(product, stock)}
          metal={variant.metal}
          carats={KC.caratOptions(product, variant.metal, stock)}
          caratId={variant.caratId}
          onlySelected
        />
      </div>
    </div>
  );
}

/* ------------------------------------------------------------------ */
/* Details bar and sheet                                               */
/* The bar names one thing at a time — the piece, then what the sheet  */
/* holds — and opens the sheet wherever it's tapped.                   */
/* ------------------------------------------------------------------ */
const DETAIL_LINE_MS = 3600;

function DetailsBar({ lines, onOpen, innerRef, style }) {
  const t = themeTokens(useTheme());
  const reduced = useReducedMotion();
  const [idx, setIdx] = useStateB(0);
  const key = lines.join("|");
  useEffectB(() => {
    setIdx(0);
    if (reduced || lines.length < 2) return;
    const id = setInterval(() => setIdx((i) => (i + 1) % lines.length), DETAIL_LINE_MS);
    return () => clearInterval(id);
  }, [key, reduced]);
  return (
    <button
      ref={innerRef}
      onClick={onOpen}
      aria-label={`Details: ${lines[0]}. Open for the full details, care, returns and help.`}
      style={{
        flexShrink: 0, width: "100%", height: 112, padding: "0 28px 0 32px",
        display: "flex", alignItems: "center", gap: 24,
        background: t.bgDeep, border: `1px solid ${t.line}`, color: t.fg, cursor: "pointer", textAlign: "left",
        ...style,
      }}
    >
      <svg width="30" height="30" viewBox="0 0 24 24" fill="none" aria-hidden="true" style={{ flexShrink: 0, color: t.gold }}>
        <path d="M6 3 L18 3 L22 9 L12 22 L2 9 Z" stroke="currentColor" strokeWidth="1.3" strokeLinejoin="round" />
      </svg>
      <span aria-hidden="true" style={{ position: "relative", flex: 1, minWidth: 0, height: 48 }}>
        {lines.map((line, i) => (
          <span key={line} style={{
            position: "absolute", inset: 0, display: "flex", alignItems: "center",
            fontFamily: SERIF, fontSize: 34, whiteSpace: "nowrap", overflow: "hidden",
            opacity: i === idx ? 1 : 0,
            filter: i === idx ? "blur(0px)" : "blur(8px)",
            transition: reduced ? "none" : "opacity 0.8s ease, filter 0.8s ease",
          }}>
            {line}
          </span>
        ))}
      </span>
      <span style={{ flexShrink: 0, display: "flex", alignItems: "center", gap: 18 }}>
        <span style={{ ...TYPE.label, color: t.fgMuted }}>Details</span>
        <span aria-hidden="true" style={{
          width: 56, height: 56, borderRadius: "50%", border: `1px solid ${t.line}`,
          display: "flex", alignItems: "center", justifyContent: "center",
        }}>
          <svg width="22" height="22" viewBox="0 0 24 24" fill="none"><path d="M12 4 V20 M4 12 H20" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" /></svg>
        </span>
      </span>
    </button>
  );
}

function AccordionItem({ title, open, onToggle, children }) {
  const t = themeTokens(useTheme());
  const reduced = useReducedMotion();
  return (
    <div style={{ borderTop: `1px solid ${t.line}` }}>
      <button onClick={onToggle} aria-expanded={open} style={{
        width: "100%", minHeight: 112, padding: "0", display: "flex", alignItems: "center", justifyContent: "space-between", gap: 24,
        background: "transparent", border: "none", color: t.fg, cursor: "pointer", textAlign: "left",
      }}>
        <span style={{ fontFamily: SERIF, fontSize: 38, lineHeight: 1.15 }}>{title}</span>
        <svg width="26" height="26" viewBox="0 0 24 24" fill="none" aria-hidden="true" style={{ flexShrink: 0, color: t.gold }}>
          <path d="M4 12 H20" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
          <path d="M12 4 V20" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" style={{
            transformOrigin: "12px 12px", transform: open ? "scaleY(0)" : "scaleY(1)", transition: reduced ? "none" : "transform 0.35s ease",
          }} />
        </svg>
      </button>
      <div style={{
        display: "grid", gridTemplateRows: open ? "1fr" : "0fr",
        transition: reduced ? "none" : `grid-template-rows 0.5s ${GRID_EASE}`,
      }}>
        <div style={{ overflow: "hidden", minHeight: 0 }}>
          <div style={{ paddingBottom: 44, opacity: open ? 1 : 0, transition: reduced ? "none" : "opacity 0.4s ease" }}>{children}</div>
        </div>
      </div>
    </div>
  );
}

function SpecTable({ rows }) {
  const t = themeTokens(useTheme());
  return (
    <dl style={{ margin: 0 }}>
      {rows.map((r, i) => (
        <div key={r.id} style={{
          display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 32, padding: "18px 0",
          borderTop: i ? `1px solid ${t.line}` : "none",
        }}>
          <dt style={{ ...TYPE.small, color: t.fgMuted, flexShrink: 0 }}>{r.label}</dt>
          <dd style={{ ...TYPE.body, fontSize: 26, margin: 0, textAlign: "right" }}>{r.value}</dd>
        </div>
      ))}
    </dl>
  );
}

function InfoBody({ section }) {
  const t = themeTokens(useTheme());
  return (
    <div>
      {section.text && <Body style={{ maxWidth: 860 }}>{section.text}</Body>}
      {section.points && (
        <ul style={{ listStyle: "none", margin: 0, padding: 0, display: "flex", flexDirection: "column", gap: 18 }}>
          {section.points.map((p) => (
            <li key={p} style={{ display: "flex", gap: 22, alignItems: "baseline" }}>
              <span aria-hidden="true" style={{ width: 8, height: 8, flexShrink: 0, borderRadius: "50%", background: t.gold, transform: "translateY(-5px)" }} />
              <Body>{p}</Body>
            </li>
          ))}
        </ul>
      )}
      {section.qr && (
        <div style={{ display: "flex", alignItems: "center", gap: 40, marginTop: 32 }}>
          <QrCode text={KC.MACHINE.whatsappUrl} size={240} label="QR code to message Kerai on WhatsApp" />
          <Body muted>Point your phone's camera at the code to open WhatsApp.</Body>
        </div>
      )}
    </div>
  );
}

/* `pieces` is one entry per piece: { key, title, rows }. The pieces open
   first; the rest of the sheet is the same for everything. */
function DetailsSheet({ open, onClose, title, pieces }) {
  const [openIds, setOpenIds] = useStateB(() => new Set(pieces.map((p) => p.key)));
  const pieceKeys = pieces.map((p) => p.key).join("|");
  useEffectB(() => { if (open) setOpenIds(new Set(pieces.map((p) => p.key))); }, [open, pieceKeys]);
  const toggle = (id) => setOpenIds((prev) => {
    const next = new Set(prev);
    if (next.has(id)) next.delete(id); else next.add(id);
    return next;
  });
  return (
    <Sheet open={open} onClose={onClose} eyebrow="Details" title={title} maxHeight={860}>
      {pieces.map((p) => (
        <AccordionItem key={p.key} title={p.title} open={openIds.has(p.key)} onToggle={() => toggle(p.key)}>
          <SpecTable rows={p.rows} />
        </AccordionItem>
      ))}
      {KC.INFO_SECTIONS.map((s) => (
        <AccordionItem key={s.id} title={s.title} open={openIds.has(s.id)} onToggle={() => toggle(s.id)}>
          <InfoBody section={s} />
        </AccordionItem>
      ))}
    </Sheet>
  );
}

function GiftLine({ innerRef, style }) {
  const t = themeTokens(useTheme());
  return (
    <div ref={innerRef} style={{ flexShrink: 0, display: "flex", alignItems: "center", justifyContent: "center", gap: 16, ...style }}>
      <svg width="28" height="28" viewBox="0 0 24 24" fill="none" aria-hidden="true" style={{ color: t.gold, flexShrink: 0 }}>
        <rect x="3.5" y="8.5" width="17" height="12" stroke="currentColor" strokeWidth="1.2" />
        <path d="M2.5 8.5 H21.5 M12 8.5 V20.5 M12 8.5 C9 8.5 7 7.5 7 5.5 C7 3.5 10 3.8 12 8.5 C14 3.8 17 3.5 17 5.5 C17 7.5 15 8.5 12 8.5" stroke="currentColor" strokeWidth="1.2" strokeLinejoin="round" />
      </svg>
      <span style={{ ...TYPE.small, color: t.fgMuted }}>{KC.GIFT_LINE}</span>
    </div>
  );
}

/* The turn as a frame sequence: dragging sideways plays it forwards or
   backwards, and a flick carries on and settles. With `autoTurn` it
   swings gently on its own until first touched. */
function ScrubSequence({ sequence, reduced, showHint = true, autoTurn = false }) {
  const t = themeTokens(useTheme());
  const wrapRef = useRefB(null);
  const canvasRef = useRefB(null);
  const framesRef = useRefB([]);
  const posRef = useRefB(0);
  const dragRef = useRefB(null);
  const flingRef = useRefB(0);
  const autoRef = useRefB(0);
  const [hint, setHint] = useStateB(true);

  const { dir, count, ext } = sequence;
  const clamp = (p) => Math.min(count - 1, Math.max(0, p));

  const draw = () => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const n = Math.round(posRef.current);
    const ready = (i) => {
      const f = framesRef.current[i];
      return f && f.complete && f.naturalWidth ? f : null;
    };
    let img = null;
    for (let d = 0; d < count && !img; d++) img = ready(n - d) || ready(n + d);
    if (!img) return;
    const ctx = canvas.getContext("2d");
    const w = canvas.width;
    const h = canvas.height;
    const size = Math.max(w, h) * (sequence.zoom || 1.3);
    ctx.clearRect(0, 0, w, h);
    ctx.drawImage(img, (w - size) / 2, (h - size) / 2, size, size);
  };

  useEffectB(() => {
    framesRef.current = Array.from({ length: count }, (_, i) => {
      const img = new Image();
      img.decoding = "async";
      img.onload = draw;
      img.src = `${dir}/${String(i + 1).padStart(3, "0")}.${ext}`;
      return img;
    });
    const canvas = canvasRef.current;
    const ro = new ResizeObserver(() => {
      const r = wrapRef.current.getBoundingClientRect();
      const dpr = window.devicePixelRatio || 1;
      canvas.width = Math.round(r.width * dpr);
      canvas.height = Math.round(r.height * dpr);
      draw();
    });
    ro.observe(wrapRef.current);

    if (autoTurn && !reduced) {
      const start = performance.now();
      const swing = (now) => {
        const phase = ((now - start) / 8000) * Math.PI * 2;
        posRef.current = ((count - 1) * (1 - Math.cos(phase))) / 2;
        draw();
        autoRef.current = requestAnimationFrame(swing);
      };
      autoRef.current = requestAnimationFrame(swing);
    }
    return () => { ro.disconnect(); cancelAnimationFrame(autoRef.current); cancelAnimationFrame(flingRef.current); };
  }, [dir]);

  const framesPerPx = () => count / wrapRef.current.getBoundingClientRect().width;

  const onDown = (e) => {
    cancelAnimationFrame(autoRef.current);
    cancelAnimationFrame(flingRef.current);
    e.currentTarget.setPointerCapture(e.pointerId);
    dragRef.current = { x: e.clientX, t: performance.now(), v: 0 };
    setHint(false);
  };
  const onMove = (e) => {
    const d = dragRef.current;
    if (!d) return;
    const now = performance.now();
    const dx = e.clientX - d.x;
    const from = posRef.current;
    posRef.current = clamp(from + dx * framesPerPx());
    d.v = (posRef.current - from) / Math.max(1, now - d.t);
    d.x = e.clientX;
    d.t = now;
    draw();
  };
  const onUp = () => {
    const d = dragRef.current;
    dragRef.current = null;
    if (!d || reduced) return;
    let v = performance.now() - d.t > 80 ? 0 : d.v;
    let last = performance.now();
    const tick = (now) => {
      const dt = now - last;
      last = now;
      const next = posRef.current + v * dt;
      posRef.current = clamp(next);
      v = next === posRef.current ? v * Math.pow(0.94, dt / 16.7) : 0;
      draw();
      if (Math.abs(v) > 0.002) flingRef.current = requestAnimationFrame(tick);
    };
    flingRef.current = requestAnimationFrame(tick);
  };

  return (
    <div
      ref={wrapRef}
      onPointerDown={onDown}
      onPointerMove={onMove}
      onPointerUp={onUp}
      onPointerCancel={onUp}
      style={{ position: "absolute", inset: 0, cursor: "grab", touchAction: "pan-y", userSelect: "none" }}
    >
      <canvas ref={canvasRef} style={{ position: "absolute", inset: 0, width: "100%", height: "100%", display: "block" }} />
      {showHint && (
        <div aria-hidden="true" style={{
          position: "absolute", left: 0, right: 0, bottom: 20, textAlign: "center", ...TYPE.label,
          color: t.fgMuted, opacity: hint ? 1 : 0, transition: "opacity 0.6s", pointerEvents: "none",
        }}>
          Drag to turn
        </div>
      )}
    </div>
  );
}

function ScreenDetail({ theme, product, metal, caratId, stock, notice = null, onChangeMetal, onChangeCarat, onContinue, onBack, onHome, onLogo }) {
  const t = themeTokens(theme);
  const reduced = useReducedMotion();

  const metals = KC.metalOptions(product, stock);
  const carats = KC.caratOptions(product, metal, stock);
  const carat = carats.find((c) => c.id === caratId && c.available) || null;
  const variant = carat ? KC.VARIANT_BY_SKU[carat.sku] : null;
  const priceMinor = variant ? variant.amountMinor : KC.fromPrice(product, metal, stock);

  const [infoOpen, setInfoOpen] = useStateB(false);
  const [collapsing, setCollapsing] = useStateB(false);
  const [hintVisible, setHintVisible] = useStateB(!!product.model);
  useEffectB(() => {
    if (!hintVisible) return;
    const id = setTimeout(() => setHintVisible(false), 5000);
    return () => clearTimeout(id);
  }, [hintVisible]);

  const morphRef = useRefB(window.__keraiMorph);
  const initialOrbit = morphRef.current && morphRef.current.modelOrbit;
  useLayoutEffectB(() => { window.__keraiMorph = null; }, []);

  const mediaRef = useRefB(null);
  const barRef = useRefB(null);
  const titleRef = useRefB(null);
  const selectorsRef = useRefB(null);
  const actionsRef = useRefB(null);
  const exitingRef = useRefB(false);
  const summaryRef = useRefB(null);
  const summaryBoxRef = useRefB(null);

  const GAP = 22;
  const SQUARE_H = (1080 - SIDE_B * 2 - GAP) / 2;

  const handleBack = async () => {
    if (exitingRef.current) return;
    exitingRef.current = true;
    const pending = [actionsRef, barRef, selectorsRef, titleRef, mediaRef].map((ref, i) => {
      const el = ref.current;
      if (!el) return null;
      el.getAnimations().forEach((a) => a.cancel());
      return el.animate(
        [
          { transform: "translateX(0)", opacity: 1, filter: "blur(0px)" },
          { transform: "translateX(72px)", opacity: 0, filter: "blur(12px)" },
        ],
        { duration: 720, delay: i * 12, easing: DETAIL_EASE_OUT, fill: "forwards" }
      ).finished;
    });
    try { await Promise.all(pending.filter(Boolean)); } catch {}
    window.__keraiEnterFrom = "left";
    onBack();
  };

  const playPaymentTransition = async () => {
    exitingRef.current = true;
    window.__keraiPayFromPdp = true;
    if (reduced || !summaryRef.current) return;
    setCollapsing(true);
    await riseToPayment({
      wrap: summaryRef.current,
      boxes: [summaryBoxRef.current],
      fades: [mediaRef, barRef, actionsRef].map((ref) => ref.current),
      targetTop: KERAI_PAY_BOX.top,
    });
  };

  const sequence = product.lifestyle ? product.sequenceSingle || null : null;

  const continueToPayment = () => {
    if (!variant || exitingRef.current) return;
    window.__keraiPaySequence = sequence;
    onContinue(
      { kind: "item", sku: variant.sku, displayedMinor: variant.amountMinor },
      { transition: playPaymentTransition }
    );
  };

  /* Media */
  const shot = KC.packshot(product, metal, carat ? carat.id : null);
  let media;
  if (product.lifestyle) {
    const ls = product.lifestyle;
    let feature;
    if (sequence) {
      feature = <MediaTile dark><ScrubSequence key={sequence.dir} sequence={sequence} reduced={reduced} autoTurn /></MediaTile>;
    } else if (product.video) {
      feature = (
        <MediaTile dark>
          <video src={product.video} autoPlay={!reduced} loop muted playsInline style={{
            position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover",
            display: "block", transform: "scale(1.3)", transformOrigin: "center center",
          }} />
        </MediaTile>
      );
    } else if (product.model) {
      feature = (
        <MediaTile dark>
          <HeroImage id={`det-${product.id}`} src={product.model} fallbackSrc={shot.src} studio tilt={0}
                     initialOrbit={initialOrbit} metalColor={KC.METALS[metal].model} onInteract={() => setHintVisible(false)} />
          <div aria-hidden="true" style={{
            position: "absolute", left: 0, right: 0, bottom: 20, textAlign: "center", ...TYPE.label,
            color: t.fgMuted, opacity: hintVisible ? 1 : 0, transition: "opacity 0.6s", pointerEvents: "none",
          }}>
            Drag to turn
          </div>
        </MediaTile>
      );
    } else {
      const still = product.still || shot;
      feature = <MediaTile dark>{coverImg(still.src, "center", "contain", 36)}</MediaTile>;
    }
    media = (
      <div style={{ display: "flex", flexDirection: "column", gap: GAP, height: "100%" }}>
        <MediaTile style={{ flex: 1, minHeight: 0 }}>{coverImg(ls.src, ls.position)}</MediaTile>
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: GAP, height: SQUARE_H, flexShrink: 0 }}>
          {feature}
          <MediaTile>{product.scale && coverImg(product.scale.src, product.scale.position)}</MediaTile>
        </div>
      </div>
    );
  } else {
    media = <MediaTile dark style={{ height: "100%" }}>{coverImg(shot.src, "center", "contain", 40)}</MediaTile>;
  }

  const sheetPieces = [{ key: "piece", title: "This piece", rows: KC.specRows(product, variant) }];

  return (
    <ScreenFrame theme={theme} onBack={onBack} onHome={onHome}>
      <KeraiGlassHeader theme={theme} onLogo={onLogo} />
      <style>{DETAIL_KEYFRAMES}</style>

      <div style={{
        position: "absolute", top: KERAI_HEAD_H, left: 0, right: 0, bottom: 0,
        padding: `0 ${SIDE_B}px ${SIDE_B}px`, display: "flex", flexDirection: "column", gap: GAP, background: t.bg,
      }}>
        <div ref={mediaRef} style={{ flex: 1, minHeight: 0, animation: slideIn(0.02) }}>{media}</div>

        <div ref={summaryRef} style={{ position: "relative", flexShrink: 0, display: "flex", flexDirection: "column", gap: GAP, isolation: "isolate", marginTop: 8 }}>
          <div ref={summaryBoxRef} aria-hidden="true" style={{ position: "absolute", inset: -24, zIndex: -1, opacity: 0, border: `1px solid ${t.line}` }} />
          <div ref={titleRef} style={{ flexShrink: 0, animation: slideIn(0.1) }}>
            <SummaryTitle product={product} priceMinor={priceMinor} />
          </div>
          <div ref={selectorsRef} style={{ flexShrink: 0, animation: slideIn(0.12) }}>
            <SummarySelectors
              metals={metals}
              metal={metal}
              carats={carats}
              caratId={carat ? carat.id : null}
              onChangeMetal={onChangeMetal}
              onChangeCarat={onChangeCarat}
              collapse={collapsing}
            />
          </div>
        </div>

        <DetailsBar innerRef={barRef} lines={KC.detailLines(product, variant)} onOpen={() => setInfoOpen(true)} style={{ animation: slideIn(0.14) }} />

        {notice && (
          <div role="status" style={{ flexShrink: 0 }}>
            <Body size={24}>{notice}</Body>
          </div>
        )}

        <div ref={actionsRef} style={{ flexShrink: 0, display: "flex", gap: 18, animation: slideIn(0.18) }}>
          <div style={{ flex: "0 0 250px" }}>
            <PrimaryButton variant="ghost" onClick={handleBack}>Back</PrimaryButton>
          </div>
          <div style={{ flex: 1 }}>
            <PrimaryButton dense disabled={!variant} onClick={continueToPayment}>
              {variant ? `Continue to payment · ${KC.formatMoney(variant.amountMinor)}` : "Continue to payment"}
            </PrimaryButton>
          </div>
        </div>
      </div>

      <DetailsSheet open={infoOpen} onClose={() => setInfoOpen(false)} title={pieceTitle(product)} pieces={sheetPieces} />
    </ScreenFrame>
  );
}

/* ------------------------------------------------------------------ */
/* 05. SET — the matching pieces, bought and collected as one          */
/* ------------------------------------------------------------------ */
function SetTotal({ set }) {
  const t = themeTokens(useTheme());
  const saving = KC.setSavingMinor(set);
  return (
    <div style={{ borderTop: `1px solid ${t.line}`, borderBottom: `1px solid ${t.line}`, padding: "22px 0", display: "flex", justifyContent: "space-between", alignItems: "center", gap: 24 }}>
      <div>
        <Eyebrow style={{ color: t.fgMuted }}>Set price</Eyebrow>
        {saving > 0 && <div style={{ ...TYPE.small, color: t.gold, marginTop: 8 }}>You save {KC.formatMoney(saving)} on the pieces bought separately</div>}
      </div>
      <Display size={56} style={{ flexShrink: 0 }}>{KC.formatMoney(set.priceMinor)}</Display>
    </div>
  );
}

function ScreenSet({ theme, set, fromSku, stock, notice = null, onBack, onContinue, onHome, onLogo }) {
  const t = themeTokens(theme);
  const reduced = useReducedMotion();
  const parts = KC.setParts(set);
  const fromProduct = KC.PRODUCT_BY_ID[KC.VARIANT_BY_SKU[fromSku].productId];
  const lifestyle = fromProduct.lifestyle || (parts.find((x) => x.product.lifestyle) || {}).product?.lifestyle;
  const available = KC.isAvailable(set.sku, stock) && set.components.includes(fromSku);
  const GAP = 22;

  const [infoOpen, setInfoOpen] = useStateB(false);
  const mediaRef = useRefB(null);
  const barRef = useRefB(null);
  const summaryRef = useRefB(null);
  const boxRefs = [useRefB(null), useRefB(null)];
  const totalRef = useRefB(null);
  const statusRef = useRefB(null);
  const actionsRef = useRefB(null);
  const exitingRef = useRefB(false);

  const playPaymentTransition = async () => {
    exitingRef.current = true;
    window.__keraiPayFromPdp = true;
    if (reduced || !summaryRef.current) return;
    await riseToPayment({
      wrap: summaryRef.current,
      boxes: boxRefs.map((r) => r.current),
      fades: [mediaRef, barRef, totalRef, statusRef, actionsRef].map((r) => r.current),
      targetTop: KERAI_PAY_BOX.setTop,
    });
  };

  const continueToPayment = () => {
    if (!available || exitingRef.current) return;
    onContinue({ kind: "set", setId: set.id, fromSku, displayedMinor: set.priceMinor }, { transition: playPaymentTransition });
  };

  const handleBack = () => {
    window.__keraiEnterFrom = "left";
    onBack();
  };

  const lines = [`${parts.length} pieces in ${KC.METALS[set.metal].name.toLowerCase()}, boxed together`, ...KC.detailLines(fromProduct, null).slice(1)];
  const sheetPieces = parts.map(({ product, variant }) => ({ key: variant.sku, title: pieceTitle(product), rows: KC.specRows(product, variant) }));

  return (
    <ScreenFrame theme={theme} onBack={onBack} onHome={onHome}>
      <KeraiGlassHeader theme={theme} onLogo={onLogo} />
      <style>{DETAIL_KEYFRAMES}</style>

      <div style={{
        position: "absolute", top: KERAI_HEAD_H, left: 0, right: 0, bottom: 0,
        padding: `0 ${SIDE_B}px ${SIDE_B}px`, display: "flex", flexDirection: "column", gap: GAP, background: t.bg,
      }}>
        <div ref={mediaRef} style={{ flex: 1, minHeight: 0, animation: slideIn(0.02) }}>
          <MediaTile style={{ height: "100%" }}>{lifestyle && coverImg(lifestyle.src, lifestyle.position)}</MediaTile>
        </div>

        <div ref={summaryRef} style={{ flexShrink: 0, display: "flex", flexDirection: "column", gap: KERAI_PAY_BOX.setGap + 48, margin: "28px 0 24px" }}>
          {parts.map(({ product, variant, still }, i) => (
            <SetPieceRow key={variant.sku} product={product} variant={variant} still={still} stock={stock} boxRef={boxRefs[i]}
                         style={{ animation: slideIn(0.08 + i * 0.04) }} />
          ))}
        </div>

        <div ref={totalRef} style={{ flexShrink: 0, animation: slideIn(0.14) }}>
          <SetTotal set={set} />
        </div>

        <DetailsBar innerRef={barRef} lines={lines} onOpen={() => setInfoOpen(true)} style={{ animation: slideIn(0.16) }} />

        {(!available || notice) && (
          <div ref={statusRef} role="status" style={{ flexShrink: 0 }}>
            <Body size={24}>{!available ? "This set is no longer available." : notice}</Body>
          </div>
        )}

        <div ref={actionsRef} style={{ flexShrink: 0, display: "flex", gap: 18, animation: slideIn(0.18) }}>
          <div style={{ flex: "0 0 250px" }}>
            <PrimaryButton variant="ghost" onClick={handleBack}>Back</PrimaryButton>
          </div>
          <div style={{ flex: 1 }}>
            <PrimaryButton dense disabled={!available} onClick={continueToPayment}>
              Continue to payment · {KC.formatMoney(set.priceMinor)}
            </PrimaryButton>
          </div>
        </div>
      </div>

      <DetailsSheet open={infoOpen} onClose={() => setInfoOpen(false)} title={set.name} pieces={sheetPieces} />
    </ScreenFrame>
  );
}

Object.assign(window, {
  ScreenBrowse,
  ScreenDetail,
  ScreenSet,
  SummaryTitle,
  SummarySelectors,
  KeraiGlassHeader,
  SetTotal,
  SquareStill,
  ScrubSequence,
  GiftLine,
  KERAI_HEAD_H,
  pieceTitle,
  typeName,
  productLabel,
});
