/* global React */
const { useState, useEffect, useRef, useMemo, createContext, useContext } = React;

/* ------------------------------------------------------------------ */
/* Kerai logo — uses the official wordmark SVG via CSS mask so it      */
/* picks up the parent text color cleanly.                             */
/* ------------------------------------------------------------------ */
function KeraiMark({ size = 26, color = "currentColor" }) {
  // Logo aspect: 373.24 × 207.58; KERAI portion is ~70% of total height
  // so size (the visual scale of KERAI letters) ~= 0.7 of total height
  const totalHeight = size / 0.7;
  const totalWidth = totalHeight * (373.24 / 207.58);
  return (
    <span
      role="img"
      aria-label="Kerai"
      style={{
        display: "inline-block",
        width: totalWidth,
        height: totalHeight,
        backgroundColor: color,
        WebkitMaskImage: "url(assets/kerai-logo.svg)",
        maskImage: "url(assets/kerai-logo.svg)",
        WebkitMaskSize: "contain",
        maskSize: "contain",
        WebkitMaskRepeat: "no-repeat",
        maskRepeat: "no-repeat",
        WebkitMaskPosition: "center",
        maskPosition: "center",
      }}
    />
  );
}

/* ------------------------------------------------------------------ */
/* Theme context — dark (deep brown) vs ivory                          */
/* ------------------------------------------------------------------ */
const ThemeCtx = createContext("dark");
const useTheme = () => useContext(ThemeCtx);

const PALETTE = {
  brown: "#281c16",
  brownDeep: "#140d0b",
  brownSoft: "#3a2a21",
  ivory: "#f6f1ea",
  ivoryWarm: "#ede4d4",
  ivoryDeep: "#e7dccb",
  gold: "#968269",
  goldLight: "#b9a787",
  orange: "#e67725",
  grey: "#cecac8",
  white: "#ffffff",
};

// Light: the brand's white and light grey with deep-brown type and flat
// gold accents. Dark: deep brown with ivory type and lifted gold.
function themeTokens(theme) {
  if (theme === "light" || theme === "ivory") {
    return {
      bg: PALETTE.white,
      bgSoft: "#f5f4f3",          // light grey at ~20% on white
      bgDeep: "#ecebea",          // light grey at ~40% on white
      fg: PALETTE.brown,
      fgMuted: "rgba(40,28,22,0.62)",
      fgFaint: "rgba(40,28,22,0.38)",
      line: "rgba(40,28,22,0.14)",
      gold: PALETTE.gold,
      goldLight: PALETTE.goldLight,
      orange: PALETTE.orange,
      placeholder: "rgba(40,28,22,0.05)",
      placeholderStripe: "rgba(40,28,22,0.09)",
      hover: "rgba(40,28,22,0.05)",
      subtle: "rgba(40,28,22,0.06)",
      subtleHover: "rgba(40,28,22,0.1)",
      shadow: "rgba(40,28,22,0.16)",
      glass: "rgba(255,255,255,0.62)",
      glassDense: "rgba(255,255,255,0.86)",
      swatchEdge: "rgba(40,28,22,0.22)",
    };
  }
  return {
    bg: PALETTE.brown,
    bgSoft: "#2f211a",
    bgDeep: PALETTE.brownDeep,
    fg: PALETTE.ivory,
    fgMuted: "rgba(246,241,234,0.62)",
    fgFaint: "rgba(246,241,234,0.32)",
    line: "rgba(246,241,234,0.13)",
    gold: PALETTE.goldLight,
    goldLight: "#d2bf9c",
    orange: PALETTE.orange,
    placeholder: "rgba(246,241,234,0.04)",
    placeholderStripe: "rgba(246,241,234,0.08)",
    hover: "rgba(246,241,234,0.06)",
    subtle: "rgba(246,241,234,0.08)",
    subtleHover: "rgba(246,241,234,0.12)",
    shadow: "rgba(0,0,0,0.55)",
    glass: "rgba(40,28,22,0.55)",
    glassDense: "rgba(40,28,22,0.84)",
    swatchEdge: "transparent",
  };
}

