/* global React, ReactDOM, lucide */
// YachtingStack Storefront — shared atoms. Exported to window for cross-file use.

// ---------- Icon (Lucide) ----------
function Icon({ name, size = 18, color = "currentColor", stroke = 1.6, style = {} }) {
  const r = React.useRef(null);
  React.useEffect(() => {
    if (!r.current) return;
    r.current.innerHTML = "";
    const node = window.lucide && (lucide[name] || (lucide.icons && lucide.icons[name]));
    if (!node) return;
    const el = lucide.createElement(node);
    el.setAttribute("width", size); el.setAttribute("height", size);
    el.setAttribute("stroke", color); el.setAttribute("stroke-width", stroke);
    r.current.appendChild(el);
  }, [name, size, color, stroke]);
  return <span ref={r} style={{ display: "inline-flex", lineHeight: 0, ...style }} />;
}

// ---------- Wordmark ----------
function Wordmark({ h = 30, fs = 19, mono = false }) {
  return (
    <span style={{ display: "inline-flex", alignItems: "center", gap: 9 }}>
      <img src="/assets/logo/YachtingStack-Logo.png" alt="YachtingStack" style={{ height: h, width: "auto" }} />
      <span style={{ fontFamily: "var(--font-ui)", fontWeight: 300, letterSpacing: "-0.01em", fontSize: fs }}>
        <span style={{ color: "var(--ink)" }}>Yachting</span><span style={{ color: "var(--cyan)" }}>Stack</span>
      </span>
    </span>
  );
}

// ---------- Eyebrow / kicker ----------
function Eyebrow({ children, tone = "cyan", style = {} }) {
  const c = tone === "cyan" ? "var(--cyan)" : tone === "bad" ? "var(--bad)" : "var(--ink-3)";
  return (
    <div style={{ fontFamily: "var(--font-mono)", fontSize: 11, fontWeight: 500, letterSpacing: "0.22em",
      textTransform: "uppercase", color: c, opacity: 0.85, ...style }}>{children}</div>
  );
}

// ---------- Button ----------
function Btn({ children, variant = "primary", size = "md", icon, iconRight, onClick, href, full, disabled, style = {}, ...rest }) {
  const [h, setH] = React.useState(false);
  const [a, setA] = React.useState(false);
  const sz = { sm: { p: "0 14px", mh: 32, fs: 13 }, md: { p: "0 18px", mh: 40, fs: 14 }, lg: { p: "0 24px", mh: 46, fs: 14.5 } }[size];
  const variants = {
    primary: { base: { background: "var(--cyan)", color: "var(--cyan-ink)", border: "1px solid transparent", fontWeight: 500 },
      hover: { boxShadow: "0 0 0 1px rgba(80,134,182,.4), 0 8px 30px -8px rgba(80,134,182,.6)", background: "var(--cyan-2)" } },
    ghost: { base: { background: "transparent", color: "var(--ink-2)", border: "1px solid var(--hairline-2)", fontWeight: 400 },
      hover: { color: "var(--ink)", borderColor: "rgba(255,255,255,.34)", background: "var(--wa-03)" } },
    soft: { base: { background: "rgba(80,134,182,.08)", color: "var(--cyan-2)", border: "1px solid rgba(80,134,182,.2)", fontWeight: 500 },
      hover: { background: "rgba(80,134,182,.14)", borderColor: "rgba(80,134,182,.34)" } },
  };
  const v = variants[variant] || variants.primary;
  const El = href ? "a" : "button";
  return (
    <El {...rest} href={href} onClick={disabled ? undefined : onClick}
      onMouseEnter={() => setH(true)} onMouseLeave={() => { setH(false); setA(false); }}
      onMouseDown={() => setA(true)} onMouseUp={() => setA(false)}
      style={{ display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 7,
        fontFamily: "var(--font-ui)", letterSpacing: ".01em", lineHeight: 1, cursor: disabled ? "not-allowed" : "pointer",
        textDecoration: "none", padding: sz.p, minHeight: sz.mh, fontSize: sz.fs, borderRadius: 7, whiteSpace: "nowrap",
        width: full ? "100%" : "auto", opacity: disabled ? 0.45 : 1,
        transition: "background .28s, color .28s, border-color .28s, box-shadow .28s, transform .12s",
        transform: a && !disabled ? "translateY(1px)" : "none", ...v.base, ...(h && !disabled ? v.hover : null), ...style }}>
      {icon && <Icon name={icon} size={15} />}{children}{iconRight && <Icon name={iconRight} size={15} />}
    </El>
  );
}

