// Just Travel — Landing Page sections (React via Babel)
// Loaded after React/Babel + tweaks-panel.jsx + utils.jsx

const { useState, useEffect, useRef } = React;

/* The icon set lives in landing-icons.jsx (Material Symbols Sharp), loaded
   before this file on every page. `Icon` and its older alias `SIcon` come from
   there; nothing declares glyphs locally any more. */

/* ------------------------------------------------------------
   Fragment jump for the client-rendered pages. When a URL arrives with a
   hash (Privacidade.html#lgpd, Cases.html#resultados, …) the browser tries
   to jump before React has rendered anything, so the target doesn't exist
   yet and the page stays at the top. Retry for a few frames until it shows
   up. Harmless where the markup is already there — the first try wins.
   ------------------------------------------------------------ */
(function hashJump() {
  // Skipped during the build's prerender pass, which runs this file in a VM
  // with no browser globals.
  if (typeof location === "undefined" || typeof requestAnimationFrame === "undefined") return;
  const id = decodeURIComponent(location.hash.slice(1));
  if (!id || id === "top") return;
  let tries = 0;
  const tick = () => {
    const target = document.getElementById(id);
    if (target) { target.scrollIntoView(); return; }
    if (tries++ < 30) requestAnimationFrame(tick);
  };
  requestAnimationFrame(tick);
})();

/* ============================================================
   Hooks
   ============================================================ */
function prefersReducedMotion() {
  return document.body.dataset.anim === "off" ||
    (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches);
}

function useInView(opts = { threshold: 0.18 }) {
  const ref = useRef(null);
  const [inView, setInView] = useState(false);
  useEffect(() => {
    if (!ref.current) return;
    const io = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) {setInView(true);io.disconnect();}
    }, opts);
    io.observe(ref.current);
    return () => io.disconnect();
  }, []);
  return [ref, inView];
}

function Reveal({ children, delay = 0, as: Tag = "div", className = "", style = {}, ...rest }) {
  const [ref, inView] = useInView();
  return (
    <Tag ref={ref}
    className={`reveal ${inView ? "is-in" : ""} ${className}`}
    style={{ "--reveal-delay": `${delay}ms`, ...style }}
    {...rest}>
      {children}
    </Tag>);

}

/* ============================================================
   1) Navbar
   ============================================================ */
/* One tree, two renderings. NAV_TREE feeds both the mega panels on the top bar
   and the hamburger sitemap, which used to be two hand-kept arrays that had
   already drifted: Comparativo.html was in the mega and missing from the
   sitemap, and since `.nav__center` is hidden below 880px that page was
   unreachable on a phone.

   The rule the tree follows: every label points at something that exists, and
   no two labels in one menu point at the same place. The footer was cleaned up
   first (see FOOTER_NAV in landing-footer.jsx); the navbar had kept 67 links
   across 17 destinations, with labels like "Integrações" and "APIs" landing on
   a page that says nothing about either, and eleven product labels all landing
   on the same anchor.

   The panel keeps the layout it always had: an eyebrow-led first cell, then
   columns of eyebrow + bold links. Only the contents changed. Do not invent a
   second panel shape for the shorter menus.

   Shape of a group:
     href        the group label is itself a link (Produtos, Preços)
     lead        first cell: { eyebrow, title, href? }. Without `href` the title
                 renders as plain text, which is how a menu whose destinations
                 are all in the columns avoids a lead that duplicates one of them
     cols        [{ eyebrow, items: [{ label, href }] }], or a function
                 returning that, for lists built from data declared further down
     sitemapOnly out of the top bar, hamburger only (Empresa) */

/* Just Academy is hosted outside this site, so it is the one off-domain
   destination in the navbar. It ships null-guarded on purpose: while the URL is
   missing the item is skipped rather than rendered dead, because a menu entry
   that 404s is worse than one that waits. Set the real address to publish it. */
const NAV_HREF_ACADEMY = null;

/* Cases is unpublished until it carries real customer stories: the invented
   agencies and quotes were removed, so the page is noindexed and its two nav
   entries ship null-guarded, the same way Just Academy waits for its URL. They
   return, with their hrefs, once approved cases exist. */
const NAV_HREF_CASES = null;

/* Products get one page with one anchor per category, not a page each. The
   taxonomy itself is CATEGORIES, further down this file. */
const PRODUCT_HREF = (slug) => `Produtos.html#${slug}`;

const NAV_TREE = [
  {
    id: "plataforma", label: "Plataforma",
    lead: { eyebrow: "Visão de produto", title: "Conheça a plataforma", href: "Whitelabel.html" },
    cols: [
      { eyebrow: "Ecossistema", items: [{ label: "Parceiros", href: "Parceiros.html#tecnologia" }] },
      { eyebrow: "Confiança",   items: [{ label: "Segurança & Conformidade", href: "Seguranca.html" }] },
    ],
  },
  {
    id: "produtos", label: "Produtos", href: "Produtos.html", selfLabel: "Todos os produtos",
    lead: { eyebrow: "Catálogo", title: "+1 milhão de produtos turísticos", href: "Produtos.html" },
    /* Built from CATEGORIES, the site's product taxonomy, which is declared
       further down this file: reading it at module level would hit the temporal
       dead zone, so the panel asks for it at render time. The groupings below
       are display only, and the reading order matches the order of the page. */
    cols: () => {
      const bySlug = Object.fromEntries(CATEGORIES.map((c) => [c.slug, c]));
      const col = (slugs) => slugs
        .filter((s) => bySlug[s])
        .map((s) => ({ label: bySlug[s].name, href: PRODUCT_HREF(s) }));
      return [
        { eyebrow: "Ingressos e parques",     items: col(["ingressos", "parques", "eventos", "esportivos"]) },
        { eyebrow: "Hospedagem e transporte", items: col(["hospedagens", "cruzeiros", "transportes"]) },
        { eyebrow: "Experiências e extras",   items: col(["experiencias", "guias", "seguros", "exclusivos"]) },
      ];
    },
  },
  {
    id: "solucao", label: "Soluções", sitemapLabel: "Soluções e clientes",
    /* No href on the lead: every destination in this menu is in the columns,
       and a lead pointing at Cases.html would just be a second "Cases de
       sucesso" in the same panel. */
    lead: { eyebrow: "Para quem é", title: "Agências, operadoras e redes vendendo no próprio nome." },
    cols: [
      { eyebrow: "Clientes",  items: [
        { label: "Cases de sucesso", href: NAV_HREF_CASES },
        { label: "Resultados",       href: NAV_HREF_CASES },
      ]},
      { eyebrow: "Comparação", items: [{ label: "Just Travel vs. OTAs", href: "Comparativo.html" }] },
      { eyebrow: "Parcerias",  items: [{ label: "Programas de parceria", href: "Parceiros.html#programas" }] },
    ],
  },
  { id: "precos", label: "Preços", href: "Precos.html", selfLabel: "Planos e preços" },
  {
    id: "recursos", label: "Recursos",
    lead: { eyebrow: "Recursos", title: "Conteúdo para a sua agência crescer." },
    cols: [
      { eyebrow: "Conteúdo", items: [
        { label: "Blog",         href: "Blog.html" },
        { label: "Just Academy", href: NAV_HREF_ACADEMY },
      ]},
      { eyebrow: "Confiança", items: [{ label: "Segurança & Conformidade", href: "Seguranca.html" }] },
    ],
  },
  /* Empresa is the only group with no menu of its own on the top bar, so it is
     what the hamburger is *for*: on desktop the panel is this column and
     nothing else (see .nav__sitemap-col--topbar). That makes it the
     institutional menu, quem somos / trabalhar aqui / falar com a gente, so it
     carries Vagas.html, the openings listing that had no entry in the navbar,
     and no longer repeats Parceiros or Segurança, which the Plataforma panel
     already owns. Anything belonging to the product story stays in its group. */
  {
    id: "empresa", label: "Empresa", sitemapOnly: true,
    cols: [
      { items: [
        { label: "Sobre nós",        href: "Sobre.html" },
        { label: "Trabalhe conosco", href: "Trabalhe-Conosco.html" },
        { label: "Vagas abertas",    href: "Vagas.html" },
        { label: "Contato",          href: "Agendar-Demo.html" },
      ]},
    ],
  },
];

/* Columns resolve late (Produtos reads CATEGORIES) and drop any item still
   waiting for a destination, which is what keeps NAV_HREF_ACADEMY safe: while
   it is null the item is skipped instead of rendering a dead link. An emptied
   column drops with it, so no stray eyebrow is left behind. */
const navCols = (group) => {
  const cols = typeof group.cols === "function" ? group.cols() : (group.cols || []);
  return cols
    .map((c) => ({ ...c, items: c.items.filter((it) => Boolean(it.href)) }))
    .filter((c) => c.items.length > 0);
};
const navItems = (group) => navCols(group).flatMap((c) => c.items);

/* The hamburger is the whole tree, one column per group, because `.nav__center`
   disappears below 880px and this becomes the only navigation on a phone: that
   is how Comparativo.html used to vanish there. A group that is itself a page
   leads its own column.

   Above that breakpoint the tree is on screen twice, and the panel was five
   columns of links the top bar already carried. So every column that has a menu
   on the top bar is tagged `topBar` and the stylesheet hides it while
   `.nav__center` is visible, leaving the desktop panel as the institutional
   menu (Empresa, the one group with no top-bar menu). Same tree, same markup,
   one rendering: the phone still gets all of it. */
const navSitemap = () => {
  /* The whole sitemap is one menu, so `seen` spans every column, not just the
     one being built: the first group that claims a destination keeps it and the
     later ones drop the repeat (Segurança stays under Plataforma and leaves
     Recursos). That is what stops the phone menu from growing back into the
     same page listed under three headings. */
  const seen = new Set();
  return NAV_TREE
    .map((g) => {
      /* Three sources, in the order they should read: the group's own page, the
         destination behind the panel's lead cell, then the columns. The lead one
         matters: "Conheça a plataforma" is only a lead, so leaving it out dropped
         Whitelabel.html from the phone menu entirely. Deduped by href, because
         the products lead points at the same page as the group. */
      const items = [
        ...(g.href ? [{ label: g.selfLabel || g.label, href: g.href }] : []),
        ...(g.lead && g.lead.href ? [{ label: g.lead.title, href: g.lead.href }] : []),
        ...navItems(g),
      ].filter((it) => !seen.has(it.href) && seen.add(it.href));
      return { id: g.id, h: g.sitemapLabel || g.label, topBar: !g.sitemapOnly, items };
    })
    .filter((col) => col.items.length > 0);
};

function NavMegaPanel({ group }) {
  const cols = navCols(group);
  const lead = group.lead;
  return (
    <div className="nav__mega-inner">
      <div className="nav__mega-grid">
        {lead &&
        <div className="nav__mega-lead">
            <div className="nav__mega-eyebrow">{lead.eyebrow}</div>
            {lead.href ?
            <a className="nav__mega-lead-title" href={lead.href}>{lead.title}</a> :
            <span className="nav__mega-lead-title">{lead.title}</span>}
          </div>
        }
        {cols.map((col, ci) =>
          <div key={ci} className="nav__mega-col">
            {col.eyebrow && <div className="nav__mega-eyebrow">{col.eyebrow}</div>}
            <ul>
              {col.items.map((it) =>
                <li key={it.label}><a className="nav__mega-link" href={it.href}>{it.label}</a></li>
              )}
            </ul>
          </div>
        )}
      </div>
    </div>);

}