/* ------------------------------------------------------------------ */
/* Type scale. Two families only: Elysian for names, prices and        */
/* headings; Season Sans for everything else. Nothing on the kiosk is  */
/* set below 24px, which reads from a standing or seated position.     */
/* ------------------------------------------------------------------ */
const SANS = "'Season Sans', 'Arial', sans-serif";
const SERIF = "'Elysian', 'Times New Roman', serif";
const TYPE = {
  min: 24,
  // Uppercase labels: eyebrows, section labels, buttons.
  label: { fontFamily: SANS, fontSize: 24, fontWeight: 400, letterSpacing: "0.16em", textTransform: "uppercase", lineHeight: 1.2 },
  // Sentence-case controls: filter choices, option names.
  control: { fontFamily: SANS, fontSize: 26, fontWeight: 400, letterSpacing: "0.01em", lineHeight: 1.2 },
  small: { fontFamily: SANS, fontSize: 24, fontWeight: 300, letterSpacing: "0.01em", lineHeight: 1.45 },
  body: { fontFamily: SANS, fontSize: 28, fontWeight: 300, letterSpacing: "0.01em", lineHeight: 1.5 },
};

function Display({ children, size = 96, weight = 400, style = {}, as: As = "h1" }) {
  return (
    <As
      style={{
        fontFamily: "'Elysian', 'Times New Roman', serif",
        fontWeight: weight,
        fontSize: size,
        lineHeight: 1.02,
        letterSpacing: "0.005em",
        margin: 0,
        ...style,
      }}
    >
      {children}
    </As>
  );
}

function Eyebrow({ children, style = {} }) {
  return <div style={{ ...TYPE.label, ...style }}>{children}</div>;
}

function Body({ children, size = 28, style = {}, muted = false }) {
  return (
    <p
      style={{
        ...TYPE.body,
        fontSize: Math.max(TYPE.min, size),
        margin: 0,
        opacity: muted ? 0.72 : 1,
        ...style,
      }}
    >
      {children}
    </p>
  );
}

/* ------------------------------------------------------------------ */
/* Hairline divider                                                    */
/* ------------------------------------------------------------------ */
function Hairline({ color, style = {} }) {
  const t = themeTokens(useTheme());
  return (
    <div
      style={{
        height: 1,
        background: color || t.line,
        width: "100%",
        ...style,
      }}
    />
  );
}

/* ------------------------------------------------------------------ */
/* Primary CTA — large, generous tap target, gold underline accent     */
/* ------------------------------------------------------------------ */
function PrimaryButton({ children, onClick, fullWidth = true, variant = "fill", disabled = false, dense = false, ariaLabel }) {
  const theme = useTheme();
  const t = themeTokens(theme);
  const [hoverRaw, setHover] = useState(false);
  const hover = hoverRaw && !disabled;

  const fillBg = t.fg;
  const fillFg = t.bg;

  // Auto-prepend a back-arrow if the label starts with "Back".
  // The editor environment wraps text children in <span>s so we have
  // to extract the textual content from React children rather than
  // checking `typeof children === "string"`.
  const extractText = (node) => {
    if (node == null || typeof node === "boolean") return "";
    if (typeof node === "string" || typeof node === "number") return String(node);
    if (Array.isArray(node)) return node.map(extractText).join("");
    if (typeof node === "object" && node.props) return extractText(node.props.children);
    return "";
  };
  const labelText = extractText(children).trim();
  const isBack = /^back/i.test(labelText);
  // Use an SVG arrow so we get exact vertical centering — the unicode
  // "←" character sits awkwardly above its font baseline in Arial.
  const BackArrow = (
    <svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true"
         style={{ flexShrink: 0, display: "block" }}>
      <path d="M19 12 H5 M11 6 L5 12 L11 18"
            stroke="currentColor" strokeWidth="1.4"
            strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );

  if (variant === "ghost") {
    return (
      <button
        onClick={disabled ? undefined : onClick}
        disabled={disabled}
        aria-label={ariaLabel}
        onMouseEnter={() => setHover(true)}
        onMouseLeave={() => setHover(false)}
        style={{
          width: fullWidth ? "100%" : "auto",
          minHeight: 96,
          padding: "0 40px",
          background: hover ? t.subtleHover : t.subtle,
          color: t.fg,
          border: "none",
          ...TYPE.label,
          fontWeight: 300,
          letterSpacing: "0.2em",
          whiteSpace: "nowrap",
          cursor: disabled ? "default" : "pointer",
          transition: "all 0.5s cubic-bezier(.2,.7,.2,1)",
          opacity: disabled ? 0.4 : hover ? 1 : 0.92,
          display: "inline-flex",
          alignItems: "center",
          justifyContent: "center",
          gap: 14,
        }}
      >
        {isBack && BackArrow}
        {children}
      </button>
    );
  }

  return (
    <button
      onClick={disabled ? undefined : onClick}
      disabled={disabled}
      aria-label={ariaLabel}
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
      style={{
        width: fullWidth ? "100%" : "auto",
        minHeight: 96,
        padding: dense ? "0 28px" : "0 48px",
        background: fillBg,
        color: fillFg,
        border: "none",
        ...TYPE.label,
        fontWeight: 600,
        letterSpacing: dense ? "0.14em" : "0.2em",
        whiteSpace: "nowrap",
        cursor: disabled ? "default" : "pointer",
        opacity: disabled ? 0.38 : 1,
        transition: "transform 0.4s cubic-bezier(.2,.7,.2,1), background 0.4s",
        transform: hover ? "translateY(-2px)" : "translateY(0)",
        display: "inline-flex",
        alignItems: "center",
        justifyContent: "center",
        gap: 14,
      }}
    >
      {isBack && BackArrow}
      {children}
    </button>
  );
}