// ---------- Access-state badge ----------
function Access({ state, mini = false }) {
  const map = {
    live:    ["Live", "var(--good)", "rgba(52,211,153,.1)", "rgba(52,211,153,.28)"],
    signup:  ["Sign up", "var(--cyan-ink)", "var(--cyan)", "var(--cyan)"],
    contact: ["Contact sales", "var(--ink-2)", "var(--wa-04)", "var(--hairline-2)"],
    onboard: ["Onboarding", "#fff", "var(--wa-08)", "rgba(255,255,255,.2)"],
    beta:    ["Private beta", "var(--violet)", "rgba(167,139,250,.1)", "rgba(167,139,250,.28)"],
    internal:["Internal", "var(--ink-3)", "transparent", "var(--hairline)"],
    soon:    ["Coming soon", "var(--warn)", "rgba(251,191,36,.08)", "rgba(251,191,36,.24)"],
    freepro: ["Free · Pro", "var(--good)", "rgba(52,211,153,.08)", "rgba(52,211,153,.24)"],
    docs:    ["Docs · API", "var(--cyan-2)", "rgba(80,134,182,.07)", "rgba(80,134,182,.22)"],
    trial:   ["Free trial", "var(--cyan-2)", "rgba(80,134,182,.1)", "rgba(80,134,182,.3)"],
    reqaccess: ["Request access", "var(--ink-2)", "var(--wa-04)", "var(--hairline-2)"],
    customdev: ["Custom Dev clients", "var(--violet)", "rgba(167,139,250,.1)", "rgba(167,139,250,.28)"],
    free: ["Free forever", "var(--good)", "rgba(52,211,153,.1)", "rgba(52,211,153,.28)"],
  };
  const [label, fg, bg, bd] = map[state] || map.live;
  return (
    <span style={{ fontFamily: "var(--font-mono)", fontSize: mini ? 9 : 10.5, fontWeight: 600, letterSpacing: ".04em",
      textTransform: "uppercase", color: fg, background: bg, border: `1px solid ${bd}`,
      padding: mini ? "2px 6px" : "4px 9px", borderRadius: 20, whiteSpace: "nowrap", lineHeight: 1.3 }}>{label}</span>
  );
}

// ---------- Reveal-on-scroll ----------
function useReveal(threshold = 0.16) {
  const ref = React.useRef(null);
  const [shown, setShown] = React.useState(() => (typeof document !== "undefined" && document.hidden) || (window.matchMedia && matchMedia("(prefers-reduced-motion: reduce)").matches));
  React.useEffect(() => {
    const el = ref.current; if (!el || shown) return;
    const io = new IntersectionObserver(([e]) => { if (e.isIntersecting) { setShown(true); io.disconnect(); } },
      { threshold, rootMargin: "0px 0px -7% 0px" });
    io.observe(el);
    const t = setTimeout(() => setShown(true), 1500); // fallback if IO never fires (backgrounded tab)
    return () => { io.disconnect(); clearTimeout(t); };
  }, [threshold]);
  return [ref, shown];
}
function Reveal({ children, delay = 0, style = {}, ...p }) {
  const [ref, shown] = useReveal();
  return (
    <div ref={ref} data-reveal="" className={shown ? "shown" : ""} style={{ transitionDelay: `${delay}ms`, ...style }} {...p}>
      {children}
    </div>
  );
}

// ---------- Count-up ----------
function useCountUp(target, run, dur = 1300) {
  const [n, setN] = React.useState(0);
  React.useEffect(() => {
    if (!run) return;
    if ((window.matchMedia && matchMedia("(prefers-reduced-motion: reduce)").matches) || document.hidden) { setN(target); return; }
    let raf, start;
    const step = (t) => { if (!start) start = t; const p = Math.min((t - start) / dur, 1);
      setN(Math.round((1 - Math.pow(1 - p, 3)) * target)); if (p < 1) raf = requestAnimationFrame(step); };
    raf = requestAnimationFrame(step); return () => raf && cancelAnimationFrame(raf);
  }, [target, run, dur]);
  return n;
}