function Navbar() {
  const [scrolled, setScrolled] = useState(false);
  const [active, setActive] = useState(null);
  const [siteOpen, setSiteOpen] = useState(false);
  const [globeSpin, setGlobeSpin] = useState(0);
  const [langOpen, setLangOpen] = useState(false);
  const [lang, setLang] = useState("pt");
  // Theme starts "light" for a deterministic prerender/first paint; the effect
  // below syncs it to whatever the anti-FOUC bootstrap script already applied
  // to <html> (stored choice or OS preference), so hydration never mismatches.
  const [theme, setTheme] = useState("light");
  const langCloseTimer = useRef(null);
  const closeTimer = useRef(null);
  const siteCloseTimer = useRef(null);

  // Apply a theme to <html>, the address-bar color, and React state (so the
  // toggle icon flips). Persisting the choice is the caller's job: the toggle
  // stores it so it pins over the OS; the OS listener below never persists.
  const applyTheme = (next) => {
    document.documentElement.dataset.theme = next;
    const meta = document.querySelector('meta[name="theme-color"]');
    if (meta) meta.setAttribute("content", next === "dark" ? "#0c1319" : "#E6F6FD");
    setTheme(next);
  };

  useEffect(() => {
    // Reflect whatever the anti-FOUC bootstrap already applied to <html>.
    setTheme(document.documentElement.dataset.theme === "dark" ? "dark" : "light");
    // Live-sync to the OS as long as the user hasn't made a manual choice — a
    // stored "jt-theme" (written by the toggle) pins the theme and opts out.
    const mq = window.matchMedia("(prefers-color-scheme: dark)");
    const onChange = (e) => {
      let stored = null;
      try { stored = localStorage.getItem("jt-theme"); } catch (_) {}
      if (stored === "dark" || stored === "light") return;
      applyTheme(e.matches ? "dark" : "light");
    };
    mq.addEventListener("change", onChange);
    return () => mq.removeEventListener("change", onChange);
  }, []);

  const toggleTheme = () => {
    const next = document.documentElement.dataset.theme === "dark" ? "light" : "dark";
    try { localStorage.setItem("jt-theme", next); } catch (_) {}
    applyTheme(next);
  };

  // Glass panel behind the navbar fades in once the hero scrolls out from
  // under it. Prefer an IntersectionObserver on the hero (no scroll thrash);
  // fall back to a scroll threshold if the hero isn't present.
  useEffect(() => {
    const hero = document.querySelector(".hero");
    if (hero && "IntersectionObserver" in window) {
      const io = new IntersectionObserver(
        ([entry]) => setScrolled(!entry.isIntersecting),
        // Nudge the root's top edge down to the navbar's bottom (~80px) so the
        // flip lands exactly as the hero clears the floating pill.
        { rootMargin: "-80px 0px 0px 0px", threshold: 0 });
      io.observe(hero);
      return () => io.disconnect();
    }
    const onScroll = () => setScrolled(window.scrollY > 24);
    window.addEventListener("scroll", onScroll, { passive: true });
    onScroll();
    return () => window.removeEventListener("scroll", onScroll);
  }, []);

  // Copy isn't translated yet, but the document's lang metadata should never
  // lie about what's selected — keeps screen readers/spellcheck/hyphenation correct.
  useEffect(() => {
    document.documentElement.lang = { pt: "pt-BR", en: "en-US", es: "es" }[lang] || "pt-BR";
  }, [lang]);

  const openMega = (id) => {
    if (closeTimer.current) {clearTimeout(closeTimer.current);closeTimer.current = null;}
    if (siteCloseTimer.current) clearTimeout(siteCloseTimer.current);
    setSiteOpen(false);
    setActive(id);
  };
  const scheduleClose = () => {
    if (closeTimer.current) clearTimeout(closeTimer.current);
    closeTimer.current = setTimeout(() => setActive(null), 180);
  };

  const openSite = () => {
    if (siteCloseTimer.current) { clearTimeout(siteCloseTimer.current); siteCloseTimer.current = null; }
    if (closeTimer.current) clearTimeout(closeTimer.current);
    setActive(null);
    setSiteOpen(true);
  };
  const scheduleCloseSite = () => {
    if (siteCloseTimer.current) clearTimeout(siteCloseTimer.current);
    siteCloseTimer.current = setTimeout(() => setSiteOpen(false), 180);
  };

  const LANGS = [
  { id: "pt", code: "PT", label: "Português", note: "Brasil" },
  { id: "en", code: "EN", label: "English", note: "United States" },
  { id: "es", code: "ES", label: "Español", note: "Internacional" }];
  const activeLang = LANGS.find((l) => l.id === lang) || LANGS[0];

  const openLang = () => {
    if (langCloseTimer.current) { clearTimeout(langCloseTimer.current); langCloseTimer.current = null; }
    if (closeTimer.current) clearTimeout(closeTimer.current);
    setActive(null);
    setSiteOpen(false);
    setLangOpen(true);
  };
  const scheduleCloseLang = () => {
    if (langCloseTimer.current) clearTimeout(langCloseTimer.current);
    langCloseTimer.current = setTimeout(() => setLangOpen(false), 260);
  };

  const langRef = useRef(null);
  useEffect(() => {
    if (!langOpen) return;
    const onDown = (e) => { if (langRef.current && !langRef.current.contains(e.target)) setLangOpen(false); };
    const onKey = (e) => { if (e.key === "Escape") setLangOpen(false); };
    document.addEventListener("pointerdown", onDown);
    document.addEventListener("keydown", onKey);
    return () => { document.removeEventListener("pointerdown", onDown); document.removeEventListener("keydown", onKey); };
  }, [langOpen]);

  // Top bar and hamburger both come off NAV_TREE, so a destination can never
  // exist in one and be missing from the other again.
  const topGroups = NAV_TREE.filter((g) => !g.sitemapOnly);
  const activeGroup = NAV_TREE.find((g) => g.id === active) || null;

  const overlayOpen = Boolean(active) || siteOpen;

  return (
    <>
      {/* Frosted scrim over the page (never over the navbar or the open panel,
          which both sit above it) so an open menu reads as the only live layer.
          Purely decorative: pointer-events stay off, closing is still handled
          by the pointer leaving the navbar. */}
      <div className={`nav-scrim ${overlayOpen ? "nav-scrim--on" : ""}`} aria-hidden="true"></div>
      <div className={`nav-glass ${scrolled ? "nav-glass--on" : ""}`} aria-hidden="true"></div>
      <nav className={`nav ${scrolled ? "nav--scrolled" : ""}`}>
        <div className="nav__shell" onMouseLeave={scheduleClose} style={{ backgroundColor: "var(--just-blue-tint)" }}>
          <div className="nav__brand">
            <a href="/" aria-label="Just Travel, página inicial">
              <img src="assets/logo-horizontal.png" alt="just travel" className="nav__logo" width="140" height="28" />
            </a>
          </div>

          <div className="nav__center">
            {topGroups.map((g) => {
              const hasMega = navItems(g).length > 0;
              const caret = hasMega &&
                <span className="nav__caret"><Icon name="caretDown" size={12} /></span>;

              /* A group that is also a page stays a link, and opens its panel
                 on hover or focus instead of swallowing the click. Produtos is
                 the case this exists for: the page is the destination, the
                 panel is the shortcut to a section of it. */
              return g.href ?
              <a
                key={g.id}
                className="nav__link"
                href={g.href}
                aria-expanded={hasMega ? active === g.id : undefined}
                onMouseEnter={() => hasMega ? openMega(g.id) : scheduleClose()}
                onFocus={() => hasMega ? openMega(g.id) : scheduleClose()}>

                  {g.label}
                  {caret}
                </a> :

              <button
                key={g.id}
                className="nav__link"
                aria-expanded={active === g.id}
                onMouseEnter={() => openMega(g.id)}
                onFocus={() => openMega(g.id)}
                onClick={() => setActive(active === g.id ? null : g.id)}>

                  {g.label}
                  {caret}
                </button>;

            })}
          </div>

          <div className="nav__right">
            <div className="nav__lang" ref={langRef} onMouseLeave={scheduleCloseLang}>
              <button
                className={`nav__icon-btn nav__icon-btn--caret ${langOpen ? "is-open" : ""}`}
                aria-label="Idioma"
                aria-expanded={langOpen}
                onClick={() => { setGlobeSpin((s) => s + 1); setLangOpen((o) => !o); }}>
                <span className="nav__globe" style={{ transform: `rotate(${globeSpin * 360}deg)` }}>
                  <Icon name="globe" size={18} />
                </span>
                <Icon name="caretDown" size={10} />
              </button>

              <div className={`nav__lang-menu ${langOpen ? "is-open" : ""}`} role="menu">
                {LANGS.map((l) =>
                <button
                  key={l.id}
                  role="menuitemradio"
                  aria-checked={lang === l.id}
                  className={`nav__lang-item ${lang === l.id ? "is-active" : ""}`}
                  onClick={() => { setLang(l.id); setLangOpen(false); }}>
                  <span className="nav__lang-item-text">
                    <span className="nav__lang-item-label">{l.label}</span>
                    <span className="nav__lang-item-note">{l.note}</span>
                  </span>
                  {lang === l.id &&
                  <span className="nav__lang-check">
                    <Icon name="check" size={16} />
                  </span>}
                </button>)}
              </div>
            </div>
            <a className="btn btn--blue btn--md" href="/#precos" onMouseEnter={scheduleClose}>
              Começar grátis
              <span className="btn__arrow"><SIcon name="btnArrow" /></span>
            </a>
            <a className="btn btn--outline btn--md" href="Agendar-Demo.html" onMouseEnter={scheduleClose}>
              Agendar demo
              <span className="btn__arrow"><SIcon name="btnArrow" /></span>
            </a>
            <div className="nav__divider"></div>
            <button
              className="nav__icon-btn nav__icon-btn--theme"
              aria-label={theme === "dark" ? "Ativar modo claro" : "Ativar modo noturno"}
              aria-pressed={theme === "dark"}
              title={theme === "dark" ? "Modo claro" : "Modo noturno"}
              onClick={toggleTheme}
              onMouseEnter={() => { scheduleClose(); scheduleCloseSite(); }}>
              {theme === "dark" ? <Icon name="sun" size={20} /> : <Icon name="moon" size={19} />}
            </button>
            <button
              className="nav__icon-btn nav__icon-btn--burger"
              aria-label={siteOpen ? "Fechar menu" : "Abrir menu"}
              aria-expanded={siteOpen}
              onMouseEnter={openSite}
              onFocus={openSite}
              onClick={() => setSiteOpen(!siteOpen)}
            >
              {siteOpen ? <Icon name="close" size={22}/> : <Icon name="menu" size={22}/>}
            </button>
          </div>
        </div>
      </nav>

      <div className="nav__mega-wrap" aria-hidden={!active}>
        <div
          className={`nav__mega ${active ? "is-open" : ""}`}
          onMouseEnter={() => active && openMega(active)}
          onMouseLeave={scheduleClose}>

          {activeGroup && <NavMegaPanel group={activeGroup} key={activeGroup.id} />}
        </div>
      </div>

      <div className="nav__sitemap-wrap" aria-hidden={!siteOpen}>
        <div
          className={`nav__sitemap ${siteOpen ? "is-open" : ""}`}
          onMouseEnter={openSite}
          onMouseLeave={scheduleCloseSite}>
          <div className="nav__sitemap-grid">
            {navSitemap().map((col) =>
              <div
                key={col.id}
                className={`nav__sitemap-col${col.topBar ? " nav__sitemap-col--topbar" : ""}`}>
                <h4>{col.h}</h4>
                <ul>
                  {col.items.map((it) =>
                    <li key={it.label}><a href={it.href}>{it.label}</a></li>
                  )}
                </ul>
              </div>
            )}
          </div>

          {/* What the top bar hands over below 880px. There the pill is only
              the logo, the language switcher and this button (see
              .nav__right's responsive block in landing-styles.css): three
              controls is what fits at 375px, and the two calls to action plus
              the theme toggle would otherwise simply be gone on a phone. They
              are rendered here on every page and hidden above 880px by
              .nav__sitemap-foot, so the desktop panel is untouched. */}
          <div className="nav__sitemap-foot">
            <a className="btn btn--blue btn--md nav__sitemap-cta" href="/#precos">
              Começar grátis
              <span className="btn__arrow"><SIcon name="btnArrow" /></span>
            </a>
            <a className="btn btn--outline btn--md nav__sitemap-cta" href="Agendar-Demo.html">
              Agendar demo
              <span className="btn__arrow"><SIcon name="btnArrow" /></span>
            </a>
            <button
              className="nav__sitemap-theme"
              aria-label={theme === "dark" ? "Ativar modo claro" : "Ativar modo noturno"}
              aria-pressed={theme === "dark"}
              onClick={toggleTheme}>
              {theme === "dark" ? <Icon name="sun" size={19} /> : <Icon name="moon" size={18} />}
              <span>{theme === "dark" ? "Modo claro" : "Modo noturno"}</span>
            </button>
          </div>
        </div>
      </div>
    </>);

}

/* ============================================================
   3) Supplier and destination logos strip

   Proof of inventory, not of customer base: the brands whose products the
   agency resells through the platform, plus the destinations the catalogue
   covers.

   It is rendered by ProductCatalog, at the bottom of that section, not from the
   page's own list of sections: the strip is the evidence under the catalogue,
   and it is the one place on the page where the brands sit next to the products
   they belong to. It keeps its own <section> and label because it is still a
   distinct region, but no background of its own, so it continues the white the
   catalogue is on.

   Two lists feed one marquee track. Keeping them separate is not cosmetic:
   SUPPLIERS is a claim that the platform sells that product, DESTINATIONS only
   claims coverage of the destination. Do not merge them.

   `file` is the logo asset under assets/logos/ (SVG preferred, WebP if only
   a raster exists), produced by scripts/partner-logos.mjs. While it is null the
   brand renders as a wordmark in the display face, which is a deliberate,
   presentable fallback rather than a missing image. Fill `file` in as each
   brand is cleared for use.

   Two entries stay on the wordmark on purpose, and neither is waiting for an
   asset. Celebration Suites has artwork (it is on Parceiros.html, in colour),
   but the mark is a dense filled shield with a confetti-strewn monogram: under
   this track's treatment, a flat monochrome silhouette 28px tall, it collapses
   into a blob exactly the way the Greater Miami box and the Orlando Magic
   emblem do below. The wordmark is the more legible of the two options here.
   oParks has no asset yet.
   ============================================================ */
/* Order matters for exactly one reason, and it is not alphabetical or
   commercial: the two entries that render as a WORDMARK have to sit as far
   apart in the track as the loop allows. Side by side (they used to be at 7
   and 8) the pair reads as a gap in a row of logos, because a 20px display-face
   name is a different kind of object from a 28px silhouette, and two of them
   together look like the strip is missing artwork rather than carrying a
   deliberate fallback.

   The distance that matters is in PIXELS, not in list positions, because the
   items are nothing like the same width: Las Vegas renders 51px wide and Visit
   Florida 183px. Counting items is what makes this look solved when it is not.
   Opposite ends of the 23-item track (11 one way, 12 the other) measures
   1388px against 2220px, because the nine destination boards on the way round
   are the widest marks in the set.

   The track is a 3607px loop, so the best any pair can do is a 1772/1835 split,
   and that is what these positions are: oParks first, Celebration Suites last,
   with the whole destinations run sitting between them on the way round. The
   pair is never in frame together at any width the site is used at.

   oParks going first is not a demotion by accident: .logos__track-wrap fades
   80px at each edge, so at first paint the item at x=0 is the one least seen,
   which is the right place for the entry with no artwork yet. If a wordmark
   gains its asset, or a brand joins either list, re-measure before reordering
   on any other grounds. */
const SUPPLIERS = [
  { name: "oParks", file: null },
  { name: "Disney", file: "disney.webp" },
  { name: "Universal Orlando", file: "universal.webp" },
  { name: "SeaWorld", file: "seaworld.webp" },
  { name: "Discovery Cove", file: "discovery-cove.webp" },
  { name: "Legoland", file: "legoland.webp" },
  { name: "Broadway Inbound", file: "broadway-inbound.webp" },
  { name: "Meliá", file: "melia.webp" },
  { name: "Accor", file: "accor.webp" },
  { name: "Wyndham", file: "wyndham.webp" },
  { name: "Hilton", file: "hilton.webp" },
  { name: "MSC Cruzeiros", file: "msc-cruzeiros.webp" },
  { name: "Assist Card", file: "assist-card.webp" },
  { name: "Celebration Suites", file: null },
];

/* Destination marketing organisations, in the same track as the suppliers.
   Kept in a separate list because they are not suppliers: they are the official
   tourism boards of the destinations the catalogue covers, which is why the
   claim above the track names "marcas e destinos" rather than "fornecedores".

   Three of the imported destination marks are missing here on purpose. The
   track renders every logo as a flat monochrome silhouette at 28px, and the
   Greater Miami box, the Orlando Magic emblem and the Visit The USA dot matrix
   all turn to mush at that size and treatment. They are on Parceiros.html
   instead, in colour and at a size where they read. */
const DESTINATIONS = [
  { name: "Visit Orlando", file: "visit-orlando.webp" },
  { name: "Visit Florida", file: "visit-florida.webp" },
  { name: "Visit Central Florida", file: "visit-central-florida.webp" },
  { name: "Experience Kissimmee", file: "kissimmee.webp" },
  { name: "Visit Lauderdale", file: "visit-lauderdale.webp" },
  { name: "Las Vegas", file: "las-vegas.webp" },
  { name: "NYC The Official Guide", file: "nyc-guide.webp" },
  { name: "Visit California", file: "visit-california.webp" },
  { name: "Visit Europe", file: "visit-europe.webp" },
];

