/* shared.jsx destructures only useState/useEffect/useRef/useMemo/
   useLayoutEffect off React — useCallback is NOT in that list, and these
   files share one global scope, so reaching for it here is a
   ReferenceError at runtime. Plain handlers instead; there is no memo
   boundary below this that would benefit from a stable identity anyway. */

/* `react/jsx-no-undef` does not consult the `global` directive above, so it
   flags Reveal and IconArrow — which are real, defined in shared.jsx and
   icons.jsx and shared through `window` because these preview files are
   <script type="text/babel"> in ONE global scope, not modules. Importing
   them is not possible; assigning them to local consts is actively unsafe,
   since Babel compiles a top-level const to a `var` that would clobber the
   global every other section reads (the bug that took out every icon on the
   site earlier this cycle). A scoped disable is the honest option. */

/* This page is static HTML served from public/ and compiled by in-browser
   Babel — there is no Next.js route, no bundler and no next/image here, so
   the LCP advice does not apply and the component does not exist. Every
   other preview file uses a plain <img> for the same reason. */

/* ============================================================
 * EXPLORE — eight doors into the mega menu
 *
 * This replaces the five long product sections (Inbox, Voice, Brain,
 * Engine, Integrations) that used to live between the journey timeline
 * and the outcomes block. Each of those now has a real product page, so
 * the homepage's job here is to SEND people there, not to re-argue the
 * case at 3,500 lines.
 *
 * Each card carries a generated 5s loop shot in the film's language —
 * close, shallow, tactile, no product UI. The video is the reward for
 * pointing at the card; at rest the card is a still poster and a line of
 * type, and the page is silent.
 *
 * PLAYBACK RULES, all of which exist because of a specific failure:
 *  - preload="none". Eight videos at ~400KB would be 3MB of autoloaded
 *    weight for content most visitors never point at.
 *  - The <video> element is only given its src on first hover, so a
 *    visitor who never touches this section downloads nothing.
 *  - play() returns a promise that REJECTS if the element is paused
 *    before it resolves — i.e. any quick mouse-across. Unhandled, that
 *    throws an AbortError into the console on every pass. Caught below.
 *  - On unhover the video pauses but does NOT reset to 0, so returning
 *    to a card resumes rather than restarting — much calmer when
 *    scanning across the grid.
 *  - Reduced motion never plays; the poster is the whole experience.
 * ============================================================ */

const CDN = "https://cdn.guestmaker.ai/marketing/homepage-v3/explore";
/* R2 keys are stable, so a re-uploaded poster keeps being served from cache
   until the URL changes. Bump this whenever an asset is re-cut — the hero
   does the same thing (hero-1-loop.mp4?v=7). */
const AV = "2";

const destinations = [
  {
    key: "brain",
    label: "Hotel Brain",
    href: "platform/knowledge.html",
    title: "One brain per hotel",
    body: "Every policy, rate rule and restaurant hour in one place. The single source every channel answers from.",
  },
  {
    key: "memory",
    label: "Guest Memories",
    href: "platform/memory.html",
    title: "It remembers",
    body: "Allergies, anniversaries, the room they asked for last time. Carried forward so nothing is ever asked twice.",
  },
  {
    key: "inbox",
    label: "Inboxes",
    href: "platform/inbox.html",
    title: "Every channel, one thread",
    body: "WhatsApp, Instagram, Messenger, web chat and email land in a single inbox your team already knows how to use.",
  },
  {
    key: "voice",
    label: "Voice Agent",
    href: "platform/voice.html",
    title: "It picks up",
    body: "Calls answered in the guest's language, around the clock, with a warm transfer to a person the moment it matters.",
  },
  {
    key: "crm",
    label: "Agentic CRM",
    /* Two destinations, so this card carries `options` instead of `href`.
       A card with options renders as a div rather than an anchor: two
       links nested inside an <a> is invalid HTML, and browsers recover
       from it by unnesting them, which breaks the card. */
    title: "Two sides of one CRM",
    body: "The guests who stay, and the agencies and corporates that send them. One contact record, one set of bookings, both sides reading the same history.",
    options: [
      { href: "platform/crm.html", label: "B2C CRM", sub: "Guests, stays and stages" },
      { href: "platform/crm-b2b.html", label: "B2B CRM", sub: "Agencies, TTOO and corporate" },
    ],
  },
  {
    key: "cdp",
    label: "CDP",
    href: "platform/cdp.html",
    title: "Four records, one guest",
    body: "AI identity resolution collapses the duplicates every PMS and booking engine quietly creates.",
  },
  {
    key: "journeys",
    label: "Campaigns & Journeys",
    href: "platform/journeys.html",
    title: "Sent because it fits",
    body: "Visual journeys and campaigns that fire on what a guest actually did, not on the day of the week.",
  },
  {
    key: "loyalty",
    label: "Loyalty",
    href: "platform/loyalty.html",
    title: "A reason to come back",
    body: "Points, tiers and cash credit that work on direct bookings, and pay for themselves in repeat stays.",
  },
];