// ---------- CTA text ----------
// One fixed label so every CTA button across the site says the same thing.
// (Was a wall-clock rotation; the hook name survives so call sites stay put.)
const CTA_WORDS = ["Get On Board"];
function useRotatingCTA() {
  return CTA_WORDS[0];
}

// ---------- Generic-AI vs dedicated-agents comparison grid ----------
// Shared by For Yachts and the AI Agents product page. `w` is a whatIs spec:
// either { cols: [{ label, sub? }...], rows: [[label, ...marks]] } for N
// comparison columns, or the legacy 2-column shape { generic, genericSub,
// dedicated, rows: [[label, g, a]] }. Marks are "yes" | "partial" | "no";
// the LAST column is always the YachtingStack one and carries the tint band.
// Headers lean via the cmp-* classes (22° desktop, 45° phones) so the mark
// columns stay narrow no matter how many there are.
const CMP_MARKS = {
  yes:     { ic: "Check", c: "var(--good)", label: "Yes" },
  partial: { ic: "Minus", c: "var(--warn)", label: "Partly" },
  no:      { ic: "X",     c: "var(--bad)",  label: "No" },
};
function CmpMark({ v }) {
  const m = CMP_MARKS[v] || CMP_MARKS.no;
  return <span title={m.label} aria-label={m.label} style={{ display: "inline-flex", lineHeight: 0 }}><Icon name={m.ic} size={16} color={m.c} /></span>;
}
function CompareGrid({ w }) {
  const cols = w.cols || [{ label: w.generic, sub: w.genericSub }, { label: w.dedicated }];
  const lastCol = cols.length - 1;
  const lastRow = w.rows.length - 1;
  const tint = { background: "rgba(80,134,182,.07)", borderLeft: "1px solid rgba(80,134,182,.26)", borderRight: "1px solid rgba(80,134,182,.26)" };
  const markCell = { display: "flex", alignItems: "center", justifyContent: "center", padding: "13px 8px" };
  return (
    <>
      <div style={{ display: "grid", gridTemplateColumns: `minmax(0,1fr) repeat(${cols.length}, auto)` }}>
        {/* header row */}
        <div />
        {/* header cells stay untinted — the highlight band starts at the first
            mark row (a tinted box around the leaning label read as a glitch) */}
        {cols.map((c, ci) => (
          <div key={c.label} className="cmp-headcell" style={{ ...markCell, padding: "12px 14px" }}>
            <span className="cmp-nowrap cmp-head" style={{ fontFamily: "var(--font-mono)", fontSize: 10.5,
              fontWeight: ci === lastCol ? 700 : 600, letterSpacing: ".04em", textTransform: "uppercase",
              color: ci === lastCol ? "var(--cyan-2)" : "var(--ink-3)", textAlign: "center", lineHeight: 1.5 }}>
              <span style={{ display: "block" }}>{c.label}</span>
              {c.sub && <span className="cmp-nowrap cmp-sub" style={{ display: "block", marginTop: 2, fontWeight: 500, color: "var(--ink-4)" }}>{c.sub}</span>}
            </span>
          </div>
        ))}
        {w.rows.map(([label, ...marks], i) => (
          <React.Fragment key={label}>
            <div style={{ display: "flex", alignItems: "center", padding: "13px 12px 13px 0", borderTop: "1px solid var(--hairline)",
              fontFamily: "var(--font-ui)", fontWeight: 300, fontSize: 14, lineHeight: 1.5, color: "var(--ink-2)" }}>{label}</div>
            {marks.map((m, ci) => (
              <div key={ci} style={{ ...markCell,
                ...(ci === lastCol
                  ? { ...tint,
                      borderTop: i === 0 ? "1px solid rgba(80,134,182,.26)" : "1px solid rgba(80,134,182,.14)",
                      borderBottom: i === lastRow ? "1px solid rgba(80,134,182,.26)" : "none",
                      borderRadius: i === 0 ? "12px 12px 0 0" : i === lastRow ? "0 0 12px 12px" : 0 }
                  : { borderTop: "1px solid var(--hairline)" }) }}><CmpMark v={m} /></div>
            ))}
          </React.Fragment>
        ))}
      </div>
      <div style={{ display: "inline-flex", alignItems: "center", gap: 7, marginTop: 16,
        fontFamily: "var(--font-ui)", fontWeight: 300, fontSize: 12.5, color: "var(--ink-4)" }}>
        <Icon name="Minus" size={13} color="var(--warn)" />{w.legend}
      </div>
    </>
  );
}