function SupplierLogo({ supplier }) {
  const [broken, setBroken] = useState(false);
  if (!supplier.file || broken) {
    return <span className="supplier-logo supplier-logo--word">{supplier.name}</span>;
  }
  /* Two layers, not a filter: a CSS filter can only approximate a target
     colour (hue-rotate/sepia math), never land on the token's exact hex. The
     real logo sits underneath at full colour always; a tint layer shaped by
     the same file as a mask (so it always matches the logo's own silhouette,
     not a bounding box) sits on top at var(--just-blue) and fades out on
     hover, in landing-styles.css. */
  const url = `url(assets/logos/${supplier.file})`;
  return (
    <span className="supplier-logo">
      <img
        className="supplier-logo__img"
        src={`assets/logos/${supplier.file}`}
        alt={supplier.name}
        loading="lazy"
        onError={() => setBroken(true)} />
      <span
        className="supplier-logo__tint"
        aria-hidden="true"
        style={{ WebkitMaskImage: url, maskImage: url }} />
    </span>);

}

function SupplierLogos() {
  const logos = [...SUPPLIERS, ...DESTINATIONS].map((s, i) => (
    <SupplierLogo key={i} supplier={s} />
  ));
  return (
    <section className="logos" aria-label="Fornecedores e destinos conectados à plataforma">
      <div className="logos__track-wrap">
        {/* Duplicated once so the marquee can loop at -50% with no seam. */}
        <div className="logos__track">
          {logos}
          <span aria-hidden="true" className="logos__clone">{logos}</span>
        </div>
      </div>
      {/* The claim stays inside the measured column, because it is prose and
          has to hold a readable line length. The track above is deliberately
          outside it, so the marquee runs the full width of the page. Now
          below the track rather than above it, and carrying the weight of a
          headline rather than a caption — see .logos__claim. */}
      <div className="container">
        <p className="logos__claim">
          <strong>+R$ 1 bilhão</strong> vendidos pelas agências que operam na Just Travel, com as marcas<br /> e os destinos que você já vende.
        </p>
      </div>
    </section>);

}

/* ============================================================
   4) Animated metrics
   ============================================================ */
function Counter({ to, duration = 1800, format = (n) => n.toLocaleString("pt-BR") }) {
  const [val, setVal] = useState(0);
  const [ref, inView] = useInView({ threshold: 0.4 });
  useEffect(() => {
    if (!inView) return;
    if (prefersReducedMotion()) {setVal(to);return;}
    let raf, start;
    const step = (t) => {
      if (!start) start = t;
      const p = Math.min(1, (t - start) / duration);
      // ease-out cubic
      const eased = 1 - Math.pow(1 - p, 3);
      setVal(Math.round(to * eased));
      if (p < 1) raf = requestAnimationFrame(step);
    };
    raf = requestAnimationFrame(step);
    return () => cancelAnimationFrame(raf);
  }, [inView, to, duration]);
  return <span ref={ref}>{format(val)}</span>;
}

/* ------------------------------------------------------------
   Proof placement: one owner per number.

   Every figure on this page is stated exactly once, in the section whose
   argument it actually proves, so the page compounds instead of repeating:

     hero ................. no figure                   (headline + render only)
     this section ......... um ano de operação: ingressos, pedidos, clientes
     product catalogue .... +1 milhão de produtos       (literally the catalogue)
     supplier strip ....... +R$ 1 bilhão vendidos       (closes the catalogue,
                                                         with the brands beside
                                                         the number)
     how it works ......... 24 horas até o ar           (it is step 01)
     TRAVIA band .......... 68% · 3min · 24/7
     investors ............ +R$ 30 milhões captados
     pricing .............. R$ 0 e R$ 399               (the offer itself)

   This section carries the track record: one year of real operation, the one
   thing the rest of the page never states. It answers the question the agency
   asks before "is it reliable?", which is "does anyone actually sell through
   this?".

   The reliability proof it used to carry survives as prose in the lead instead
   of three tiles, because two of the three figures have owners elsewhere:
   -80% de fraude on Seguranca.html and in Cases, 24h de suporte in the FAQ and
   on Preços. The exception is 99,9% de disponibilidade, which appears nowhere
   else on the site, so it stays here, in the sentence above the numbers, with
   the approved wording: availability, never "SLA", since the Termos formalise
   neither a percentage nor a remedy.

   The two counted figures are exact on purpose: 44.576 e 36.617 are read off
   the operation, and the precision is what makes them credible. The ticket
   figure is not counted, it is 35.874 pedidos times an average of three
   ingressos, so it ships rounded as "+107 mil" rather than as a
   false-precision 107.622.
   ------------------------------------------------------------ */
/* Three one-off icons for this card row only, exported from the Figma
   prototype as raw SVGs (ticket, cart, smile). Deliberately not folded into
   the generated Material Symbols set in landing-icons.jsx: that set is a
   filled, square-cornered cut, and mixing it with the thin Lucide-style
   stroke these came in would read as two icon languages in one row of three
   cards. Kept local to Metrics instead, stroke swapped to currentColor so
   each card colors its own icon (dark on the two grey cards, the same
   near-black as the rest of the lead card's ink on the blue one), and
   original paths/viewBox left untouched: the wrapper fixes the output size
   at 44x44 rather than the coordinates being hand-edited to match. */