/* ------------------------------------------------------------------ */
/* Choice — one tap selects. The selected choice is outlined in        */
/* orange, so it is never mistaken for the primary button.             */
/* ------------------------------------------------------------------ */
function Choice({ selected = false, disabled = false, onClick, children, ariaLabel, style = {} }) {
  const t = themeTokens(useTheme());
  return (
    <button
      onClick={disabled ? undefined : onClick}
      disabled={disabled}
      aria-pressed={selected}
      aria-label={ariaLabel}
      style={{
        height: 88,
        padding: "0 20px",
        flex: "1 1 auto",
        minWidth: 0,
        display: "inline-flex",
        alignItems: "center",
        justifyContent: "center",
        gap: 14,
        background: t.subtle,
        color: t.fg,
        border: "none",
        boxShadow: selected ? `inset 0 0 0 2px ${PALETTE.orange}` : "inset 0 0 0 2px transparent",
        ...TYPE.control,
        fontWeight: selected ? 500 : 400,
        whiteSpace: "nowrap",
        cursor: disabled ? "default" : "pointer",
        opacity: disabled ? 0.34 : 1,
        transition: "box-shadow 0.35s ease, opacity 0.35s ease",
        ...style,
      }}
    >
      {children}
    </button>
  );
}

/* ------------------------------------------------------------------ */
/* Sheet — the one modal surface. Rises from the bottom so its content */
/* and its close control sit in easy reach; tapping outside closes it. */
/* ------------------------------------------------------------------ */
const SHEET_EASE = "cubic-bezier(.22,.7,.2,1)";
const SHEET_FOOT_H = 200;