// ---------- Device mockups (shared by the Agents product page and For Yachts) ----------
// One device screen: tries the real screenshot first and falls back to a quiet,
// gridded placeholder while the asset doesn't exist yet — drop the file at the
// spec'd path and the screen fills itself, no code change needed.
// `fit` — "cover" (default) crops to fill the screen; "contain" letterboxes the
// whole capture; "none" renders the capture at its native 1:1 pixel size,
// cropped to the frame (use for readability at actual display size).
// `position` overrides the crop/letterbox anchor.
function AgScreen({ src, alt, label, icon, fit = "cover", position = null }) {
  const [state, setState] = React.useState("loading"); // loading | ok | missing
  return (
    <div style={{ position: "absolute", inset: 0, background: "var(--panel)" }}>
      {state !== "ok" && (
        <div style={{ position: "absolute", inset: 0, display: "flex", flexDirection: "column", alignItems: "center",
          justifyContent: "center", gap: 10, padding: 12, textAlign: "center",
          backgroundImage: "radial-gradient(closest-side at 50% 42%, rgba(80,134,182,.16), transparent 78%)," +
            "repeating-linear-gradient(0deg, var(--wa-grid) 0 1px, transparent 1px 24px)," +
            "repeating-linear-gradient(90deg, var(--wa-grid) 0 1px, transparent 1px 24px)" }}>
          <Icon name={icon} size={26} color="var(--ink-4)" stroke={1.2} />
          <span style={{ fontFamily: "var(--font-mono)", fontSize: 9.5, fontWeight: 500, letterSpacing: ".14em",
            textTransform: "uppercase", color: "var(--ink-4)", lineHeight: 1.6 }}>{label}<br />screenshot</span>
        </div>
      )}
      <img src={src} alt={state === "ok" ? alt : ""} loading="lazy" decoding="async"
        onLoad={() => setState("ok")} onError={() => setState("missing")}
        style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: fit,
          objectPosition: position || (fit === "contain" ? "center" : "top center"),
          display: state === "missing" ? "none" : "block", opacity: state === "ok" ? 1 : 0, transition: "opacity .5s var(--ease)" }} />
    </div>
  );
}

// CSS-drawn laptop frame. Pass `shot` for a single screenshot screen, or
// `children` to render custom screen content (e.g. a slideshow) instead.
// `aspect` overrides the lid's aspect ratio — set it to the screenshot's own
// ratio so full-page captures display uncropped.
function AgLaptop({ shot, children, aspect = "16 / 10.2" }) {
  return (
    <div style={{ position: "relative", width: "100%" }}>
      {/* lid + screen */}
      <div style={{ position: "relative", margin: "0 auto", width: "88%", aspectRatio: aspect,
        borderRadius: "16px 16px 0 0", background: "linear-gradient(180deg, #131c2d, #0a101c)",
        border: "1px solid rgba(255,255,255,.14)", borderBottom: "none", padding: "11px 11px 0", boxSizing: "border-box",
        boxShadow: "0 46px 100px -42px rgba(0,0,0,.85)" }}>
        <span aria-hidden="true" style={{ position: "absolute", top: 4, left: "50%", transform: "translateX(-50%)",
          width: 4, height: 4, borderRadius: "50%", background: "rgba(255,255,255,.2)" }} />
        <div style={{ position: "relative", width: "100%", height: "100%", borderRadius: "7px 7px 0 0", overflow: "hidden" }}>
          {children || <AgScreen {...shot} icon="Monitor" />}
          {/* periodic sheen sweeping across the glass */}
          <div className="ag-sheen" aria-hidden="true" style={{ position: "absolute", top: "-20%", bottom: "-20%", left: 0, width: "36%",
            background: "linear-gradient(105deg, transparent, rgba(148,193,218,.09), transparent)", pointerEvents: "none" }} />
        </div>
      </div>
      {/* deck */}
      <div style={{ position: "relative", width: "100%", height: 15, borderRadius: "3px 3px 12px 12px",
        background: "linear-gradient(180deg, #1c2739, #0c131f)", border: "1px solid rgba(255,255,255,.12)", boxSizing: "border-box" }}>
        <span aria-hidden="true" style={{ position: "absolute", top: 0, left: "50%", transform: "translateX(-50%)",
          width: "16%", height: 5, borderRadius: "0 0 8px 8px", background: "rgba(0,0,0,.5)" }} />
      </div>
    </div>
  );
}