const ExploreCard = ({ d, i }) => {
  const videoRef = useRef(null);
  // Held in state rather than a ref so that attaching the source triggers
  // the re-render that actually puts the src on the element.
  const [armed, setArmed] = useState(false);
  const [showOptions, setShowOptions] = useState(false);
  // A card with two destinations cannot be an anchor — nesting <a> inside
  // <a> is invalid, and browsers silently unnest it, which detaches the
  // options from the card.
  const Wrap = d.options ? "div" : "a";
  // Whether the pointer is CURRENTLY inside. A ref, not state: it is read
  // by the effect below and must not itself cause a render.
  const hoverRef = useRef(false);
  const barRef = useRef(null);
  const timeRef = useRef(null);
  const rafRef = useRef(0);

  const reduced =
    typeof window !== "undefined" &&
    window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  /* The progress hairline tracks real playback position, so it needs a
     value every frame. That is written STRAIGHT to the DOM through refs —
     putting currentTime in React state would re-render this component ~60
     times a second for as long as a pointer rests on a card, to move one
     line by a few pixels.

     rAF rather than the `timeupdate` event: timeupdate fires roughly 4x a
     second, which on a 5s loop is a visible staircase rather than a line. */
  const stopTicking = () => {
    if (rafRef.current) cancelAnimationFrame(rafRef.current);
    rafRef.current = 0;
  };

  const tick = () => {
    const v = videoRef.current;
    if (!v || !hoverRef.current) return stopTicking();
    // duration is NaN until metadata arrives — dividing by it paints the
    // bar at NaN width, which renders as a full bar for the first frames.
    if (v.duration > 0) {
      if (barRef.current) {
        barRef.current.style.transform = `scaleX(${v.currentTime / v.duration})`;
      }
      if (timeRef.current) {
        timeRef.current.textContent = `${v.currentTime.toFixed(1)}s`;
      }
    }
    rafRef.current = requestAnimationFrame(tick);
  };

  // play() rejects with AbortError whenever the element is paused before
  // the promise settles — i.e. any quick mouse-across. Unhandled, that
  // throws into the console on every pass over the grid.
  const play = () => {
    const v = videoRef.current;
    if (!v) return;
    const p = v.play();
    if (p && typeof p.catch === "function") p.catch(() => {});
    stopTicking();
    rafRef.current = requestAnimationFrame(tick);
  };

  // A card can unmount mid-hover (a resize that reflows the grid). Without
  // this the rAF loop keeps running against a detached element forever.
  useEffect(() => stopTicking, []);

  // On the FIRST hover the video element does not exist yet — setArmed
  // only queues the render that mounts it, so videoRef.current is still
  // null inside enter() and a play() there is a silent no-op. This runs
  // after that mount. The hoverRef guard covers the visitor who has
  // already moved on by then: without it the clip would play, invisible
  // at opacity 0, for as long as the page is open.
  useEffect(() => {
    if (armed && hoverRef.current) play();
    // `play` is redefined on every render, so listing it here would re-run
    // this effect on every render and restart the clip from the top each
    // time. The effect is meant to fire on one transition only: the render
    // that first mounts the <video>. `armed` is exactly that signal.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [armed]);

  const enter = () => {
    if (reduced) return;
    hoverRef.current = true;
    setArmed(true);
    play();
  };

  const leave = () => {
    hoverRef.current = false;
    stopTicking();
    const v = videoRef.current;
    // Deliberately not resetting currentTime — coming back to a card
    // resumes rather than restarting, which is far calmer when scanning.
    if (v) v.pause();
    // The BAR, however, must reset. Pausing the clip and stopping the rAF
    // leaves the fill frozen wherever it happened to be, so a card you had
    // visited then left sat there greyed-out and quiet with its transport
    // reading 70% — a control stating something untrue about its own
    // state. Seen on a real screenshot, not reasoned about.
    if (barRef.current) barRef.current.style.transform = "scaleX(0)";
    if (timeRef.current) timeRef.current.textContent = "0.0s";
  };

  return (
    <Reveal delay={i * 70} className="xp-cell">
      <Wrap
        className={`xp-card ${d.options ? "has-options" : ""} ${showOptions ? "open" : ""}`}
        {...(d.options
          ? { role: "group", "aria-label": d.label }
          : { href: d.href })}
        onMouseEnter={enter}
        onMouseLeave={leave}
        onFocus={enter}
        onBlur={leave}
      >
        <div className="xp-media">
          <img className="xp-poster" src={`${CDN}/${d.key}.jpg?v=${AV}`} alt="" loading="lazy" decoding="async" />
          {armed && (
            <video
              ref={videoRef}
              className="xp-video"
              src={`${CDN}/${d.key}.mp4?v=${AV}`}
              muted
              loop
              playsInline
              preload="none"
              aria-hidden="true"
            />
          )}
          {/* Warm tint sitting over the desaturated rest state. soft-light
              keeps the plaster and brass readable instead of flooding them,
              which a multiply or a `color` blend would. Fades out as the
              real colour arrives. */}
          <span className="xp-tint" aria-hidden="true" />
          <span className="xp-scrim" aria-hidden="true" />

          {/* Instrumentation. The film is deliberately timeless, so the
              contemporary note comes from the layer READING it — an index,
              a transport light, and a line tracking the loop — rather than
              from anything done to the footage. */}
          <span className="xp-index" aria-hidden="true">{String(i + 1).padStart(2, "0")}</span>
          <span className="xp-lamp" aria-hidden="true" />

          <div className="xp-bar-row">
            <span className="xp-label">{d.label}</span>
            <span className="xp-time" ref={timeRef} aria-hidden="true">0.0s</span>
          </div>
          <span className="xp-track" aria-hidden="true">
            <span className="xp-fill" ref={barRef} />
          </span>
        </div>

        <div className="xp-body">
          <h3 className="xp-title">{d.title}</h3>
          <p className="xp-text">{d.body}</p>

          {d.options ? (
            <>
              <button
                type="button"
                className={`xp-go xp-go-btn ${showOptions ? "open" : ""}`}
                aria-expanded={showOptions}
                onClick={() => setShowOptions((v) => !v)}
              >
                Explore
                <IconArrow size={14} stroke={1.6} />
              </button>
              {/* grid-template-rows rather than a max-height guess, so the
                  panel opens to whatever the two options actually need. */}
              <div className={`xp-options ${showOptions ? "open" : ""}`}>
                <div>
                  {d.options.map((o) => (
                    <a key={o.href} className="xp-option" href={o.href}>
                      <span className="xp-option-label">{o.label}</span>
                      <span className="xp-option-sub">{o.sub}</span>
                      <IconArrow size={13} stroke={1.6} />
                    </a>
                  ))}
                </div>
              </div>
            </>
          ) : (
            <span className="xp-go">
              Explore
              <IconArrow size={14} stroke={1.6} />
            </span>
          )}
        </div>
      </Wrap>
    </Reveal>
  );
};

const SectionExplore = () => {
  return (
    <section className="section-pad xp" id="explore">
      <div className="container-wide">
        <div className="xp-head">
          <Reveal><div className="eyebrow">The platform, module by module</div></Reveal>
          <Reveal delay={120}>
            <h2 className="h-1" style={{ marginTop: 24, maxWidth: 900 }}>
              Eight parts. <span className="italic-accent">One guest.</span>
            </h2>
          </Reveal>
          <Reveal delay={220}>
            <p className="lead" style={{ marginTop: 28, maxWidth: 720 }}>
              Each one is useful on its own. Together they are the reason a guest never has to explain themselves twice.
            </p>
          </Reveal>
        </div>

        <div className="xp-grid">
          {destinations.map((d, i) => <ExploreCard key={d.key} d={d} i={i} />)}
        </div>
      </div>

      <style>{`
        .xp { background: var(--bg); --e: cubic-bezier(0.22, 1, 0.36, 1); }
        .xp-head { margin-bottom: 64px; max-width: 1100px; }

        .xp-grid {
          display: grid;
          grid-template-columns: repeat(4, 1fr);
          gap: 20px;
        }
        /* Reveal is the grid item; the anchor is its child. Both stretch or
           the rows stop sharing a baseline. */
        .xp-cell { display: flex; height: 100%; }
        .xp-card {
          flex: 1;
          display: flex; flex-direction: column;
          border: 1px solid var(--hairline);
          border-radius: 14px;
          overflow: hidden;
          background: var(--bg-elev-1);
          text-decoration: none;
          color: inherit;
          transition: border-color 520ms var(--e), transform 520ms var(--e), background 520ms var(--e);
        }
        .xp-card:hover, .xp-card:focus-visible {
          border-color: var(--accent-glow);
          background: var(--bg-elev-2);
          transform: translateY(-3px);
        }
        .xp-card:focus-visible { outline: 1px solid var(--accent); outline-offset: 2px; }

        /* ---- media ------------------------------------------------ */
        .xp-media {
          position: relative;
          aspect-ratio: 16 / 10;
          overflow: hidden;
          background: var(--bg-elev-3);
        }
        .xp-poster, .xp-video {
          position: absolute; inset: 0;
          width: 100%; height: 100%;
          object-fit: cover;
          display: block;
        }
        /* The poster is a frame of the SAME clip — mostly the resolved
           gesture at ~4.6s, because several of these shots have not begun
           their action at 0.2s and an empty "before" frame reads as a
           broken image. So poster and first video frame do not always
           match, and the 420ms crossfade is doing real work: it lands as a
           soft dissolve within one continuous shot rather than a cut. */
        .xp-video { opacity: 0; transition: opacity 420ms var(--e); }
        .xp-card:hover .xp-video, .xp-card:focus-within .xp-video { opacity: 1; }

        /* ---- the grade ------------------------------------------------
           At rest the footage is held near-monochrome and slightly lifted;
           hovering releases it into full natural colour. The films are
           deliberately timeless, so this is where the contemporary note
           comes from — the treatment, not the footage.

           The filter goes on the poster and the video individually, NOT on
           .xp-media: a filter on the wrapper would desaturate the index,
           the lamp and the label along with the picture, and those are
           meant to stay in brand colour throughout. */
        .xp-poster, .xp-video {
          filter: saturate(0.18) contrast(1.06) brightness(0.97);
          transition: filter 620ms var(--e), transform 900ms var(--e);
        }
        .xp-card:hover .xp-poster, .xp-card:focus-within .xp-poster,
        .xp-card:hover .xp-video, .xp-card:focus-within .xp-video {
          filter: saturate(1) contrast(1) brightness(1);
        }
        .xp-poster { transform: scale(1.04); }
        .xp-card:hover .xp-poster, .xp-card:focus-within .xp-poster { transform: scale(1); }

        .xp-tint {
          position: absolute; inset: 0;
          background: linear-gradient(150deg,
            color-mix(in srgb, var(--accent) 42%, transparent) 0%,
            color-mix(in srgb, var(--accent) 10%, transparent) 60%,
            transparent 100%);
          mix-blend-mode: soft-light;
          opacity: 1;
          transition: opacity 620ms var(--e);
          pointer-events: none;
        }
        .xp-card:hover .xp-tint, .xp-card:focus-within .xp-tint { opacity: 0; }

        /* Darkens the bottom third so the label stays legible over any
           frame of any clip, including the near-white linen ones. */
        .xp-scrim {
          position: absolute; inset: 0;
          background: linear-gradient(to top, rgba(0,0,0,0.62) 0%, rgba(0,0,0,0.14) 46%, transparent 74%);
          pointer-events: none;
        }

        /* ---- instrumentation ------------------------------------------ */
        .xp-index {
          position: absolute; top: 12px; left: 14px;
          font-family: var(--f-mono);
          font-size: 10px; letter-spacing: 0.12em;
          color: rgba(255,255,255,0.62);
          transition: color 420ms var(--e);
        }
        .xp-card:hover .xp-index, .xp-card:focus-within .xp-index { color: #fff; }

        /* The transport light. Dim and grey while parked; lit while the
           clip is actually running. */
        .xp-lamp {
          position: absolute; top: 15px; right: 14px;
          width: 5px; height: 5px; border-radius: 50%;
          background: rgba(255,255,255,0.38);
          transition: background 420ms var(--e), box-shadow 420ms var(--e);
        }
        .xp-card:hover .xp-lamp, .xp-card:focus-within .xp-lamp {
          background: var(--accent);
          box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 30%, transparent);
        }

        .xp-bar-row {
          position: absolute; left: 14px; right: 14px; bottom: 17px;
          display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
          transition: transform 520ms var(--e);
        }
        .xp-card:hover .xp-bar-row, .xp-card:focus-within .xp-bar-row { transform: translateY(-2px); }
        .xp-label {
          font-family: var(--f-mono);
          font-size: 10px; letter-spacing: 0.13em; text-transform: uppercase;
          color: #fff;
        }
        /* The readout is meaningless until something is playing, so it is
           invisible at rest rather than sitting there at a frozen 0.0s. */
        .xp-time {
          font-family: var(--f-mono);
          font-size: 9.5px; letter-spacing: 0.06em;
          color: rgba(255,255,255,0.7);
          opacity: 0;
          transition: opacity 420ms var(--e);
          font-variant-numeric: tabular-nums;
        }
        .xp-card:hover .xp-time, .xp-card:focus-within .xp-time { opacity: 1; }

        /* The track appears with the readout rather than sitting there at
           rest. A transport bar under a still frame has nothing to report,
           and at 26% white over the warm grade it read as a pale fuchsia
           rule directly under the label — indistinguishable from a bar
           already part-filled. The instrumentation is the reward for
           pointing at the card, not furniture on all eight. */
        .xp-track {
          position: absolute; left: 14px; right: 14px; bottom: 11px;
          height: 1px; overflow: hidden;
          background: rgba(255,255,255,0.28);
          opacity: 0;
          transition: opacity 420ms var(--e);
        }
        .xp-card:hover .xp-track, .xp-card:focus-within .xp-track { opacity: 1; }
        .xp-fill {
          display: block; width: 100%; height: 100%;
          background: var(--accent);
          transform: scaleX(0); transform-origin: left center;
        }

        /* ---- body -------------------------------------------------- */
        .xp-body {
          display: flex; flex-direction: column; gap: 10px;
          padding: 20px 18px 18px;
          flex: 1;
        }
        .xp-title {
          font-family: var(--f-display);
          font-size: 21px; line-height: 1.15; letter-spacing: -0.01em;
          margin: 0;
        }
        .xp-text {
          font-size: 13px; line-height: 1.55; color: var(--ink-3);
          margin: 0;
        }
        /* margin-top:auto pins the CTA to the bottom so all eight align
           regardless of how long the body copy runs. */
        .xp-go {
          margin-top: auto; padding-top: 14px;
          display: inline-flex; align-items: center; gap: 7px;
          font-size: 12px; font-weight: 500;
          color: var(--ink-3);
          transition: color 420ms var(--e), gap 420ms var(--e);
        }
        .xp-card:hover .xp-go, .xp-card:focus-within .xp-go {
          color: var(--accent); gap: 11px;
        }

        /* ---- two-destination card ---------------------------------- */
        /* A div, not an anchor, so it needs the pointer and the focus ring
           the anchor gave it for free. */
        .xp-card.has-options { cursor: default; }
        .xp-go-btn {
          background: none; border: 0; padding: 14px 0 0;
          font: inherit; cursor: pointer;
          width: 100%;
        }
        .xp-go-btn .arrow, .xp-go-btn svg { transition: transform 420ms var(--e); }
        .xp-go-btn.open svg { transform: rotate(90deg); }
        .xp-go-btn:focus-visible { outline: 1px solid var(--accent); outline-offset: 3px; border-radius: 6px; }

        .xp-options {
          display: grid;
          grid-template-rows: 0fr;
          transition: grid-template-rows 380ms var(--e);
        }
        .xp-options.open { grid-template-rows: 1fr; }
        .xp-options > div { overflow: hidden; }
        .xp-option {
          display: grid;
          grid-template-columns: 1fr auto;
          align-items: center;
          gap: 4px 10px;
          padding: 11px 12px;
          margin-top: 8px;
          border: 1px solid var(--hairline);
          border-radius: 10px;
          background: var(--bg);
          text-decoration: none;
          transition: border-color 320ms var(--e), background 320ms var(--e), transform 320ms var(--e);
        }
        .xp-option:hover, .xp-option:focus-visible {
          border-color: var(--accent-glow);
          background: var(--bg-elev-2);
          transform: translateX(2px);
        }
        .xp-option svg { color: var(--accent); grid-row: span 2; }
        .xp-option-label { font-size: 13.5px; font-weight: 500; color: var(--ink); }
        .xp-option-sub {
          grid-column: 1;
          font-family: var(--f-mono);
          font-size: 9.5px; letter-spacing: 0.1em; text-transform: uppercase;
          color: var(--ink-3);
        }

        @media (max-width: 1180px) { .xp-grid { grid-template-columns: repeat(3, 1fr); } }
        @media (max-width: 860px)  { .xp-grid { grid-template-columns: repeat(2, 1fr); } }
        @media (max-width: 520px)  { .xp-grid { grid-template-columns: 1fr; } }

        @media (prefers-reduced-motion: reduce) {
          .xp-card, .xp-poster, .xp-video, .xp-tint, .xp-bar-row,
          .xp-index, .xp-lamp, .xp-time, .xp-go,
          .xp-options, .xp-option, .xp-go-btn svg {
            transition-duration: 1ms !important;
          }
          .xp-poster { transform: none; }
          /* No clip ever plays here, so the transport readout would sit at
             a permanent 0.0s next to a bar that never moves — a control
             that is visibly lying about its state. Remove both. */
          .xp-track, .xp-time { display: none; }
        }
      `}</style>
    </section>
  );
};

window.SectionExplore = SectionExplore;