function Sheet({ open, onClose, eyebrow, title, children, closeLabel = "Close", maxHeight = 1640, footer = null, ariaLabel }) {
  const t = themeTokens(useTheme());
  const reduced = useReducedMotion();
  const [mounted, setMounted] = useState(open);
  const [shown, setShown] = useState(false);

  useEffect(() => {
    if (open) {
      setMounted(true);
      const id = requestAnimationFrame(() => requestAnimationFrame(() => setShown(true)));
      return () => cancelAnimationFrame(id);
    }
    setShown(false);
    const id = setTimeout(() => setMounted(false), reduced ? 0 : 420);
    return () => clearTimeout(id);
  }, [open]);

  if (!mounted) return null;
  const dur = reduced ? "0s" : "0.55s";
  return (
    <div role="dialog" aria-modal="true" aria-label={ariaLabel || (typeof title === "string" ? title : undefined)}
         style={{ position: "absolute", inset: 0, zIndex: 60 }}>
      <div onClick={onClose} style={{
        position: "absolute", inset: 0, background: "rgba(12,8,6,0.66)",
        opacity: shown ? 1 : 0, transition: `opacity ${dur} ease`,
      }} />
      <div style={{
        position: "absolute", left: 0, right: 0, bottom: 0, maxHeight,
        display: "flex", flexDirection: "column",
        background: t.bg, borderTop: `1px solid ${t.line}`,
        transform: shown ? "translateY(0)" : "translateY(100%)",
        transition: `transform ${dur} ${SHEET_EASE}`,
      }}>
        {(eyebrow || title) && (
          <div style={{ flexShrink: 0, padding: "56px 72px 28px" }}>
            {eyebrow && <Eyebrow style={{ color: t.gold, marginBottom: 16 }}>{eyebrow}</Eyebrow>}
            {title && <Display size={56} as="h2">{title}</Display>}
          </div>
        )}
        <div style={{ flex: "1 1 auto", minHeight: 0, overflowY: "auto", padding: `0 72px ${SHEET_FOOT_H + 24}px`, WebkitOverflowScrolling: "touch", scrollbarWidth: "none" }}>
          {children}
        </div>
        {/* Glass, so the content scrolling behind shows there is more. */}
        <div style={{
          position: "absolute", left: 0, right: 0, bottom: 0, height: SHEET_FOOT_H, boxSizing: "border-box",
          padding: "32px 72px 72px", display: "flex", gap: 18,
          background: t.glass, backdropFilter: "blur(22px) saturate(1.1)", WebkitBackdropFilter: "blur(22px) saturate(1.1)",
          borderTop: `1px solid ${t.line}`,
        }}>
          {footer}
          <div style={{ flex: 1 }}>
            <PrimaryButton variant="ghost" onClick={onClose}>{closeLabel}</PrimaryButton>
          </div>
        </div>
      </div>
    </div>
  );
}

/* QR code drawn from its text. Dark modules on an ivory plate with a
   quiet zone, which phone cameras read at arm's length. */
function QrCode({ text, size = 300, label }) {
  const shape = React.useMemo(() => {
    if (!window.qrcode || !text) return null;
    const qr = window.qrcode(0, "M");
    qr.addData(text);
    qr.make();
    const n = qr.getModuleCount();
    let d = "";
    for (let r = 0; r < n; r++) for (let c = 0; c < n; c++) if (qr.isDark(r, c)) d += `M${c} ${r}h1v1h-1z`;
    return { n, d };
  }, [text]);
  return (
    <div role="img" aria-label={label} style={{ width: size, height: size, padding: Math.round(size * 0.08), boxSizing: "border-box", background: PALETTE.ivory, flexShrink: 0 }}>
      {shape && (
        <svg viewBox={`0 0 ${shape.n} ${shape.n}`} width="100%" height="100%" shapeRendering="crispEdges" style={{ display: "block" }}>
          <path d={shape.d} fill={PALETTE.brownDeep} />
        </svg>
      )}
    </div>
  );
}

/* WhatsApp mark, one colour so it takes the button's ink. */
function WhatsAppGlyph({ size = 30 }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true" style={{ flexShrink: 0, display: "block" }}>
      <path fill="currentColor" d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 0 1-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 0 1-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 0 1 2.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0 0 12.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 0 0 5.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 0 0-3.48-8.413Z" />
    </svg>
  );
}

/* ------------------------------------------------------------------ */
/* Subtle progress / step indicator                                    */
/* ------------------------------------------------------------------ */
function StepDots({ total, current }) {
  const t = themeTokens(useTheme());
  const HIGHLIGHT = "#e67725";
  return (
    <div style={{ display: "flex", gap: 14, alignItems: "center" }}>
      {Array.from({ length: total }).map((_, i) => {
        const active = i === current;
        return (
          <div
            key={i}
            style={{
              width: active ? 12 : 8,
              height: active ? 12 : 8,
              borderRadius: "50%",
              background: active ? HIGHLIGHT : t.fgFaint,
              boxShadow: active ? `0 0 12px ${HIGHLIGHT}66` : "none",
              transition: "all 0.5s cubic-bezier(.2,.7,.2,1)",
            }}
          />
        );
      })}
    </div>
  );
}