// CSS-drawn phone frame. Same contract as AgLaptop: `shot` or `children`.
function AgPhone({ shot, children }) {
  return (
    <div style={{ position: "relative", width: "100%", aspectRatio: "9 / 19", borderRadius: "13% / 6.2%",
      background: "linear-gradient(180deg, #141d2f, #0a101c)", border: "1px solid rgba(255,255,255,.18)",
      padding: "5.5%", boxSizing: "border-box",
      boxShadow: "0 36px 74px -26px rgba(0,0,0,.9), 0 0 0 1px rgba(0,0,0,.5)" }}>
      <div style={{ position: "relative", width: "100%", height: "100%", borderRadius: "9% / 4.3%", overflow: "hidden" }}>
        {children || <AgScreen {...shot} icon="Smartphone" />}
        {/* island */}
        <span aria-hidden="true" style={{ position: "absolute", top: "2.4%", left: "50%", transform: "translateX(-50%)",
          width: "36%", height: "3.4%", borderRadius: 20, background: "rgba(0,0,0,.85)", border: "1px solid var(--wa-08)" }} />
      </div>
    </div>
  );
}

// ---------- Solution showcase (shared by For Yachts and the Agents product page) ----------
// Real product captures inside the laptop / phone mockups, one device at a time.
// A small Desktop/Mobile toggle sits above the mockup; the laptop letterboxes the
// ~1915x940 desktop captures uncropped (framed by a simulated browser bar and
// system bar), the phone insets the mobile captures below the camera island so
// the app header isn't hidden. Cycling only runs while the block is on screen
// (never under reduced motion) and pauses on hover; clicking opens a
// near-fullscreen lightbox over the unscaled originals in `${dir}/full/`.
// Missing files fall back to AgScreen's gridded placeholder, so entries can be
// spec'd before their captures land. Below 820px the toggle hides and the phone
// carries the block alone (see the .showcase-* rules in index.html).
const prefersReducedMotion = () => window.matchMedia && matchMedia("(prefers-reduced-motion: reduce)").matches;

// Hover cover for the showcase mockups — a designed affordance in place of the
// browser's zoom-in cursor: the screen dims softly and a centered chip invites
// the click. pointer-events stay off so the click falls through to the mockup.
function EnlargeCover({ show }) {
  return (
    <div aria-hidden="true" style={{ position: "absolute", inset: 0, zIndex: 6, display: "flex", alignItems: "center", justifyContent: "center",
      pointerEvents: "none", opacity: show ? 1 : 0, transition: "opacity .35s var(--ease)",
      background: "radial-gradient(closest-side at 50% 46%, rgba(4,9,17,.42), transparent 76%)" }}>
      <span style={{ display: "inline-flex", alignItems: "center", gap: 8, padding: "9px 15px", borderRadius: 999,
        background: "rgba(9,15,26,.82)", backdropFilter: "blur(7px)", WebkitBackdropFilter: "blur(7px)",
        border: "1px solid rgba(80,134,182,.5)",
        boxShadow: "0 14px 44px -14px rgba(0,0,0,.85), 0 0 24px -6px rgba(80,134,182,.35)",
        transform: show ? "none" : "translateY(7px) scale(.94)", transition: "transform .35s var(--ease)",
        fontFamily: "var(--font-mono)", fontSize: 10.5, fontWeight: 600, letterSpacing: ".09em", textTransform: "uppercase", color: "var(--cyan-2)" }}>
        <Icon name="Maximize2" size={13} color="var(--cyan-2)" /> Click to enlarge
      </span>
    </div>
  );
}