const METRIC_ICONS = {
  ticket: (
    <svg viewBox="0 0 22 16" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M12 1V3M12 13V15M12 7V9M1 5C1.79565 5 2.55871 5.31607 3.12132 5.87868C3.68393 6.44129 4 7.20435 4 8C4 8.79565 3.68393 9.55871 3.12132 10.1213C2.55871 10.6839 1.79565 11 1 11V13C1 13.5304 1.21071 14.0391 1.58579 14.4142C1.96086 14.7893 2.46957 15 3 15H19C19.5304 15 20.0391 14.7893 20.4142 14.4142C20.7893 14.0391 21 13.5304 21 13V11C20.2044 11 19.4413 10.6839 18.8787 10.1213C18.3161 9.55871 18 8.79565 18 8C18 7.20435 18.3161 6.44129 18.8787 5.87868C19.4413 5.31607 20.2044 5 21 5V3C21 2.46957 20.7893 1.96086 20.4142 1.58579C20.0391 1.21071 19.5304 1 19 1H3C2.46957 1 1.96086 1.21071 1.58579 1.58579C1.21071 1.96086 1 2.46957 1 3V5Z" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  ),
  cart: (
    <svg className="metric-card__icon--compact" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M1 1.02832L2.099 1.00032C2.33643 0.994332 2.56824 1.07305 2.75293 1.22238C2.93762 1.37171 3.06314 1.5819 3.107 1.81532L5.797 16.1623C5.83993 16.3916 5.96169 16.5987 6.14121 16.7478C6.32072 16.8968 6.54669 16.9783 6.78 16.9783H16.95M16.95 16.9783C15.8454 16.9783 14.95 17.8737 14.95 18.9783C14.95 20.0829 15.8454 20.9783 16.95 20.9783C18.0546 20.9783 18.95 20.0829 18.95 18.9783C18.95 17.8737 18.0546 16.9783 16.95 16.9783ZM3.513 3.97832H19.948C20.0968 3.97802 20.2438 4.01093 20.3783 4.07465C20.5127 4.13837 20.6313 4.2313 20.7253 4.34666C20.8193 4.46202 20.8863 4.5969 20.9216 4.74146C20.9568 4.88603 20.9594 5.03664 20.929 5.18232L19.903 11.4083C19.8037 11.8598 19.5511 12.2629 19.1881 12.5491C18.8251 12.8354 18.3742 12.987 17.912 12.9783H5.2M8.95 18.9783C8.95 20.0829 8.05457 20.9783 6.95 20.9783C5.84543 20.9783 4.95 20.0829 4.95 18.9783C4.95 17.8737 5.84543 16.9783 6.95 16.9783C8.05457 16.9783 8.95 17.8737 8.95 18.9783Z" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  ),
  smile: (
    <svg className="metric-card__icon--compact" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M14 9V8M15.472 14C14.9092 14.629 14.2201 15.1322 13.4496 15.4767C12.679 15.8212 11.8445 15.9993 11.0005 15.9993C10.1565 15.9993 9.32196 15.8212 8.55145 15.4767C7.78094 15.1322 7.09178 14.629 6.529 14M8 9V8M21 11C21 16.5228 16.5228 21 11 21C5.47715 21 1 16.5228 1 11C1 5.47715 5.47715 1 11 1C16.5228 1 21 5.47715 21 11Z" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  ),
};
function MetricIcon({ name }) {
  return <span className="metric-card__icon" aria-hidden="true">{METRIC_ICONS[name]}</span>;
}

/* Three equal cards, one told apart by fill (see .metric-card--lead in
   landing-styles.css): the same weighted-trio move the cases band uses. */
const METRIC_CARDS = [
  { id: "ingressos", icon: "ticket", lead: true,
    value: <><span className="metric-card__prefix">+</span><Counter to={107} /><span className="metric-card__suffix">mil</span></>,
    label: "Ingressos emitidos no período, uma média de três por pedido." },
  { id: "pedidos", icon: "cart",
    value: <Counter to={44576} />,
    label: "Pedidos processados, do carrinho ao voucher" },
  { id: "clientes", icon: "smile",
    value: <Counter to={36617} />,
    label: "Clientes atendidos com voucher e suporte no mesmo lugar" },
];

function Metrics() {
  return (
    <section className="section section--alt section--alt-divider" id="numeros">
      <div className="container">
        <Reveal as="div" className="sect-head">
          <span className="eyebrow">Confiança</span>
          <h2 className="h-section">Todo ano, esse volume passa pela Just</h2>
          <p className="lead">São números de um ano de operação das agências na plataforma. Você não entra num piloto: entra numa estrutura que já roda em escala, com infraestrutura redundante, 99,9% de disponibilidade e backup diário.</p>
        </Reveal>

        <div className="metrics">
          {METRIC_CARDS.map((m, i) => (
            <Reveal
              key={m.id}
              className={`metric-card${m.lead ? " metric-card--lead" : ""}`}
              delay={i * 120}>
              <MetricIcon name={m.icon} />
              {/* .metric-card__value is now a fixed-height slot (see
                  landing-styles.css), sized to the lead card's own bigger
                  number so the slot is identical across all three cards and
                  the label below it still starts at the same height. The
                  actual number, at each card's own font-size, is centred
                  inside that slot by -inner rather than by the slot's own
                  now-mismatched line-height. */}
              <div className="metric-card__value">
                <span className="metric-card__value-inner">{m.value}</span>
              </div>
              <div className="metric-card__label">{m.label}</div>
            </Reveal>
          ))}
        </div>

        <Reveal as="div" className="metrics__foot" delay={320}>
          <a className="btn btn--outline btn--lg" href="Seguranca.html">
            Ver como protegemos os dados
            <span className="btn__arrow"><SIcon name="btnArrow" /></span>
          </a>
        </Reveal>
      </div>
    </section>);

}

/* ============================================================
   4b) Product catalogue

   Owns the #produtos anchor. The navbar mega menu and the footer point
   roughly thirty links here ("Hotéis", "Cruzeiros", "Seguro viagem", …),
   so the target has to be an actual catalogue rather than the feature
   carousel that used to live at this id.

   The section closes with the supplier and destination strip (3, above),
   rendered here rather than from the page: categories, then the brands and
   destinations behind them, on the one white block.
   ============================================================ */
/* One rail item per category; picking one (or letting the rotation land on it)
   swaps the card beside it.

   `points` is the per-product detail. Every line is scope — what the category
   contains — or a restatement of copy already published elsewhere on the site
   (real-time inventory, margins set by the agency, the supplier brands in the
   marquee above). Nothing here asserts a new commercial fact.

   `photo` is the image hook: with a file in assets/photos/ the card shows it
   under the same blue scrim, glyph and label; null paints the branded poster
   instead, so the treatment is identical either way and photography can land
   category by category.

   `slug` is the contract with Produtos.html: the navbar builds
   `Produtos.html#<slug>` from this list and produtos-app.jsx keys its long-form
   copy off the same value, so this array is the single taxonomy for the whole
   site. Renaming a slug moves an anchor that the menu is already pointing at,
   so change both ends together.

   Order is deliberate and is the order Produtos.html renders: ingressos leads
   because it is what the agency sells first, hospedagens and transportes right
   behind it as the other two a trip cannot go without. Hospedagens is one
   category, not two: it covers hotels and vacation rentals, which is why there
   is no separate "Casas para alugar" entry.

   "Guias e dicas" (slug guias) used to be the eleventh entry here. Removed on
   request: this was the only place that category existed, so the navbar link
   (NAV_TREE's col() already drops any slug missing from this array) and the
   Produtos.html section it built (produtos-app.jsx maps this same array) are
   both gone with it, not just the card here. produtos-app.jsx still carries a
   PD_INTRO/PD_PHOTOS entry for "guias"; harmless dead data, left alone since
   this file is the only one this change touches. */
const CATEGORIES = [
  { icon: "ticket",   slug: "ingressos",  name: "Ingressos",         body: "Atrações, shows e parques com emissão imediata.", photo: "produtos/ingressos.avif",
    points: ["Atrações, shows e passeios guiados", "Parques da Disney, Universal e SeaWorld", "Voucher emitido na hora da compra"] },
  { icon: "bed",      slug: "hospedagens", name: "Hospedagens",      body: "Hotéis, resorts, pousadas e casas por temporada.", photo: "produtos/hospedagens.avif",
    points: ["Redes Hilton, Accor, Meliá e Wyndham", "Casas e apartamentos para famílias e grupos", "Confirmação e voucher no mesmo fluxo"] },
  { icon: "car",      slug: "transportes", name: "Transportes",      body: "Aluguel de carros, transfers e traslados.", photo: "produtos/transportes.avif",
    points: ["Locação de carros por diária", "Transfer de aeroporto e hotel", "Traslados entre cidades e atrações"] },
  { icon: "ship",     slug: "cruzeiros",  name: "Cruzeiros",         body: "Marítimos e fluviais das principais armadoras.", photo: "produtos/cruzeiros.avif",
    points: ["Roteiros marítimos e fluviais", "Armadoras como a MSC Cruzeiros", "Cabines por categoria e ocupação"] },
  { icon: "calendar", slug: "eventos",    name: "Eventos",           body: "Shows, festivais e experiências com data marcada.", photo: "produtos/eventos.avif",
    points: ["Shows, festivais e temporadas", "Ingressos com data e setor definidos", "Pacote com hospedagem e transporte"] },
  { icon: "ferris",   slug: "parques",    name: "Parques temáticos", body: "Orlando, Europa e Ásia, com ingressos oficiais.", photo: "produtos/parques.avif",
    points: ["Disney, Universal, SeaWorld e Legoland", "Passaportes de um dia ou vários dias", "Ingressos oficiais dos parques"] },
  { icon: "shield",   slug: "seguros",    name: "Seguros",           body: "Seguro viagem e assistência para cada destino.", photo: "produtos/seguros.avif",
    points: ["Assistência médica em viagem", "Coberturas por destino e duração", "Parceiros como a Assist Card"] },
  { icon: "sparkles", slug: "experiencias", name: "Experiências",    body: "Passeios, tours guiados e roteiros locais.", photo: "produtos/experiencias.avif",
    points: ["City tours e passeios guiados", "Gastronomia, natureza e cultura", "Roteiros de meio dia ou dia inteiro"] },
  { icon: "trophy",   slug: "esportivos", name: "Eventos esportivos", body: "Jogos, campeonatos e pacotes de torcida.", photo: "produtos/esportivos.avif",
    points: ["Jogos, clássicos e campeonatos", "Ingressos por setor do estádio", "Pacotes com hospedagem e traslado"] },
  { icon: "tag",      slug: "exclusivos", name: "Produtos exclusivos", body: "Tarifas negociadas que você não encontra nas OTAs.", photo: "produtos/exclusivos.avif",
    points: ["Tarifas negociadas pela Just Travel", "Condições que as OTAs não oferecem", "Margem definida por você"] },
];

const PRODUCT_ROTATE_MS = 6000;

function ProductCatalog() {
  const [i, setI] = useState(0);
  // Two independent reasons to hold the rotation, because they answer to
  // different people. `hovering` is the courtesy pause for whoever is reading
  // the card right now; `tookOver` is the mechanism WCAG 2.2.2 asks for, since
  // this region updates itself every 6s and hover never reaches a touch or
  // keyboard visitor.
  //
  // That mechanism used to be a "Pausar rotação" button. It is now the tablist
  // itself: picking a category, by tap, click or arrow key, stops the rotation
  // for good. Every input modality has a way to take control, and the visitor
  // who chose a card never has it pulled out from under them. It has to survive
  // the pointer leaving, so it stays apart from `hovering` instead of sharing
  // one flag.
  const [tookOver, setTookOver] = useState(false);
  const [hovering, setHovering] = useState(false);
  const paused = tookOver || hovering;
  const tabRefs = useRef([]);
  const active = CATEGORIES[i];

  // Manual pick: show that card and hand the section over to the visitor.
  const pick = (n) => { setI(n); setTookOver(true); };

  // One timer per shown card rather than one interval for the section: because
  // the effect re-runs on every index change, a manual pick restarts the full
  // dwell instead of inheriting whatever was left of the previous tick.
  useEffect(() => {
    if (paused || prefersReducedMotion()) return;
    const id = setTimeout(() => setI((n) => (n + 1) % CATEGORIES.length), PRODUCT_ROTATE_MS);
    return () => clearTimeout(id);
  }, [i, paused]);

  // Roving tabindex: the tablist is one tab stop and the arrows move within it.
  // The rail is vertical, so Up/Down are the arrows the pattern asks for; Left
  // and Right stay wired because below 860px the same rail lays itself out as a
  // horizontal row and pointing them at nothing would be a dead key there.
  const onKeyDown = (e) => {
    const dir = e.key === "ArrowDown" || e.key === "ArrowRight" ? 1 :
                e.key === "ArrowUp" || e.key === "ArrowLeft" ? -1 : 0;
    if (!dir) return;
    e.preventDefault();
    const next = (i + dir + CATEGORIES.length) % CATEGORIES.length;
    pick(next);
    tabRefs.current[next]?.focus();
  };

  return (
    <section className="section" id="produtos">
      <div className="container">
        <Reveal as="div" className="sect-head">
          <span className="eyebrow">Catálogo</span>
          <h2 className="h-section">Conheça os nossos produtos</h2>
          <p className="lead">
            Tudo que o seu cliente precisa para ter a viagem perfeita, pronto para você vender no mesmo carrinho.
          </p>
        </Reveal>

        {/* Hover or focus anywhere in here holds the rotation, so nobody loses
            the card they are actually reading. */}
        <Reveal as="div" className={`prod ${paused ? "is-paused" : ""}`} delay={80}
          onMouseEnter={() => setHovering(true)}
          onMouseLeave={() => setHovering(false)}
          onFocusCapture={() => setHovering(true)}
          onBlurCapture={() => setHovering(false)}>

          {/* One rail holding all eleven categories, beside the card rather
              than above it: stacked, the list reads as an index the eye can run
              down, and the card keeps its place instead of being pushed around
              by however many rows the pills happened to wrap into. Still one
              filled track with borderless items inside it, the same control the
              showcase below uses for Automatizar / Controlar / Atender, so the
              interaction only has to be learned once. */}
          <div className="prod__tabs" role="tablist" aria-orientation="vertical"
            aria-label="Categorias de produtos" onKeyDown={onKeyDown}>
            {CATEGORIES.map((c, n) =>
              <button
                key={c.name}
                ref={(el) => { tabRefs.current[n] = el; }}
                id={`prod-tab-${n}`}
                type="button"
                role="tab"
                aria-selected={n === i}
                aria-controls="prod-panel"
                tabIndex={n === i ? 0 : -1}
                className={`prod-tab ${n === i ? "is-active" : ""}`}
                onClick={() => pick(n)}>
                <SIcon name={c.icon} size={16} />
                {c.name}
              </button>
            )}
          </div>

          <div className="prod__card" id="prod-panel" role="tabpanel"
            aria-labelledby={`prod-tab-${i}`} tabIndex={-1} key={i}>
            <div className="prod__visual" style={{ "--prod-i": i }}>
              {active.photo &&
                <img className="prod__photo" src={`/assets/photos/${active.photo}`} alt="" loading="lazy" />}
              <span className="prod__glyph" aria-hidden="true">
                <SIcon name={active.icon} size={148} />
              </span>
            </div>
            <div className="prod__body">
              <h3 className="prod__name">{active.name}</h3>
              <p className="prod__desc">{active.body}</p>
              <ul className="prod__points">
                {active.points.map((p) =>
                  <li key={p}><SIcon name="check" size={16} />{p}</li>
                )}
              </ul>
            </div>
          </div>
        </Reveal>

        <Reveal as="div" className="cat-foot" delay={120}>
          {/* The catalogue page is the next step from here, now that it exists:
              this section is the teaser, Produtos.html is the whole shelf with
              one anchor per category. Pricing is one click away in the navbar
              and two sections below.

              Filled rather than outlined: with the supplier strip closing the
              section below it, this is the last thing asked for before the page
              moves on to how it works, and the outline pill went unnoticed
              against the card above it. */}
          <a className="btn btn--blue btn--lg" href="Produtos.html">
            Vender esses produtos
            <span className="btn__arrow"><SIcon name="btnArrow" /></span>
          </a>
          {/* Moved here from the section head: the figure that says how big the
              catalogue is now sits beside the CTA it supports instead of over
              it. */}
          <p className="lead">
            <strong>+1 milhão de produtos</strong> conectados por API, com preço e disponibilidade em tempo real.
          </p>
        </Reveal>
      </div>

      {/* The supplier and destination strip closes the catalogue instead of
          opening the page. It is the same argument one step further on: these
          are the categories, and these are the brands and destinations behind
          them. It stays outside .container because the marquee runs edge to
          edge, and it carries no fill of its own any more, so the two read as
          one white block rather than as two sections that happen to touch. */}
      <SupplierLogos />
    </section>);

}

/* ============================================================
   5) How it works — the four steps and the WhatsApp CTA

   One thing on the band now: the four steps, side by side rather than
   stacked (one column per step, see .steps), with a single WhatsApp button
   centred below them. The section still carries the whole conversion on its
   own, so it keeps dropping the eyebrow and the standfirst the head used to
   have.

   The lead form that used to sit beside the rail (name/e-mail/phone/agency,
   collected to write into a WhatsApp message) was removed on request,
   replaced by HOW_WHATSAPP_HREF below: same destination, fixed message, no
   fields. .how-grid, the two-column layout that split the rail from the
   form, went with it — the rail is the section's only column now.

   The orbit that used to sit above the title here (the mark with the
   catalogue's icons turning around it) was removed on request too. Nothing
   else in the section depended on it: the icons are the site's shared set,
   used elsewhere regardless, and the mark asset (assets/logo-mark.png) is
   used in half a dozen other places too. .sect-head.how-head still carries
   the gap/max-width it was tuned to (see that rule), left as-is since a
   single centred title inside a wider-than-needed box is not a visible
   defect.

   The four steps keep the walk they always had: each one takes the blue
   accent for STEP_DWELL_MS once the rail is in view, then hands it to the
   next; after the fourth the highlight clears and the pass starts over,
   looping for as long as the rail stays in view (see the stepsInView
   observer and runPass in HowItWorks). Reduced motion still gets the
   resting rail with no walk at all, since the loop only re-states the order
   the numerals already give. None of this cares about the rail's own
   orientation: the sequence only ever toggles .is-lit, so it ran the same
   way before the rail went from a vertical stack to a horizontal row —
   only .steps::before (the connecting line) needed to turn from a vertical
   rule to a horizontal one, in landing-styles.css. */
/* Four one-off icons for the steps rail only, sourced from Downloads
   (01Vector.svg / 02Vector.svg / 03Vector.svg / 04Vector.svg — matched by
   the number prefix to each step's own "01"-"04") rather than the shared
   Material Symbols set in landing-icons.jsx. Same reasoning as
   IA_PILLAR_ICONS/METRIC_ICONS above: kept local instead of overwriting the
   bag/chat/cart/users keys, which other sections still use (bag and cart
   are also METRIC_CARDS icons, for one). Stroke swapped to currentColor so
   .step__mark's own colour (--just-blue at rest, white when lit) colours
   the icon; sizing is untouched — .step__mark svg already fixes any child
   svg at 48px, so these carry no width/height of their own, same as the
   old SIcon call did with size={null}. stroke-width="1.6" (down from the
   source files' own 2) is fixed on request, in both the .is-lit and resting
   states — a thin/thick pass keyed on .is-lit was tried and reverted. */
const STEP_ICONS = {
  ativar: (
    <svg viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M12.8002 2.90049L10.8002 4.80049M3.90024 6.80049L1.00024 6.00049M4.80024 10.8005L2.90024 12.8005M6.00024 1.00049L6.80024 3.90049M7.83724 8.49049C7.79847 8.39914 7.78787 8.29829 7.8068 8.20088C7.82574 8.10346 7.87334 8.01393 7.94351 7.94376C8.01368 7.87359 8.10322 7.82598 8.20063 7.80705C8.29805 7.78811 8.3989 7.79871 8.49024 7.83749L19.4902 12.3375C19.5881 12.3777 19.6708 12.4478 19.7263 12.538C19.7817 12.6281 19.8072 12.7334 19.799 12.8389C19.7907 12.9444 19.7493 13.0446 19.6805 13.125C19.6117 13.2055 19.5192 13.262 19.4162 13.2865L15.0672 14.3275C14.8877 14.3704 14.7235 14.4621 14.5929 14.5925C14.4623 14.723 14.3704 14.887 14.3272 15.0665L13.2872 19.4165C13.263 19.5198 13.2065 19.6127 13.126 19.6818C13.0455 19.7508 12.9451 19.7925 12.8393 19.8008C12.7335 19.809 12.6279 19.7834 12.5376 19.7276C12.4474 19.6718 12.3772 19.5888 12.3372 19.4905L7.83724 8.49049Z" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  ),
  divulgar: (
    <svg viewBox="0 0 22 20" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M17 5V3C17 2.46957 16.7893 1.96086 16.4142 1.58579C16.0391 1.21071 15.5304 1 15 1H3C2.46957 1 1.96086 1.21071 1.58579 1.58579C1.21071 1.96086 1 2.46957 1 3V10C1 10.5304 1.21071 11.0391 1.58579 11.4142C1.96086 11.7893 2.46957 12 3 12H11M9 16V12.04M6 16H11M17 9H19C20.1046 9 21 9.89543 21 11V17C21 18.1046 20.1046 19 19 19H17C15.8954 19 15 18.1046 15 17V11C15 9.89543 15.8954 9 17 9Z" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  ),
  vender: (
    <svg viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M1 1.02832L2.099 1.00032C2.33643 0.994332 2.56824 1.07305 2.75293 1.22238C2.93762 1.37171 3.06314 1.5819 3.107 1.81532L5.797 16.1623C5.83993 16.3916 5.96169 16.5987 6.14121 16.7478C6.32072 16.8968 6.54669 16.9783 6.78 16.9783H16.95M16.95 16.9783C15.8454 16.9783 14.95 17.8737 14.95 18.9783C14.95 20.0829 15.8454 20.9783 16.95 20.9783C18.0546 20.9783 18.95 20.0829 18.95 18.9783C18.95 17.8737 18.0546 16.9783 16.95 16.9783ZM3.513 3.97832H19.948C20.0968 3.97802 20.2438 4.01093 20.3783 4.07465C20.5127 4.13837 20.6313 4.2313 20.7253 4.34666C20.8193 4.46202 20.8863 4.5969 20.9216 4.74146C20.9568 4.88603 20.9594 5.03664 20.929 5.18232L19.903 11.4083C19.8037 11.8598 19.5511 12.2629 19.1881 12.5491C18.8251 12.8354 18.3742 12.987 17.912 12.9783H5.2M8.95 18.9783C8.95 20.0829 8.05457 20.9783 6.95 20.9783C5.84543 20.9783 4.95 20.0829 4.95 18.9783C4.95 17.8737 5.84543 16.9783 6.95 16.9783C8.05457 16.9783 8.95 17.8737 8.95 18.9783Z" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  ),
  fas: (
    <svg viewBox="0 0 22 20" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M1 6.51615C1.00002 5.40335 1.33759 4.31674 1.96813 3.39982C2.59867 2.4829 3.49252 1.77881 4.53161 1.38055C5.5707 0.982294 6.70616 0.908598 7.78801 1.1692C8.86987 1.4298 9.84722 2.01243 10.591 2.84015C10.6434 2.89617 10.7067 2.94082 10.7771 2.97135C10.8474 3.00188 10.9233 3.01764 11 3.01764C11.0767 3.01764 11.1526 3.00188 11.2229 2.97135C11.2933 2.94082 11.3566 2.89617 11.409 2.84015C12.1504 2.00705 13.128 1.41952 14.2116 1.15575C15.2952 0.891989 16.4335 0.9645 17.4749 1.36364C18.5163 1.76277 19.4114 2.46961 20.0411 3.39006C20.6708 4.3105 21.0053 5.40091 21 6.51615C21 8.80615 19.5 10.5162 18 12.0162L12.508 17.3292C12.3217 17.5432 12.0919 17.7151 11.834 17.8335C11.5762 17.9518 11.296 18.014 11.0123 18.0158C10.7285 18.0176 10.4476 17.959 10.1883 17.8439C9.92893 17.7288 9.69703 17.5598 9.508 17.3482L4 12.0162C2.5 10.5162 1 8.81615 1 6.51615Z" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  ),
};
function StepIcon({ name }) {
  return STEP_ICONS[name] || null;
}

/* The catalogue is counted once, in the catalogue section. Step 03 is about
   what happens to the sale, not about inventory. */
const HOW_STEPS = [
  { n: "01", icon: "ativar", title: "Ative sua loja",
    body: "Escolha o plano, configure logo, cores e domínio próprio. Sua plataforma fica no ar em até 24 horas." },
  { n: "02", icon: "divulgar", title: "Divulgue online",
    body: "Compartilhe seu link, posts prontos e campanhas. A Just Academy te ensina como atrair os primeiros clientes." },
  { n: "03", icon: "vender", title: "Triplique suas vendas",
    body: "Monte o pacote inteiro num carrinho só: hospedagem, ingressos, transporte e seguro. A margem de cada item é você quem define." },
  { n: "04", icon: "fas", title: "Transforme em fãs",
    body: "Pós-venda, atendimento e fidelização integrados. Cliente satisfeito volta, indica e compra de novo." },
];

const STEP_DWELL_MS = 3000;
/* Lead-in: the rows are still arriving when the rail crosses the threshold
   (0 to 360ms of stagger on top of a 600ms reveal), so the walk waits for the
   first one to land instead of lighting it mid-fade. */
const STEP_LEAD_IN_MS = 520;

/* ------------------------------------------------------------
   The section's WhatsApp CTA.

   There used to be a lead form here (name/e-mail/phone/agency), on the same
   contract as FooterStartForm in landing-footer.jsx: no backend to post to
   and none planned, so a valid submit opened WhatsApp with the answers
   already written into the message. Removed on request in favour of a
   single button; with no fields left to collect, the message is now a fixed
   line instead of one built from field values.

   The number is repeated here rather than read from landing-footer.jsx: that
   file loads after this one, so its `const` is still in the temporal dead zone
   while this module is being evaluated during the prerender.
   ------------------------------------------------------------ */
const HOW_WHATSAPP = "5571996951307";
const HOW_WHATSAPP_HREF =
  `https://api.whatsapp.com/send/?phone=${HOW_WHATSAPP}&text=` +
  encodeURIComponent("Olá! Quero falar com um especialista da Just Travel.");

function HowItWorks() {
  // Above the Reveal default: the walk should start when the rail is actually
  // being looked at, not when its first pixel clears the fold, because twelve
  // seconds of sequence started early means reading step 04 while 01 is still
  // lit. Not higher than 0.3 though: a threshold a short landscape viewport
  // can never satisfy would mean the walk simply never runs there — the row
  // is shorter now that it runs horizontally, but the margin costs nothing.
  //
  // A local observer, not the shared useInView: that hook disconnects after
  // its first hit (it is built for a one-shot reveal, and Reveal below still
  // uses it that way), so it can only ever report "has been seen once", never
  // "is visible right now". The walk needs the second one, on request: it
  // loops for as long as the rail is in view and stops (rather than keep
  // firing off-screen) once it is not.
  const stepsRef = useRef(null);
  const [stepsInView, setStepsInView] = useState(false);
  useEffect(() => {
    if (!stepsRef.current) return;
    const io = new IntersectionObserver(([entry]) => setStepsInView(entry.isIntersecting),
      { threshold: 0.3 });
    io.observe(stepsRef.current);
    return () => io.disconnect();
  }, []);

  const [lit, setLit] = useState(-1);

  // One timer per step plus one to clear, all scheduled off the same start,
  // then the clear timer schedules the next pass itself: `timers` is
  // reassigned (not appended to) each pass, but only ever holds the IDs
  // still pending at any given moment, since every ID from a finished pass
  // has already fired by the time the next one is scheduled. Cleanup below
  // always clears whatever `timers` currently points to, so leaving the
  // section — stepsInView flips false, or the component unmounts — clears
  // the pass in progress instead of leaving it to fire off-screen and start
  // another loop nobody asked for.
  useEffect(() => {
    if (!stepsInView || prefersReducedMotion()) { setLit(-1); return; }
    let timers = [];
    const runPass = () => {
      timers = HOW_STEPS.map((_, n) =>
        setTimeout(() => setLit(n), STEP_LEAD_IN_MS + n * STEP_DWELL_MS));
      timers.push(setTimeout(() => { setLit(-1); runPass(); },
        STEP_LEAD_IN_MS + HOW_STEPS.length * STEP_DWELL_MS));
    };
    runPass();
    return () => timers.forEach(clearTimeout);
  }, [stepsInView]);

  return (
    <section className="section section--alt how-section" id="solucao">
      {/* The brand pattern band, edge to edge and static. It lives outside
          .container because it runs the full width of the page, and it is one
          empty element on purpose: the whole strip is a single composited
          image (scripts/pattern-strip.mjs) repeated on the x axis, so there is
          nothing here to label and nothing to announce. */}
      <div className="pattern-band" aria-hidden="true" />

      <div className="container">
        <Reveal as="div" className="sect-head how-head">
          <h2 className="h-section">Da assinatura à primeira venda, em 4 passos</h2>
        </Reveal>

        {/* An ordered list, not four divs: the numerals are the point of the
            rail, and the order is content rather than styling. Side by side
            now rather than stacked, one column per step (see .steps). */}
        <ol className="steps" ref={stepsRef}>
          {HOW_STEPS.map((s, n) =>
            <Reveal as="li" key={s.n} className={`step ${lit === n ? "is-lit" : ""}`} delay={n * 120}>
              {/* The scale rides on this wrapper, not on .step: .step is the
                  .reveal, and its transform is pinned under reduced motion. */}
              <div className="step__inner">
                <span className="step__mark"><StepIcon name={s.icon} /></span>
                <div className="step__text">
                  <h3 className="step__title">{`${s.n}. ${s.title}`}</h3>
                  <p className="step__body">{s.body}</p>
                </div>
              </div>
            </Reveal>
          )}
        </ol>

        {/* Replaces the lead form that used to sit beside the rail: one
            button, the same WhatsApp destination, no fields to fill in. */}
        <Reveal as="div" className="how-cta" delay={HOW_STEPS.length * 120 + 80}>
          <a className="btn btn--blue btn--lg" href={HOW_WHATSAPP_HREF}
            target="_blank" rel="noopener">
            Fale com um especialista
            <span className="btn__arrow"><SIcon name="btnArrow" /></span>
          </a>
        </Reveal>
      </div>
    </section>);

}

/* ============================================================
   6) Funcionalidades e benefícios — three feature tiles on the mosaic band

   The page's darkest break, pinned near-black in BOTH themes the way
   Depoimentos and the footer are. Everything inside it is a literal rather
   than a token: the ink ramp and the brand blue both flip in dark mode and
   would drag this type toward the fill it sits on.

   Three tiles of equal width and equal weight this time, none in the brand
   blue: a badge (the band's own near-black, --cases-fill, so it reads as a
   cutout rather than a fourth surface), an icon, two lines of bold white
   copy. No stat, no brand name, no per-tile link: the row is a benefit
   statement, not a set of case studies, so nothing here points at
   Artigo.html or blog-data.js any more.

   The mosaic is the band's decoration: two clusters of the brand's own tiles
   parked in the margins either side of the column, the same kit the hero's
   decorative layer draws from. It is scenery, so it is aria-hidden and inert,
   and it is dropped below 1280px, where the margins it lives in are gone.
   Untouched by this rewrite.
   ============================================================ */

/* Three one-off icons for this row only, exported from the Figma prototype as
   raw SVGs (monitor+phone, a percent/discount tag, a shield with a check).
   Same call as METRIC_ICONS above and for the same reason: not folded into
   the generated Material Symbols set in landing-icons.jsx, so the row stays
   visually consistent with itself rather than matching that set's filled,
   square-cornered cut. Stroke swapped to currentColor so the badge's own
   color (--cases-accent) carries through; original paths/viewBox untouched,
   the badge fixes the output size rather than the coordinates being hand-
   edited to match. */
const CASE_ICONS = {
  ecommerce: (
    <svg viewBox="0 0 52 48" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M40.15 11.5278V6.63889C40.15 5.34227 39.6443 4.09877 38.7441 3.18192C37.8439 2.26508 36.623 1.75 35.35 1.75H6.55C5.27696 1.75 4.05606 2.26508 3.15589 3.18192C2.25571 4.09877 1.75 5.34227 1.75 6.63889V23.75C1.75 25.0466 2.25571 26.2901 3.15589 27.207C4.05606 28.1238 5.27696 28.6389 6.55 28.6389H25.75M20.95 38.4167V28.7367M13.75 38.4167H25.75M40.15 21.3056H44.95C47.601 21.3056 49.75 23.4944 49.75 26.1944V40.8611C49.75 43.5612 47.601 45.75 44.95 45.75H40.15C37.499 45.75 35.35 43.5612 35.35 40.8611V26.1944C35.35 23.4944 37.499 21.3056 40.15 21.3056Z" stroke="currentColor" strokeWidth="3.5" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  ),
  tarifas: (
    <svg viewBox="0 0 52 52" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M32.9666 18.54L18.5711 32.9412M18.5711 18.54H18.5951M32.9666 32.9412H32.9906M6.21487 17.6279C5.86468 16.0498 5.91845 14.4088 6.3712 12.8571C6.82395 11.3053 7.66102 9.89309 8.8048 8.75125C9.94858 7.60942 11.362 6.77498 12.9141 6.3253C14.4662 5.87562 16.1066 5.82526 17.6833 6.1789C18.5512 4.8211 19.7467 3.70371 21.1598 2.9297C22.5728 2.1557 24.1579 1.75 25.7689 1.75C27.3798 1.75 28.9649 2.1557 30.378 2.9297C31.791 3.70371 32.9865 4.8211 33.8544 6.1789C35.4335 5.82372 37.0767 5.87385 38.6313 6.32462C40.1858 6.77539 41.6012 7.61216 42.7457 8.75709C43.8901 9.90202 44.7266 11.3179 45.1772 12.8731C45.6278 14.4282 45.6779 16.0722 45.3228 17.6519C46.6801 18.5201 47.7971 19.7161 48.5708 21.1297C49.3445 22.5433 49.75 24.129 49.75 25.7406C49.75 27.3522 49.3445 28.9379 48.5708 30.3515C47.7971 31.7651 46.6801 32.9611 45.3228 33.8293C45.6763 35.4066 45.626 37.0477 45.1765 38.6004C44.727 40.1531 43.8929 41.5671 42.7515 42.7113C41.6101 43.8556 40.1984 44.693 38.6473 45.1459C37.0962 45.5988 35.4558 45.6526 33.8784 45.3023C33.0117 46.6653 31.8152 47.7875 30.3997 48.5649C28.9843 49.3424 27.3956 49.75 25.7809 49.75C24.1661 49.75 22.5774 49.3424 21.162 48.5649C19.7465 47.7875 18.55 46.6653 17.6833 45.3023C16.1066 45.6559 14.4662 45.6056 12.9141 45.1559C11.362 44.7062 9.94858 43.8718 8.8048 42.7299C7.66102 41.5881 6.82395 40.1758 6.3712 38.6241C5.91845 37.0724 5.86468 35.4314 6.21487 33.8533C4.84719 32.9874 3.72063 31.7895 2.93999 30.3711C2.15936 28.9527 1.75 27.3598 1.75 25.7406C1.75 24.1214 2.15936 22.5285 2.93999 21.1101C3.72063 19.6917 4.84719 18.4938 6.21487 17.6279Z" stroke="currentColor" strokeWidth="3.5" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  ),
  garantia: (
    <svg viewBox="0 0 44 52" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M14.25 25.7471L19.25 30.5463L29.25 20.9479M41.75 28.1467C41.75 40.1446 33 46.1436 22.6 49.6231C22.0554 49.8002 21.4638 49.7917 20.925 49.5991C10.5 46.1436 1.75 40.1446 1.75 28.1467V11.3495C1.75 10.7131 2.01339 10.1027 2.48223 9.6527C2.95107 9.20269 3.58696 8.94987 4.25 8.94987C9.25 8.94987 15.5 6.07035 19.85 2.42297C20.3796 1.98864 21.0534 1.75 21.75 1.75C22.4466 1.75 23.1204 1.98864 23.65 2.42297C28.025 6.09435 34.25 8.94987 39.25 8.94987C39.913 8.94987 40.5489 9.20269 41.0178 9.6527C41.4866 10.1027 41.75 10.7131 41.75 11.3495V28.1467Z" stroke="currentColor" strokeWidth="3.5" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  ),
};

/* Two lines each, exactly as the Figma prototype breaks them: not left to
   text-wrap, the way .case-tile__text's centring would otherwise land the
   break wherever the column happens to be narrow. */
const CASE_TILES = [
  { id: "ecommerce", icon: "ecommerce", lines: ["Soluções de", "E-Commerce completa"] },
  { id: "tarifas",   icon: "tarifas",   lines: ["Melhores tarifas", "para competir"] },
  { id: "garantia",  icon: "garantia",  lines: ["100% garantia", "anti-fraude"] },
];

/* The mosaic, [tile, column, row, rotation] per square.

   The tiles are the brand kit the hero's decorative layer already draws from,
   the eleven 120x120 squares in assets/mosaico/ (see scripts/mosaic-tiles.mjs,
   which names them and writes the WebP). Nothing here is drawn: every shape on
   the band is a file, and the two rules the hero states hold here as well.
   Tiles abut, because a tile touching its cluster at one corner reads as a
   stray mark instead of as part of it, and rotation is in multiples of 90 and
   only ever on the geometric tiles: a passport on its side is a mistake, not a
   variation.

   Column 1 is the outer edge on both sides (the right panel is laid out rtl),
   so the two lists read the same way, outside in. The left cluster hangs off
   the bottom of the band and the right one off the top, which is what keeps
   both of them out of the headline and leaves the diagonal the composition
   runs on. Tiles that reach in past the margin end up behind a card, which is
   opaque, so they are covered rather than colliding with the copy. */
const CASES_TILES_LEFT = [
  ["pin", 1, 1],
  ["brilho", 1, 2],
  ["quarto", 2, 2, 90],    // vertex top-left, arc turning back under the sparkle
  ["passaporte", 1, 3],
  ["anel", 1, 4, 180],
  ["triangulo", 2, 4],
  ["brilho", 1, 5],
  ["trem", 2, 5],
  ["aviao", 3, 5],
  ["aviao-papel", 4, 5],
];

const CASES_TILES_RIGHT = [
  ["pin", 1, 1],
  ["quarto", 2, 1, 270],   // vertex bottom-right, arc facing the corner
  ["aviao", 3, 1],
  ["brilho", 4, 1],
  ["lente", 1, 2, 90],
  ["azul", 2, 2],
  ["passaporte", 1, 3],
  ["trem", 2, 3],
];

function CasesMosaic({ side, tiles }) {
  return (
    <div className={`cases__mosaic cases__mosaic--${side}`} aria-hidden="true">
      {tiles.map(([tile, col, row, rot], i) =>
        <img key={`${tile}-${col}-${row}-${i}`}
             className="cases-mt"
             src={`assets/mosaico/${tile}.webp`}
             alt=""
             width="120"
             height="120"
             draggable={false}
             decoding="async"
             loading="lazy"
             /* Grid cell and rotation are the tile's identity, not its
                styling: the pitch and the rotate() that reads --mt-rot are in
                the stylesheet. */
             style={{
               gridColumn: col,
               gridRow: row,
               ...(rot ? { "--mt-rot": `${rot}deg` } : null),
             }} />
      )}
    </div>);

}
function CaseTile({ data, delay }) {
  return (
    <Reveal className="case-tile" delay={delay}>
      <span className="case-tile__badge" aria-hidden="true">{CASE_ICONS[data.icon]}</span>
      <p className="case-tile__text">
        {data.lines[0]}<br />{data.lines[1]}
      </p>
    </Reveal>);

}

function AgencyCases() {
  return (
    <section className="section cases" id="cases">
      <CasesMosaic side="left" tiles={CASES_TILES_LEFT} />
      <CasesMosaic side="right" tiles={CASES_TILES_RIGHT} />

      <div className="container cases__inner">
        <Reveal as="div" className="sect-head cases__head">
          <span className="eyebrow cases__eyebrow">Funcionalidades e benefícios</span>
          <h2 className="cases__title">Tudo o que sua agência precisa para vender mais</h2>
        </Reveal>

        {/* No card leads this time, so the three simply stagger in together
            left to right rather than one first and the rest as a pair. */}
        <div className="cases__grid">
          {CASE_TILES.map((c, i) =>
            <CaseTile key={c.id} data={c} delay={i * 120} />
          )}
        </div>

        {/* Straight to Preços, not to Cases.html: after three proofs the next
            question is what it costs, and the full list is still one click away
            from the navbar, the footer and the results section below. */}
        <Reveal as="div" className="cases__all" delay={360}>
          <a className="btn btn--blue btn--lg" href="/#precos">
            Começar agora
            <span className="btn__arrow"><SIcon name="btnArrow" /></span>
          </a>
        </Reveal>
      </div>
    </section>);

}

/* ============================================================
   6b) IA, CRM and omnichannel — the paid tier

   This is the section the R$ 399 plan sells, so it names the three things
   that plan unlocks: the agent that sells (TRAVIA), the inbox that
   centralises every channel, and the CRM the conversations land in.
   ============================================================ */
/* Three one-off icons for the IA/CRM pillars only, sourced from Downloads
   (Ele qualifica e vende sozinho.svg / E não trabalha sozinho.svg / Nenhum
   lead se perde no caminho.svg — one per pillar, matched by filename) rather
   than the generated Material Symbols set in landing-icons.jsx. Same
   reasoning as METRIC_ICONS above: that set is a filled, square-cornered
   cut, and mixing it with the thin stroke these came in would read as two
   icon languages in one row of three tiles. Kept local to AiSuite instead of
   overwriting the shared sparkles/inbox/funnel keys, which other sections
   still use (sparkles is a CATEGORIES icon, for one).

   Stroke swapped to currentColor so the tile's own color (var(--just-blue),
   set on .ia-pillar__tile) colours the icon, and stroke-width matched to
   METRIC_ICONS's own 1.75 (was 2 in the source files) for the same reason
   those did: it is the project's one other stroke-icon set. Original
   paths/viewBox left untouched; width/height on the <svg> fix the output at
   32px instead of the coordinates being hand-edited to match. */
const IA_PILLAR_ICONS = {
  qualifica: (
    <svg width="32" height="32" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M19.0024 1.00239V5.00239M21.0024 3.00239H17.0024M10.0194 1.81639C10.0622 1.587 10.184 1.37981 10.3635 1.23071C10.543 1.08162 10.769 1 11.0024 1C11.2358 1 11.4618 1.08162 11.6413 1.23071C11.8208 1.37981 11.9425 1.587 11.9854 1.81639L13.0364 7.37439C13.111 7.76954 13.3031 8.13301 13.5874 8.41737C13.8718 8.70172 14.2352 8.89375 14.6304 8.96839L20.1884 10.0194C20.4178 10.0622 20.625 10.184 20.7741 10.3635C20.9232 10.543 21.0048 10.769 21.0048 11.0024C21.0048 11.2358 20.9232 11.4618 20.7741 11.6413C20.625 11.8208 20.4178 11.9425 20.1884 11.9854L14.6304 13.0364C14.2352 13.111 13.8718 13.3031 13.5874 13.5874C13.3031 13.8718 13.111 14.2352 13.0364 14.6304L11.9854 20.1884C11.9425 20.4178 11.8208 20.625 11.6413 20.7741C11.4618 20.9232 11.2358 21.0048 11.0024 21.0048C10.769 21.0048 10.543 20.9232 10.3635 20.7741C10.184 20.625 10.0622 20.4178 10.0194 20.1884L8.96839 14.6304C8.89375 14.2352 8.70172 13.8718 8.41737 13.5874C8.13301 13.3031 7.76954 13.111 7.37439 13.0364L1.81639 11.9854C1.587 11.9425 1.37981 11.8208 1.23071 11.6413C1.08162 11.4618 1 11.2358 1 11.0024C1 10.769 1.08162 10.543 1.23071 10.3635C1.37981 10.184 1.587 10.0622 1.81639 10.0194L7.37439 8.96839C7.76954 8.89375 8.13301 8.70172 8.41737 8.41737C8.70172 8.13301 8.89375 7.76954 8.96839 7.37439L10.0194 1.81639ZM5.00239 19.0024C5.00239 20.107 4.10696 21.0024 3.00239 21.0024C1.89782 21.0024 1.00239 20.107 1.00239 19.0024C1.00239 17.8978 1.89782 17.0024 3.00239 17.0024C4.10696 17.0024 5.00239 17.8978 5.00239 19.0024Z" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  ),
  omnicanal: (
    <svg width="32" height="32" viewBox="0 0 20 22" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M1 10H4C4.53043 10 5.03914 10.2107 5.41421 10.5858C5.78929 10.9609 6 11.4696 6 12V15C6 15.5304 5.78929 16.0391 5.41421 16.4142C5.03914 16.7893 4.53043 17 4 17H3C2.46957 17 1.96086 16.7893 1.58579 16.4142C1.21071 16.0391 1 15.5304 1 15V10ZM1 10C1 8.8181 1.23279 7.64778 1.68508 6.55585C2.13738 5.46392 2.80031 4.47177 3.63604 3.63604C4.47177 2.80031 5.46392 2.13738 6.55585 1.68508C7.64778 1.23279 8.8181 1 10 1C11.1819 1 12.3522 1.23279 13.4442 1.68508C14.5361 2.13738 15.5282 2.80031 16.364 3.63604C17.1997 4.47177 17.8626 5.46392 18.3149 6.55585C18.7672 7.64778 19 8.8181 19 10M19 10V15M19 10H16C15.4696 10 14.9609 10.2107 14.5858 10.5858C14.2107 10.9609 14 11.4696 14 12V15C14 15.5304 14.2107 16.0391 14.5858 16.4142C14.9609 16.7893 15.4696 17 16 17H17C17.5304 17 18.0391 16.7893 18.4142 16.4142C18.7893 16.0391 19 15.5304 19 15M19 15V17C19 18.0609 18.5786 19.0783 17.8284 19.8284C17.0783 20.5786 16.0609 21 15 21H10" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  ),
  funil: (
    <svg width="32" height="32" viewBox="0 0 22 21" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M8.99964 18C8.99955 18.1858 9.05125 18.368 9.14893 18.5261C9.24661 18.6842 9.38641 18.8119 9.55264 18.895L11.5526 19.895C11.7051 19.9712 11.8746 20.0072 12.0449 19.9994C12.2152 19.9917 12.3807 19.9406 12.5257 19.8509C12.6707 19.7613 12.7903 19.636 12.8733 19.4871C12.9562 19.3381 12.9997 19.1705 12.9996 19V12C12.9999 11.5044 13.1841 11.0265 13.5166 10.659L20.7396 2.67C20.8691 2.52656 20.9542 2.34868 20.9847 2.15788C21.0152 1.96708 20.9898 1.77153 20.9115 1.59487C20.8333 1.41822 20.7055 1.26802 20.5436 1.16245C20.3818 1.05688 20.1929 1.00046 19.9996 1H1.99964C1.80625 1.00007 1.61702 1.05622 1.45489 1.16164C1.29276 1.26706 1.16467 1.41723 1.08614 1.59396C1.00762 1.7707 0.982026 1.96641 1.01246 2.15739C1.0429 2.34837 1.12807 2.52643 1.25764 2.67L8.48264 10.659C8.81518 11.0265 8.99942 11.5044 8.99964 12V18Z" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  ),
};
function IaPillarIcon({ name }) {
  return IA_PILLAR_ICONS[name] || null;
}

const AI_PILLARS = [
  {
    icon: "qualifica",
    title: "Ele qualifica e vende sozinho",
    body: "Conheça a IA que faz triagem e qualificação, monta pacotes, faz cross-sell, gera o carrinho e fecha a venda no seu lugar.",
  },
  {
    icon: "omnicanal",
    title: "E não trabalha sozinho",
    body: "No omnicanal, seus atendentes, seus agentes de IA e todos os canais de comunicação ficam em um só lugar.",
  },
  {
    icon: "funil",
    title: "Nenhum lead se perde no caminho",
    body: "Cada conversa do omnicanal vira um lead no CRM, e você acompanha o funil de vendas inteiro em uma ferramenta prática e completa.",
  },
];

// Plain, not --alt: HowItWorks right above it is tinted, and two tinted bands
// in a row read as one long stripe. The dark TRAVIA panel inside carries this
// section visually either way.
function AiSuite() {
  return (
    <section className="section" id="ia">
      <div className="container">
        <Reveal className="ia-sdr">
          <div className="ia-sdr__top">
            <div className="ia-sdr__copy">
              <span className="eyebrow">TRAVIA · IA, CRM e Omnicanal</span>
              <h2 className="h-section">Já conhece o TRAVIA? Ele responde cotações, qualifica leads e fecha vendas, 24/7</h2>
              <p className="lead">É a IA da Just Travel, o vendedor da sua agência que nunca dorme. Ele atende no WhatsApp, monta roteiros, envia orçamentos e marca o follow-up sozinho. Você só entra quando a venda está pronta.</p>
              {/* The price is deliberately not in this label. It used to read
                  "por R$ 399/mês", which made 399 the first figure a visitor
                  met on the page, two sections before the free offer it is an
                  add-on to. Pricing owns both numbers. */}
              <div className="ia-sdr__ctas">
                <a className="btn btn--blue btn--lg" href="/#precos">
                  Conhecer IA e CRM
                  <span className="btn__arrow"><SIcon name="btnArrow" /></span>
                </a>
                <a className="btn btn--ghost btn--lg ia-sdr__demo" href="Agendar-Demo.html">
                  Ver demonstração ao vivo
                  <SIcon name="arrowRight" size={16} />
                </a>
              </div>
            </div>

            {/* Right column reads top to bottom: the conversation, then the
                three figures that summarise it. Moving them out of the copy
                column evens the two sides out and takes height off the band. */}
            <div className="ia-sdr__panel">
              <ChatMockup />

              <div className="ia-sdr__highlights">
                <div className="ia-sdr__highlight">
                  <div className="ia-sdr__highlight-num">68%</div>
                  <div className="ia-sdr__highlight-label">leads qualificados sem agente</div>
                </div>
                <div className="ia-sdr__highlight">
                  <div className="ia-sdr__highlight-num">3min</div>
                  <div className="ia-sdr__highlight-label">tempo médio de resposta</div>
                </div>
                <div className="ia-sdr__highlight">
                  <div className="ia-sdr__highlight-num">24/7</div>
                  <div className="ia-sdr__highlight-label">sem fim de semana ou feriado</div>
                </div>
              </div>
            </div>
          </div>

          <div className="ia-sdr__pillars">
            {AI_PILLARS.map((p) =>
              <div className="ia-pillar" key={p.title}>
                {/* IaPillarIcon (IA_PILLAR_ICONS above), not SIcon: these
                    three are the Downloads SVGs, not the shared icon set.
                    32px is fixed on the <svg> itself now, same figure as
                    before. */}
                <span className="ia-pillar__tile" aria-hidden="true">
                  <IaPillarIcon name={p.icon} />
                </span>
                <h3 className="ia-pillar__title">{p.title}</h3>
                <p className="ia-pillar__body">{p.body}</p>
              </div>
            )}
          </div>
        </Reveal>
      </div>
    </section>);

}

function ChatMockup() {
  // animated, looping. Pauses based on animation density.
  const allMsgs = [
  { type: "lead", text: "Oi! Tenho 4 pessoas, queria ir pra Cancún em fevereiro 🌴" },
  { type: "ai", text: "Que ótima escolha! Para 4 pessoas em fev/26, posso fechar hospedagem All Inclusive de 7 noites a partir de R$ 4.890/pessoa. Inclui hotel, transfer e seguro viagem." },
  { type: "lead", text: "Dá pra incluir ingressos pros parques?" },
  { type: "typing" },
  { type: "ai", text: "Claro! Adiciono ingressos do Xcaret e Xel-Há ao pacote. Travei os valores por 24h, envio o orçamento para o seu e-mail e marco follow-up amanhã 10h. 👍" }];


  const [visible, setVisible] = useState(2);
  const [resetting, setResetting] = useState(false);

  useEffect(() => {
    if (prefersReducedMotion()) {setVisible(allMsgs.length);return;}
    let i = 2;
    let resetTimer;
    const tick = () => {
      i = i + 1;
      if (i > allMsgs.length) {
        // Fade the stack out before starting over, so the loop breathes
        // instead of snapping the last three messages out of existence.
        setResetting(true);
        resetTimer = setTimeout(() => { i = 2; setVisible(2); setResetting(false); }, 200);
        return;
      }
      setVisible(i);
    };
    const id = setInterval(tick, 2200);
    return () => { clearInterval(id); clearTimeout(resetTimer); };
  }, []);

  return (
    <div className={`chat ${resetting ? "is-resetting" : ""}`}>
      <div className="chat__header">
        <div className="chat__avatar">JT</div>
        <div>
          <div className="chat__name">TRAVIA · IA SDR</div>
          <div className="chat__status">Online · responde em 3 min</div>
        </div>
      </div>
      <div className="chat__msgs">
        {allMsgs.slice(0, visible).map((m, i) =>
        m.type === "typing" ?
        <div key={i} className="chat__msg chat__msg--typing"><span></span><span></span><span></span></div> :

        <div key={i} className={`chat__msg chat__msg--${m.type}`}>{m.text}</div>

        )}
      </div>
    </div>);

}

/* ============================================================
   6c) Depoimentos, removed

   The quote mosaic that stood here carried four invented testimonials, the same
   fiction as the quotes removed from cases-app.jsx. PRODUCT.md and DESIGN.md
   forbid publishing an invented testimonial, so the whole Clients() section, its
   testimonial data, the DepKit mosaic and the PHOTO helper were removed. A real
   deck returns here, with approved quotes, when the team has them: rebuild it as
   a fresh composition rather than restoring this data.
   ============================================================ */

/* ============================================================
   6d) Investors — compact band

   Rendered near the bottom of the page (see landing-app.jsx), right after the
   press band: the two are the same argument from two directions, who backed
   the company and who wrote about it, so they close the page together.

   The one list of investors on the site: Sobre.html lays the same ten marks
   out flat in its own band, reading this array, so the two can never drift.
   `w`/`h` are each trimmed export's own dimensions (written by
   scripts/sobre-assets.mjs), and both bands align the logos by height and let
   the width follow, so one declared size would squash the wide ones. The
   attributes are here only so the browser reserves the right box before the
   image lands.

   `file` works exactly like SUPPLIERS above: null renders the fund as a
   wordmark, so a new investor is presentable before its asset is cleared.

   `light: true` records the two exports that are LIGHT artwork, drawn for a
   dark background (040 Ventures is white type; Anjos do Brasil is gold). It is
   a fact about the files rather than a switch: nothing branches on it now.
   Sobre.html shows every mark as it is, over its blue panel, and this band is
   the brand's blue too, so it knocks all ten out to one flat white silhouette,
   which is the only treatment that reads for both directions of artwork on one
   dark fill. It stays in the data because the next band that needs to tell the
   two kinds apart should not have to work them out again.
   ============================================================ */
const INVESTORS = [
  { name: "Bossa Invest", file: "investidores/bossa-invest.webp", w: 407, h: 120 },
  { name: "Faculdade Baiana de Direito", file: "investidores/faculdade-baiana-de-direito.webp", w: 314, h: 120 },
  { name: "Rede Mais", file: "investidores/rede-mais.webp", w: 364, h: 120 },
  { name: "Sai do Papel", file: "investidores/sai-do-papel.webp", w: 458, h: 120 },
  { name: "040 Ventures", file: "investidores/040-ventures.webp", w: 251, h: 112, light: true },
  { name: "Anjos do Brasil", file: "investidores/anjos-do-brasil.webp", w: 258, h: 120, light: true },
  { name: "Drogarias Velanes", file: "investidores/drogarias-velanes.webp", w: 350, h: 94 },
  { name: "Sanar", file: "investidores/sanar.webp", w: 400, h: 120 },
  { name: "Conexa", file: "investidores/conexa.webp", w: 550, h: 120 },
  { name: "H.Stern", file: "investidores/hstern.webp", w: 522, h: 120 },
];

/* The marquee only loops seamlessly once the track is wider than its frame
   (it translates by -50%), and one pass of the ten marks measures ~1370px at
   26px tall, short of a wide desktop — so the list runs twice per pass. The
   second run is the duplicate the loop needs, not padding for missing content:
   it is hidden from assistive tech below, like the supplier strip's clone. */
const INVESTOR_MARQUEE = [];
while (INVESTOR_MARQUEE.length < 20) INVESTOR_MARQUEE.push(...INVESTORS);

function Investors() {
  // Only the first run of INVESTORS is real content; everything after it exists
  // to give the marquee enough width to loop, so it is hidden from assistive
  // tech. Without this a screen reader read the whole list twice over, and then
  // twice again for the clone.
  const logos = INVESTOR_MARQUEE.map((inv, i) => {
    const padding = i >= INVESTORS.length;
    return inv.file
      ? <img className="investors__logo"
          key={i} src={`assets/logos/${inv.file}`}
          alt={padding ? "" : inv.name} aria-hidden={padding || undefined}
          width={inv.w} height={inv.h} loading="lazy" decoding="async" />
      : <span className="investors__logo investors__logo--word" key={i}
          aria-hidden={padding || undefined}>{inv.name}</span>;
  });

  return (
    <>
      <section className="section section--tight investors-band" id="investidores">
        <div className="container">
          <Reveal as="div" className="sect-head sect-head--center">
            <h2 className="h-section">Nossos investidores</h2>
            <p className="lead">Já captamos <strong>+R$ 30 milhões</strong> para ajudar você a vender mais viagens.</p>
          </Reveal>

          <Reveal as="div" className="investors" delay={80}>
            <div className="logos__track-wrap">
              {/* Same duplicate-and-translate marquee as the supplier strip. */}
              <div className="logos__track investors__track">
                {logos}
                <span aria-hidden="true" className="logos__clone">{logos}</span>
              </div>
            </div>
          </Reveal>
        </div>
      </section>

      {/* The brand's pattern line, closing the blue band the way the same
          strip opens the "how it works" section: it is the seam between this
          band and the FAQ below it, so the two filled surfaces never touch.
          One empty element, one repeating image, nothing to announce. */}
      <div className="pattern-band" aria-hidden="true" />
    </>);

}

/* ============================================================
   7) Pricing (freemium)
   ============================================================ */
function Pricing() {
  return (
    <section className="section" id="precos">
      <div className="container">
        <Reveal as="div" className="sect-head sect-head--center">
          <span className="eyebrow">Preços</span>
          <h2 className="h-section">Crie a sua loja e comece a vender de graça</h2>
          <p className="lead">Sem mensalidade, sem taxa de setup, sem cobrança no seu cartão de crédito.</p>
        </Reveal>

        {/* One row, two offers. The free plan takes two thirds of it and the
            optional paid layer stands beside it, on the one card in the section
            that is dark in both themes. They used to be two blocks under two
            section heads, which read as two sections and left the R$ 399 a
            scroll away from the R$ 0 it is priced against. The commission line
            closes the row for both. */}
        <div className="pricing-stack">
          <div className="pricing-row">
            <Reveal as="div" className="free-plan" delay={80}>
              <div className="free-plan__aside">
                <div className="free-plan__badge">Grátis</div>
                <div className="free-plan__name">Loja online</div>
                <div className="free-plan__price">
                  <span className="free-plan__price-currency">R$</span>
                  <span className="free-plan__price-value">0</span>
                  <span className="free-plan__price-period">/mês</span>
                </div>
                <div className="free-plan__sub">Sem cartão de crédito. Sem fidelidade. Você só paga a comissão sobre as vendas que fizer.</div>
                <a className="btn btn--blue btn--lg free-plan__cta" href="https://office-dashboard.justtraveltour.com/public-whitelabel/new/step-1">
                  Criar minha loja grátis
                  <span className="btn__arrow"><SIcon name="btnArrow" /></span>
                </a>

                <div className="free-plan__trust">
                  <span className="free-plan__trust-item"><SIcon name="shield" size={14} />Antifraude embutido</span>
                  <span className="free-plan__trust-item"><SIcon name="lock" size={14} />Checkout seguro</span>
                  <span className="free-plan__trust-item"><SIcon name="check" size={14} />Conforme a LGPD</span>
                </div>
              </div>

              <div className="free-plan__main">
                <div className="free-plan__main-head">Tudo isto incluso:</div>
                <ul className="free-plan__features">
                  <li><SIcon name="check" size={18} />Loja online completa e com a sua marca</li>
                  {/* One span, not two flex children: the row is a flex box, so
                      a bare <strong> beside the text would take the 10px icon
                      gap as a word space. */}
                  <li><SIcon name="check" size={18} /><span>Acesso a <strong>+1M produtos turísticos</strong></span></li>
                  <li><SIcon name="check" size={18} />Domínio próprio</li>
                  <li><SIcon name="check" size={18} />Checkout PIX, boleto e cartão</li>
                  <li><SIcon name="check" size={18} />Painel de gestão das suas vendas</li>
                  <li><SIcon name="check" size={18} />Editor visual sem código</li>
                  <li><SIcon name="check" size={18} />Onboarding guiado</li>
                  <li><SIcon name="check" size={18} />Just Academy: trilhas de capacitação</li>
                  <li><SIcon name="check" size={18} />Suporte 24h</li>
                </ul>
              </div>
            </Reveal>

            {/* The paid layer, now a card in the same row instead of a block
                with its own section head, so it carries the name and the one
                line of context the head used to give it. */}
            <Reveal as="div" className="upsell" delay={140}>
              <div className="upsell__copy">
                <span className="eyebrow upsell__eyebrow">Opcional</span>
                <h3 className="upsell__name">Loja online, IA e CRM</h3>
                <p className="upsell__lead">TRAVIA vendendo 24h, omnicanal, CRM e automações por cima da sua loja grátis.</p>
                {/* Stacked now that the layer has a column of its own: beside
                    the free plan's checklist the six items read as the answer
                    to it, line for line. */}
                <ul className="upsell__features">
                  <li><SIcon name="check" size={14} />IA que vende sozinha 24h</li>
                  <li><SIcon name="check" size={14} />IA que qualifica os leads</li>
                  <li><SIcon name="check" size={14} />Omnicanal de atendimento</li>
                  <li><SIcon name="check" size={14} />CRM completo</li>
                  <li><SIcon name="check" size={14} />Automações e campanhas</li>
                  <li><SIcon name="check" size={14} />Recuperação de carrinho</li>
                </ul>
              </div>
              <div className="upsell__buy">
                <div className="upsell__price">
                  <span className="upsell__price-currency">R$</span>
                  <span className="upsell__price-value">399</span>
                  <span className="upsell__price-period">/mês</span>
                </div>
                <a className="btn btn--outline btn--sm upsell__cta" href="https://office-dashboard.justtraveltour.com/public-whitelabel/new/step-1">
                  Ativar IA e CRM
                  <span className="btn__arrow"><SIcon name="btnArrow" /></span>
                </a>
                <span className="upsell__note">Cancele quando quiser. A loja continua grátis.</span>
              </div>
            </Reveal>
          </div>

          <div className="pricing__small">
            <strong>A Just Travel só ganha quando você vende.</strong>
          </div>
        </div>
      </div>
    </section>);

}

/* ============================================================
   8) Por trás da Just Travel

   The company behind the offer, placed right after the price: the visitor has
   just read what it costs, and the next question is who is on the other end of
   it. One full-bleed photograph of the operation, the counted facts on a bar
   in the bottom-left corner, and the message on a card beside it, with a
   three-photo carousel of the operation in its middle (see EquipeCarousel).

   The band is a photograph in BOTH themes, so the bar on it is pinned to
   literals the way .cases is: --ink-900 and --just-blue flip in dark mode and
   would drag this type toward the fill it sits on. The card is the opposite
   case, a surface rather than a fill: it takes --bg-1 / --fg-* and follows the
   theme, so dark mode gets a dark card on the same photograph instead of a
   white slab glaring off it.

   The three figures are the ones the site already backs, not the ones the
   frame drew:
     +140 colaboradores      sobre-app.jsx, 2026 row of the trajectory
     +2 mil agências         same row ("+2.000 clientes atendidos"; every
                             client of this platform is an agency, and saying
                             "agências" here keeps it apart from the 36.617
                             "clientes atendidos" in Metrics, which are the
                             travellers those agencies sold to)
     3 países                times próprios no Brasil, nos EUA e em Portugal
   The frame's third figure was "+10 anos no mercado". The CNPJ was registered
   on 19/01/2021 and Sobre opens with "Fundada em 2020", so that one is not a
   rounding, it is off by half the company's life. It does not go back.
   ============================================================ */
const EQUIPE_STATS = [
  { v: "+140", l: "colaboradores" },
  { v: "+2 mil", l: "agências atendidas" },
  { v: "3 países", l: "com time próprio" }
];

// The three states of the photo-card component in the frame (232:17408), in
// its own order. Two of the captions are the frame's; the first one is not,
// because the frame gave that slide the same sentence the card closes on, four
// lines below it, which is this file's placeholder habit rather than a caption.
// It says something about the photograph instead.
//
// All three files are cut to 1000x505 by scripts/equipe-photos.mjs, so the
// crossfade has nothing to resize.
const EQUIPE_SLIDES = [
  {
    slug: "time",
    alt: "Seis integrantes do time da Just Travel lado a lado no escritório",
    cap: "Parte do time, hoje distribuído entre Brasil, Estados Unidos e Portugal."
  },
  {
    slug: "sede",
    alt: "Três pessoas do time diante dos monitores na sede da Just Travel, uma delas em pé ajudando as outras",
    cap: "Nossa sede: tecnologia e operação integradas."
  },
  {
    slug: "operacao",
    alt: "Escritório aberto da Just Travel, com o time distribuído pelas estações de trabalho",
    cap: "Equipes multidisciplinares trabalhando juntas."
  }
];

const EQUIPE_SLIDE_MS = 6000;

/* The card's photo is a carousel of the three, on the same contract the
   culture carousel in careers-app.jsx runs on: it advances on its own, a
   pointer or the keyboard inside it parks it, and picking a dot hands it over
   to the visitor for good. It never moves under a reader who turned motion
   off, and it never moves for a page whose density switch is off, which is
   what prefersReducedMotion() reads.

   Arrows would be the fourth control in a card that is 490px wide and already
   carries an eyebrow, a headline, a paragraph and a closing line; the frame
   draws no chrome at all, so three dots is the whole affordance.

   All three slides stay mounted, stacked in one grid cell. That is what makes
   the crossfade possible, and it also means the card is as tall as its longest
   caption from the first paint rather than resizing when slide 1 hands over to
   slide 2. The two that are not showing are out of the a11y tree. */
function EquipeCarousel() {
  const [i, setI] = useState(0);
  const [tookOver, setTookOver] = useState(false);
  const [hovering, setHovering] = useState(false);
  const count = EQUIPE_SLIDES.length;
  const paused = tookOver || hovering;

  useEffect(() => {
    if (paused || prefersReducedMotion()) return;
    const id = setTimeout(() => setI((n) => (n + 1) % count), EQUIPE_SLIDE_MS);
    return () => clearTimeout(id);
  }, [i, paused]);

  return (
    <div className="eq-carousel"
         role="group"
         aria-roledescription="carrossel"
         aria-label="Fotos do time da Just Travel"
         onMouseEnter={() => setHovering(true)}
         onMouseLeave={() => setHovering(false)}
         onFocusCapture={() => setHovering(true)}
         onBlurCapture={() => setHovering(false)}>
      <div className="eq-carousel__stack">
        {EQUIPE_SLIDES.map((s, idx) =>
          <figure className={`eq-slide ${idx === i ? "is-on" : ""}`}
                  key={s.slug}
                  aria-hidden={idx === i ? undefined : "true"}>
            <picture>
              <source srcSet={`assets/photos/equipe/${s.slug}.avif`} type="image/avif" />
              <img src={`assets/photos/equipe/${s.slug}.webp`} alt={s.alt}
                   width="1000" height="505" loading="lazy" decoding="async" />
            </picture>
            <figcaption className="eq-slide__caption">{s.cap}</figcaption>
          </figure>
        )}
      </div>

      <div className="eq-carousel__dots">
        {EQUIPE_SLIDES.map((s, idx) =>
          <button key={s.slug}
                  type="button"
                  className={`eq-carousel__dot ${idx === i ? "is-on" : ""}`}
                  aria-label={`Ver a foto ${idx + 1} de ${count}`}
                  aria-current={idx === i ? "true" : undefined}
                  onClick={() => { setI(idx); setTookOver(true); }} />
        )}
      </div>
    </div>);

}

function Equipe() {
  return (
    <section className="section equipe" id="equipe">
      {/* Decorative: the band's subject is the card, and every word on the
          section is in the two opaque boxes above this. */}
      <picture className="equipe__bg">
        <source srcSet="assets/photos/equipe/fundo.avif" type="image/avif" />
        <img src="assets/photos/equipe/fundo.webp" alt="" width="1920" height="1280"
             loading="lazy" decoding="async" />
      </picture>

      {/* A sibling of .equipe__inner now, not a child/grid item inside it:
          absolutely positioned over the photo above 1200px (see
          .equipe__stats), which would otherwise pull .equipe__card back into
          the first grid track once this stopped occupying it. */}
      <Reveal as="div" className="equipe__stats" delay={80}>
        {EQUIPE_STATS.map((s) =>
          <div className="equipe__stat" key={s.l}>
            <div className="equipe__stat-value">{s.v}</div>
            <div className="equipe__stat-label">{s.l}</div>
          </div>
        )}
      </Reveal>

      <div className="equipe__inner">
        <Reveal as="div" className="equipe__card" delay={140}>
          <div className="sect-head equipe__head">
            <span className="eyebrow">Conheça quem faz acontecer</span>
            <h2 className="h-section">Por trás da Just Travel</h2>
            <p className="lead">Somos uma startup brasileira focada em tecnologia para o mercado de viagens. Nosso compromisso é entregar soluções que aumentam as suas vendas.</p>
          </div>

          <EquipeCarousel />
        </Reveal>
      </div>
    </section>);

}

/* ============================================================
   9) Na mídia — the press band

   Sits right after "Por trás da Just Travel", and answers the same question
   from outside the company: the band above it is the team saying who they are,
   this one is the trade and business press saying it.

   Pinned near-black in BOTH themes, on the same literals .cases carries and
   for the same reason: --ink-900 and --just-blue flip in dark mode and would
   drag this type toward the fill it sits on. The values are that band's, not
   new ones, so the two dark bands on the page are the same dark.

   EVERY CARD IS A REAL, LIVE ARTICLE, and that is the rule for this section,
   not a detail of this first fill. The Figma frame (212:4) drew seven cards
   with invented headlines under Folha de S.Paulo, Estadão, Valor, Forbes and
   InfoMoney mastheads; PRODUCT.md lists press coverage under the absences that
   future work must not fill by inventing, and a fabricated headline over a
   real masthead is the worst version of that. What went in instead is the
   coverage that exists, headline for headline as each outlet published it,
   every URL checked live on 21/08/2026. The figures inside a headline are the
   outlet's own words, attributed and linked, which is why they are allowed
   here while the same numbers would need internal backing in our own copy.

   To add a clipping: one entry here, newest first. No entry without a URL that
   resolves, and no headline rewritten to read better than it was published.

   `mark` is the outlet's masthead in assets/logos/imprensa/, and `w`/`h` are
   that file's own dimensions (all six are written at 96px tall by
   scripts/press-logos.mjs), here only so the browser reserves the right box
   before the image lands. The card aligns the marks by height and lets the
   width follow, so one declared size would squash PANROTAS. `mark: null`
   renders the outlet as type, which is the frame's own second variant and what
   a new clipping ships as until its masthead is cut.

   Exame's is the frame's asset (node 286:15767). The other five are each
   outlet's own published file: a masthead is a trademark, so it goes in as
   published or not at all. Folha de S.Paulo, Estadão, Valor, Forbes and
   InfoMoney are the five the frame also drew and they are NOT here, for the
   only reason that matters: none of them has written about the company. A
   masthead is not decoration, it is the claim that the outlet ran the story.
   ============================================================ */
const PRESS = [
  {
    outlet: "Exame",
    mark: "exame.webp", w: 558, h: 96,
    year: "2025",
    title: "Com aporte de Bossa Invest, Sai do Papel e 040, Just Travel mira turismo global",
    href: "https://exame.com/negocios/com-aporte-de-bossa-invest-sai-do-papel-e-040-just-travel-mira-turismo-global/",
    art: "exame-2025",
  },
  {
    outlet: "Startupi",
    mark: "startupi.webp", w: 290, h: 96,
    year: "2025",
    title: "Just Travel recebe investimento da Bossa Invest para acelerar a digitalização do turismo em 2025",
    href: "https://startupi.com.br/just-travel-investimento-bossa-invest/",
    art: "startupi-2025",
  },
  {
    outlet: "Brasilturis",
    mark: "brasilturis.webp", w: 353, h: 96,
    year: "2025",
    title: "Just Travel recebe aporte e acelera expansão com foco em IA e internacionalização",
    href: "https://brasilturis.com.br/2025/06/16/just-travel-recebe-aporte-e-acelera-expansao-com-foco-em-ia-e-internacionalizacao/",
    art: "brasilturis-2025",
  },
  {
    outlet: "PANROTAS",
    mark: "panrotas.webp", w: 826, h: 96,
    year: "2025",
    title: "Just Travel lança plataforma 2.0 com foco em digitalização das agências de viagens",
    href: "https://www.panrotas.com.br/mercado/tecnologia/2025/04/just-travel-lanca-plataforma-20-com-foco-em-digitalizacao-das-agencias-de-viagens_216616.html",
    art: "panrotas-2025",
  },
  {
    outlet: "Startups",
    mark: "startups.webp", w: 446, h: 96,
    year: "2024",
    title: "Após transacionar R$ 150M em 2023, Just Travel quer levantar R$ 5M",
    href: "https://startups.com.br/negocios/rodada-de-investimento/apos-transacionar-r-150m-em-2023-just-travel-quer-levantar-r-5m/",
    art: "startups-2024",
  },
  {
    outlet: "TI Inside",
    mark: "tiinside.webp", w: 385, h: 96,
    year: "2022",
    title: "Just Travel recebe aporte de R$ 1,23 milhão da Sai do Papel Capital",
    href: "https://tiinside.com.br/30/09/2022/just-travel-recebe-aporte-de-r-123-milhao-da-sai-do-papel-capital/",
    art: "tiinside-2022",
  },
  {
    outlet: "Exame",
    mark: "exame.webp", w: 558, h: 96,
    year: "2022",
    title: "Just Travel: startup que leva agências de turismo para o digital é nova aposta de Bossanova e fundos",
    href: "https://exame.com/negocios/just-travel-startup-que-leva-agencias-turismo-para-digital/",
    art: "exame-2022",
  },
  {
    outlet: "PANROTAS",
    mark: "panrotas.webp", w: 826, h: 96,
    year: "2022",
    title: "Startup Just Travel vê oportunidade na digitalização de agências",
    href: "https://www.panrotas.com.br/mercado/tecnologia/2022/04/startup-just-travel-ve-oportunidade-na-digitalizacao-de-agencias_188700.html",
    art: "panrotas-2022",
  },
];

/* One clipping. The frame puts a screenshot of the article at the top of the
   card; what goes there instead is a crop of the brand's own pattern strip,
   offset per card by --press-i so no two cards open on the same tiles. Not a
   placeholder: we hold no right to republish an outlet's page as artwork, and
   the branded plate is the same finished treatment a category with no usable
   photograph gets in the catalogue.

   The masthead is the outlet's own published file, flattened to a white
   silhouette by scripts/press-logos.mjs so eight house palettes do not compete
   with the eight headlines they introduce. An outlet with no masthead cut yet
   falls back to type, which is the frame's own second variant (it sets Folha de
   S.Paulo and Panrotas that way).

   The whole card is the link, and the masthead reads out first (it is the img's
   alt), so a screen reader announces whose headline it is about to hear. Opens
   in a new tab, like every off-domain link on the site.

   Not a Reveal, which the cards on the other bands are: half of these are the
   marquee's duplicate and the rest are off the right edge of the track, so an
   observer per card would fire on a schedule the carousel decides. The track as
   a whole is the Reveal instead.

   `clone` marks the second run of the list, which exists only to give the
   marquee something to loop into: it is out of the a11y tree and out of the tab
   order, so a screen reader reads the eight clippings once and Tab visits each
   link once. */
function PressCard({ item, i, clone }) {
  return (
    <li className={"press-item" + (clone ? " press-item--clone" : "")}
        aria-hidden={clone || undefined}>
      <a className="press-card" href={item.href}
         target="_blank" rel="noopener noreferrer"
         tabIndex={clone ? -1 : undefined}
         style={{ "--press-i": i }}
         /* A click opens the article in a new tab, but never moves focus
            off this link in THIS one — there is no navigation here for it
            to go with. Left alone, :focus-within (below) keeps the track
            paused forever after the first click, with no way for a mouse
            user to clear it (unlike Tab, which can move focus elsewhere).
            Blurring on click releases it; NaMidia's own onBlur/restoreScroll
            already resets the frame's scroll position the same way it does
            when focus leaves via Tab, so this needs nothing new. Plain Tab
            focus (no click) is untouched — the track still parks while
            keyboard-tabbing through the list. */
         onClick={(e) => e.currentTarget.blur()}>
        {/* Real screenshot when the entry has one (see PRESS's own `art`
            field); otherwise the same decorative fallback this always
            rendered, untouched. Every PRESS entry has one as of this
            writing, but the fallback stays for whenever the next clipping
            arrives without a screenshot yet. */}
        <span className={"press-card__art" + (item.art ? " press-card__art--photo" : "")} aria-hidden="true">
          {item.art &&
            <picture>
              <source srcSet={`assets/photos/imprensa/${item.art}.avif`} type="image/avif" />
              <img src={`assets/photos/imprensa/${item.art}.webp`} alt=""
                   width="680" height="308" loading="lazy" decoding="async" />
            </picture>}
        </span>
        <span className="press-card__body">
          <span className="press-card__meta">
            {item.mark
              ? <img className="press-card__mark"
                     src={`assets/logos/imprensa/${item.mark}`}
                     alt={clone ? "" : item.outlet}
                     width={item.w} height={item.h}
                     loading="lazy" decoding="async" />
              : <span className="press-card__wordmark">{item.outlet}</span>}
            <span className="press-card__year">{item.year}</span>
          </span>
          <span className="press-card__title">{item.title}</span>
          <span className="press-card__cta">
            Ler a matéria
            <SIcon name="external" size={13} />
          </span>
        </span>
      </a>
    </li>);

}

/* The clippings run edge to edge on the same duplicate-and-translate marquee
   the supplier and investor strips use, which is what the frame's overflowing
   row is asking for.

   The one thing a marquee of LINKS has to answer that a marquee of logos does
   not is that a target which drifts out from under the pointer is a target
   nobody hits. Two answers, and the band needs both:
     - it parks on hover and on focus-within, so the card stops the moment it is
       aimed at or tabbed to, by pointer and by keyboard alike;
     - with motion off (the density switch, or the visitor's own reduced-motion
       setting) it is not a still frame with six cards stranded off screen: the
       track becomes a scroll rail and the duplicate half is dropped, so every
       clipping is still reachable.

   Tab still moves focus into a card that is off screen, and the browser answers
   that by scrolling the frame sideways. That is the right behaviour while focus
   is inside, and wrong the moment it leaves: the scroll offset stays, and the
   translate then runs the track past its own end and into empty space. So the
   frame's scroll is put back once focus leaves it, which is what onBlur is
   doing here. */
function NaMidia() {
  const frame = useRef(null);

  const restoreScroll = () => {
    // Read after the focus has actually moved: on blur, document.activeElement
    // is still the element being left.
    requestAnimationFrame(() => {
      const el = frame.current;
      if (el && !el.contains(document.activeElement)) el.scrollLeft = 0;
    });
  };

  return (
    <section className="section midia" id="midia">
      {/* The brand's mosaic as the band's field, cleared out of the middle by
          the mask in the stylesheet so the headline sits on the flat fill and
          the pattern stays in the corners. Decoration, and inert. */}
      <span className="midia__pattern" aria-hidden="true" />

      <div className="container midia__inner">
        <Reveal as="div" className="sect-head sect-head--center midia__head">
          <span className="eyebrow midia__eyebrow">Na mídia</span>
          <h2 className="h-section midia__title">Just Travel nas principais manchetes</h2>
        </Reveal>
      </div>

      <Reveal as="div" className="press-marquee" delay={80}>
        <div className="press-marquee__frame" ref={frame} onBlur={restoreScroll}>
          <ul className="press-track" aria-label="Just Travel na imprensa">
            {PRESS.map((item, i) =>
              <PressCard key={item.href} item={item} i={i} />
            )}
            {PRESS.map((item, i) =>
              <PressCard key={"clone-" + item.href} item={item} i={i} clone />
            )}
          </ul>
        </div>
      </Reveal>
    </section>);

}

Object.assign(window, {
  // SupplierLogos is not here: ProductCatalog renders it, so no page composes
  // it any more and it stays private to this module.
  Navbar, Metrics, ProductCatalog, HowItWorks,
  AiSuite, Investors, Pricing, Equipe, NaMidia,
  SIcon, Reveal, Counter,
  // The product taxonomy, read by produtos-app.jsx to build Produtos.html off
  // the same list the navbar and the landing catalogue render.
  CATEGORIES, PRODUCT_HREF
});