/* ------------------------------------------------------------------ */
/* Frame — every screen is rendered inside this 1080x1920 frame.       */
/* Bottom dock has been removed — each screen owns its own bottom      */
/* shelf with its action buttons (Back lives in those shelves).        */
/* ------------------------------------------------------------------ */
const DOCK_HEIGHT = 0;

function ScreenFrame({ theme, children }) {
  const t = themeTokens(theme);
  return (
    <ThemeCtx.Provider value={theme}>
      <div
        className="screen-frame"
        style={{
          position: "relative",
          width: 1080,
          height: 1920,
          background: t.bg,
          color: t.fg,
          overflow: "hidden",
          fontFamily: "'Season Sans', 'Arial', sans-serif",
          transition: "background-color 0.6s ease, color 0.6s ease",
        }}
      >
        {/* Content layer */}
        <div style={{ position: "relative", height: "100%", zIndex: 2 }}>
          {children}
        </div>
      </div>
    </ThemeCtx.Provider>
  );
}
/* ------------------------------------------------------------------ */
/* TopBar — shared header (wordmark or screen title).                  */
/* ------------------------------------------------------------------ */
function TopBar({ title, right, left }) {
  const t = themeTokens(useTheme());
  return (
    <div
      style={{
        position: "absolute",
        top: 0,
        left: 0,
        right: 0,
        height: 132,
        padding: "0 72px",
        display: "flex",
        alignItems: "center",
        justifyContent: "space-between",
        zIndex: 10,
      }}
    >
      <div style={{ width: 200, display: "flex", alignItems: "center" }}>
        {left}
      </div>
      <div style={{ flex: 1, display: "flex", justifyContent: "center" }}>
        {title ? (
          <Eyebrow style={{ opacity: 0.62 }}>{title}</Eyebrow>
        ) : (
          <KeraiMark size={26} color={t.fg} />
        )}
      </div>
      <div style={{ width: 200, display: "flex", justifyContent: "flex-end" }}>
        {right}
      </div>
    </div>
  );
}