function DeviceShowcase({ desk, mob, dir = "/assets/images/agents/showcase", domain = "agents.yachtingstack.ai" }) {
  const [device, setDevice] = React.useState("desktop"); // desktop | mobile
  const [di, setDi] = React.useState(0);
  const [mi, setMi] = React.useState(0);
  const [paused, setPaused] = React.useState(false);
  const [inView, setInView] = React.useState(false);
  const rootRef = React.useRef(null);
  // live clock for the simulated system bar
  const [now, setNow] = React.useState(() => new Date());
  React.useEffect(() => {
    const id = setInterval(() => setNow(new Date()), 30000);
    return () => clearInterval(id);
  }, []);
  const clock = now.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
  const clockDate = now.toLocaleDateString([], { weekday: "short", day: "numeric", month: "short" });
  // On phone-width viewports the CSS pins the phone mockup regardless of the
  // toggle state — the lightbox must follow what's actually on screen, or a
  // tap on the phone would open the DESKTOP captures.
  const [phoneVp, setPhoneVp] = React.useState(() => !!(window.matchMedia && matchMedia("(max-width: 820px)").matches));
  React.useEffect(() => {
    const mq = matchMedia("(max-width: 820px)");
    const on = () => setPhoneVp(mq.matches);
    mq.addEventListener("change", on);
    return () => mq.removeEventListener("change", on);
  }, []);
  const effDevice = phoneVp ? "mobile" : device;
  // lightbox — click a mockup to view the capture near-fullscreen; chevrons
  // navigate the active device's set, X / Escape / backdrop click close it.
  const [lightbox, setLightbox] = React.useState(false);
  const lbList = effDevice === "desktop" ? desk : mob;
  const lbIdx = effDevice === "desktop" ? di : mi;
  const lbStep = (step) => (effDevice === "desktop" ? setDi : setMi)((x) => (x + step + lbList.length) % lbList.length);
  React.useEffect(() => {
    if (!lightbox) return;
    const onKey = (e) => {
      if (e.key === "Escape") setLightbox(false);
      else if (e.key === "ArrowRight") lbStep(1);
      else if (e.key === "ArrowLeft") lbStep(-1);
    };
    window.addEventListener("keydown", onKey);
    const prevOverflow = document.documentElement.style.overflow;
    document.documentElement.style.overflow = "hidden";
    return () => { window.removeEventListener("keydown", onKey); document.documentElement.style.overflow = prevOverflow; };
  }, [lightbox, device]);
  const lbBtn = { position: "absolute", zIndex: 2, display: "flex", alignItems: "center", justifyContent: "center", padding: 0,
    width: 44, height: 44, borderRadius: "50%", cursor: "pointer", border: "1px solid var(--hairline-2)",
    background: "rgba(13,22,38,.85)", color: "var(--ink-2)", transition: "border-color .2s, color .2s" };
  React.useEffect(() => {
    const el = rootRef.current;
    if (!el) return;
    const io = new IntersectionObserver(([e]) => setInView(e.isIntersecting), { threshold: 0.25 });
    io.observe(el);
    return () => io.disconnect();
  }, []);
  React.useEffect(() => {
    if (!inView || paused || lightbox || prefersReducedMotion()) return;
    const a = setInterval(() => setDi((x) => (x + 1) % desk.length), 4000);
    const b = setInterval(() => setMi((x) => (x + 1) % mob.length), 3400);
    return () => { clearInterval(a); clearInterval(b); };
  }, [inView, paused, lightbox, desk.length, mob.length]);
  const deskLayers = desk.map(([file, label], i) => (
    <div key={file} aria-hidden={i !== di} style={{ position: "absolute", inset: 0, opacity: i === di ? 1 : 0,
      transition: "opacity .6s var(--ease)" }}>
      {/* whole capture visible; files are pre-resampled to 2x the display size for crispness */}
      <AgScreen src={`${dir}/${file}.png`} alt={label} label={label} icon="Monitor" fit="contain" />
    </div>
  ));
  const mobLayers = mob.map(([file, label], i) => (
    // #0B111E = the app's own background (sampled from the captures) so the
    // status-bar strip above the screenshot blends seamlessly
    <div key={file} aria-hidden={i !== mi} style={{ position: "absolute", inset: 0, opacity: i === mi ? 1 : 0,
      transition: "opacity .6s var(--ease)", background: "#0B111E" }}>
      {/* content starts below the camera island — the strip above reads as a status bar */}
      <div style={{ position: "absolute", left: 0, right: 0, top: "6.5%", bottom: 0 }}>
        <AgScreen src={`${dir}/${file}.png`} alt={label} label={label} icon="Smartphone" />
      </div>
    </div>
  ));
  return (
    <Reveal delay={100}>
      <div ref={rootRef}>
        {/* device toggle — centered above the mockup; hidden on mobile where the phone is forced */}
        <div className="showcase-toggle" style={{ display: "flex", justifyContent: "center", gap: 8, marginTop: 28 }}>
          {[["desktop", "Monitor", "Desktop"], ["mobile", "Smartphone", "Mobile"]].map(([k, ic, lb]) => (
            <button key={k} onClick={() => setDevice(k)} aria-pressed={device === k}
              style={{ all: "unset", boxSizing: "border-box", cursor: "pointer", display: "inline-flex", alignItems: "center", gap: 7,
                padding: "7px 13px", borderRadius: 999,
                background: device === k ? "rgba(80,134,182,.14)" : "var(--surface)",
                border: `1px solid ${device === k ? "rgba(80,134,182,.5)" : "var(--hairline-2)"}`,
                fontFamily: "var(--font-mono)", fontSize: 10.5, fontWeight: 600, letterSpacing: ".06em", textTransform: "uppercase",
                color: device === k ? "var(--cyan-2)" : "var(--ink-3)",
                transition: "background .25s, border-color .25s, color .25s" }}>
              <Icon name={ic} size={13} />{lb}
            </button>
          ))}
        </div>
        <div className="showcase-row" onMouseEnter={() => setPaused(true)} onMouseLeave={() => setPaused(false)}
          style={{ position: "relative", margin: "20px auto 0", maxWidth: 860 }}>
          <div className="aura" aria-hidden="true" style={{ position: "absolute", inset: "-6% -4%", pointerEvents: "none",
            background: "radial-gradient(closest-side, rgba(80,134,182,.15), transparent 72%)" }} />
          <div style={{ position: "relative", width: "100%" }}>
            <div className="showcase-laptop" onClick={() => setLightbox(true)}
              style={{ display: device === "desktop" ? "block" : "none", position: "relative", width: "100%", cursor: "pointer" }}>
              <EnlargeCover show={paused && !lightbox} />
              <AgLaptop>
                {/* simulated browser chrome — fills the top letterbox band */}
                <div style={{ position: "absolute", left: 0, right: 0, top: 0, height: "13.5%", zIndex: 2, boxSizing: "border-box",
                  background: "var(--panel)", borderBottom: "1px solid var(--hairline-2)",
                  display: "flex", alignItems: "center", gap: 12, padding: "0 2.4%" }}>
                  <span style={{ display: "inline-flex", gap: 5, flexShrink: 0 }}>
                    {["#FF5F57", "#FEBC2E", "#28C840"].map((c) => (
                      <span key={c} style={{ width: 7, height: 7, borderRadius: "50%", background: c, opacity: 0.8 }} />
                    ))}
                  </span>
                  <span style={{ flex: "0 1 250px", display: "inline-flex", alignItems: "center", gap: 6, minWidth: 0,
                    padding: "3px 10px", borderRadius: 999, background: "rgba(255,255,255,.05)", border: "1px solid var(--hairline)" }}>
                    <Icon name="Lock" size={8} color="var(--good)" />
                    <span style={{ fontFamily: "var(--font-mono)", fontSize: 8.5, color: "var(--ink-2)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{domain}</span>
                  </span>
                  <span style={{ marginLeft: "auto", flexShrink: 0, display: "inline-flex", color: "var(--ink-4)" }}><Icon name="MoreVertical" size={11} /></span>
                </div>
                {/* capture area between the two bars */}
                <div style={{ position: "absolute", left: 0, right: 0, top: "13.5%", bottom: "9.7%", background: "var(--panel)" }}>
                  {deskLayers}
                </div>
                {/* simulated system bar — clock in the corner */}
                <div style={{ position: "absolute", left: 0, right: 0, bottom: 0, height: "9.7%", zIndex: 2, boxSizing: "border-box",
                  background: "var(--bg-deep)", borderTop: "1px solid var(--hairline-2)",
                  display: "flex", alignItems: "center", padding: "0 2.4%" }}>
                  {/* same asset as the site header (the .svg in /assets/logo is an old gold variant — don't use it) */}
                  <img src="/assets/logo/YachtingStack-Logo.png" alt="" aria-hidden="true" style={{ width: 15, height: 15, objectFit: "contain" }} />
                  <span style={{ display: "inline-flex", gap: 6, marginLeft: 12 }}>
                    {["Bot", "Inbox", "LayoutDashboard"].map((ic) => (
                      <span key={ic} style={{ width: 17, height: 17, borderRadius: 5, background: "rgba(80,134,182,.1)", border: "1px solid rgba(80,134,182,.2)",
                        display: "inline-flex", alignItems: "center", justifyContent: "center", color: "var(--cyan-2)" }}><Icon name={ic} size={9} /></span>
                    ))}
                  </span>
                  <span style={{ marginLeft: "auto", textAlign: "right", lineHeight: 1.35 }}>
                    <span style={{ display: "block", fontFamily: "var(--font-mono)", fontSize: 7.5, fontWeight: 600, color: "var(--ink-2)" }}>{clock}</span>
                    <span style={{ display: "block", fontFamily: "var(--font-mono)", fontSize: 6, color: "var(--ink-4)" }}>{clockDate}</span>
                  </span>
                </div>
              </AgLaptop>
            </div>
            <div className="showcase-phone" onClick={() => setLightbox(true)}
              style={{ display: device === "mobile" ? "block" : "none", position: "relative",
                width: "min(235px, 56vw)", margin: "0 auto", cursor: "pointer" }}>
              <EnlargeCover show={paused && !lightbox} />
              <AgPhone>{mobLayers}</AgPhone>
            </div>
          </div>
        </div>
        {/* phone viewports have no hover to reveal the cover, so a small button
            carries the same affordance there */}
        {phoneVp && (
          <div style={{ display: "flex", justifyContent: "center", marginTop: 18 }}>
            <button onClick={() => setLightbox(true)} style={{ all: "unset", boxSizing: "border-box", cursor: "pointer",
              display: "inline-flex", alignItems: "center", gap: 7, padding: "8px 15px", borderRadius: 999,
              background: "rgba(80,134,182,.1)", border: "1px solid rgba(80,134,182,.35)",
              fontFamily: "var(--font-mono)", fontSize: 10.5, fontWeight: 600, letterSpacing: ".08em", textTransform: "uppercase",
              color: "var(--cyan-2)" }}>
              <Icon name="Maximize2" size={13} /> Tap to enlarge
            </button>
          </div>
        )}
      </div>
      {/* near-fullscreen viewer — portaled to <body> (the page wrapper's transform would trap position:fixed) */}
      {lightbox && ReactDOM.createPortal(
        <div onClick={() => setLightbox(false)} style={{ position: "fixed", inset: 0, zIndex: 120, background: "rgba(4,9,17,.92)",
          display: "flex", alignItems: "center", justifyContent: "center", padding: "4vh 6vw" }}>
          <img key={lbList[lbIdx][0]} className="ymfade" src={`${dir}/full/${lbList[lbIdx][0]}.png`}
            alt={lbList[lbIdx][1]} onClick={(e) => e.stopPropagation()}
            style={{ maxWidth: "90vw", maxHeight: "82vh", width: "auto", height: "auto", borderRadius: 10,
              border: "1px solid var(--hairline-2)", boxShadow: "0 40px 120px -30px rgba(0,0,0,.95)" }} />
          <button aria-label="Close" onClick={(e) => { e.stopPropagation(); setLightbox(false); }}
            style={{ ...lbBtn, top: 20, right: 22 }}><Icon name="X" size={20} /></button>
          <button aria-label="Previous screenshot" onClick={(e) => { e.stopPropagation(); lbStep(-1); }}
            style={{ ...lbBtn, left: 22, top: "50%", transform: "translateY(-50%)" }}><Icon name="ChevronLeft" size={24} /></button>
          <button aria-label="Next screenshot" onClick={(e) => { e.stopPropagation(); lbStep(1); }}
            style={{ ...lbBtn, right: 22, top: "50%", transform: "translateY(-50%)" }}><Icon name="ChevronRight" size={24} /></button>
          <div style={{ position: "absolute", left: "50%", bottom: 20, transform: "translateX(-50%)", whiteSpace: "nowrap", pointerEvents: "none" }}>
            <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, letterSpacing: ".06em", color: "var(--ink-2)" }}>{lbList[lbIdx][1]}</span>
            <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--ink-4)", marginLeft: 12 }}>{lbIdx + 1} / {lbList.length}</span>
          </div>
        </div>,
        document.body
      )}
    </Reveal>
  );
}

Object.assign(window, { Icon, Wordmark, Eyebrow, Btn, Access, useReveal, Reveal, useCountUp, useRotatingCTA, CTA_WORDS, AgScreen, AgLaptop, AgPhone, DeviceShowcase, CompareGrid });