/* ------------------------------------------------------------------ */
/* ImageSlot wrapper                                                   */
/* (Falls back to a tinted placeholder if no image dropped yet)        */
/* ------------------------------------------------------------------ */
function HeroImage({ id, placeholder, src, fit = "cover", width = "100%", height = "100%", shape = "rect", radius = 0, style = {}, initialOrbit, tilt = -22, studio = false, metalColor, fallbackSrc, onInteract, imgStyle = {} }) {
  const t = themeTokens(useTheme());
  const mvRef = useRef(null);
  const [modelFailed, setModelFailed] = useState(false);
  const isModel = !!src && /\.(glb|gltf)$/i.test(src) && !modelFailed;

  // Colour the model's metal from the selected metal. Stone materials
  // are left alone.
  useEffect(() => {
    const el = mvRef.current;
    if (!isModel || !el || !metalColor) return;
    const apply = () => {
      const materials = (el.model && el.model.materials) || [];
      materials
        .filter((m) => !/diamond|stone|gem/i.test(m.name || ""))
        .forEach(async (m) => {
          try {
            if (m.ensureLoaded) await m.ensureLoaded();
            m.pbrMetallicRoughness.setBaseColorFactor([...metalColor, 1]);
          } catch {}
        });
    };
    if (el.loaded) apply();
    el.addEventListener("load", apply);
    return () => el.removeEventListener("load", apply);
  }, [isModel, metalColor && metalColor.join(",")]);

  useEffect(() => {
    const el = mvRef.current;
    if (!isModel || !el) return;
    const fail = () => setModelFailed(true);
    el.addEventListener("error", fail);
    return () => el.removeEventListener("error", fail);
  }, [isModel]);

  // 3D model branch — render <model-viewer> when src is a .glb/.gltf.
  // No upload UI; auto-rotates with soft shadows.
  // `tilt` rotates the host so the spin axis reads diagonal on screen.
  // `studio` swaps in a jewellery lighting setup: a bright HDR so the
  // stones catch highlights, a little extra exposure, and a slow orbit.
  // `initialOrbit` (if provided) starts the auto-rotation from a
  // specific angle so navigations between screens feel continuous.
  // If the model fails to load, `fallbackSrc` is shown instead.
  if (modelFailed && fallbackSrc) {
    src = fallbackSrc;
    fit = "contain";
  }
  if (isModel) {
    const mv = studio ? {
      "environment-image": "https://modelviewer.dev/shared-assets/environments/lightroom_14b.hdr",
      exposure: "1.1",
      "shadow-intensity": "0.5",
      "shadow-softness": "1",
      "rotation-per-second": "10deg",
      "camera-orbit": initialOrbit || "0deg 38deg 68%",
      "camera-target": "0m 0m 0m",
      "min-camera-orbit": "auto 20deg 55%",
      "max-camera-orbit": "auto 70deg 110%",
      "field-of-view": "24deg",
      "tone-mapping": "aces",
    } : {
      "environment-image": "neutral",
      exposure: "1",
      "shadow-intensity": "1",
      "shadow-softness": "0.8",
      "rotation-per-second": "20deg",
      "camera-orbit": initialOrbit || "0deg 80deg auto",
      "field-of-view": "30deg",
    };
    return (
      <div style={{ position: "relative", width, height, overflow: "hidden", ...style }} onPointerDown={onInteract}>
        <div style={{
          width: "100%",
          height: "100%",
          transform: tilt ? `rotate(${tilt}deg)` : "none",
          transformOrigin: "center center",
        }}>
          {React.createElement("model-viewer", {
            ref: mvRef,
            src,
            "camera-controls": true,
            "auto-rotate": true,
            "auto-rotate-delay": studio ? "3000" : "0",
            "interaction-prompt": "none",
            "disable-zoom": studio ? true : undefined,
            ...mv,
            style: {
              width: "100%",
              height: "100%",
              background: "transparent",
              "--poster-color": "transparent",
            },
          })}
        </div>
      </div>
    );
  }

  // When an explicit image src is supplied, render a plain <img> — no upload UI,
  // no drag/drop affordance. (Falls back to <image-slot> for any slot
  // without a default image so wholesale placeholders still show.)
  if (src) {
    return (
      <div style={{ position: "relative", width, height, overflow: "hidden", ...style }}>
        <img
          src={src}
          alt=""
          draggable={false}
          style={{
            width: "100%",
            height: "100%",
            objectFit: fit,
            display: "block",
            userSelect: "none",
            WebkitUserDrag: "none",
            ...imgStyle,
          }}
        />
      </div>
    );
  }
  const attrs = {
    id,
    placeholder,
    shape,
    radius: String(radius),
    fit,
    style: {
      width: "100%",
      height: "100%",
      display: "block",
      "--bg": t.placeholder,
      "--stripe": t.placeholderStripe,
      "--fg": t.fgMuted,
    },
  };
  return (
    <div style={{ position: "relative", width, height, ...style }}>
      {React.createElement("image-slot", attrs)}
    </div>
  );
}

function useReducedMotion() {
  const query = "(prefers-reduced-motion: reduce)";
  const [reduced, setReduced] = useState(() => window.matchMedia && window.matchMedia(query).matches);
  useEffect(() => {
    if (!window.matchMedia) return;
    const mq = window.matchMedia(query);
    const onChange = () => setReduced(mq.matches);
    mq.addEventListener("change", onChange);
    return () => mq.removeEventListener("change", onChange);
  }, []);
  return reduced;
}

/* Make components globally available */
Object.assign(window, {
  useReducedMotion,
  TYPE,
  SANS,
  SERIF,
  Choice,
  Sheet,
  QrCode,
  WhatsAppGlyph,
  KeraiMark,
  ThemeCtx,
  useTheme,
  PALETTE,
  themeTokens,
  Display,
  Eyebrow,
  Body,
  Hairline,
  PrimaryButton,
  StepDots,
  ScreenFrame,
  TopBar,
  HeroImage,
  DOCK_HEIGHT,
});
