// sections-mid.jsx — Creatives, Cases, WhyTraffy, FinalCTA
// All strings via t(key) for full EN/RU translation.

const { useState: __useState } = React;

function Creatives({ t }) {
  const items = [1, 2, 3].map(i => ({
    n: String(i).padStart(2, "0"),
    title: t(`creatives.${i}.title`),
    body: t(`creatives.${i}.body`),
  }));
  return (
    <section className="section creatives" id="creatives" data-screen-label="05 Creatives">
      <SectionRule />
      <div className="section-inner">
        <div className="creatives-head reveal">
          <DotLabel>{t("creatives.kicker")}</DotLabel>
          <h2>{t("creatives.title")}</h2>
          <div className="sub">{t("creatives.sub")}</div>
        </div>
        <div className="creatives-layout">
          <div className="creatives-grid glass-target">
            {items.map((c) => (
              <div className="creative-item reveal" key={c.n}>
                <h3>{c.title}</h3>
                <p>{c.body}</p>
              </div>
            ))}
          </div>
          <div className="creatives-kpi reveal">
            <KpiDashMockup />
          </div>
        </div>
      </div>
    </section>
  );
}

// ----- CASES: Library + Study, exclusive expand. Sources gated by Library. -----

// Library cases mapped to real assets in project/video/. Each item carries a
// `video` path (mp4 or html for playable demos) and a `poster` for the still
// frame shown before play. Three items per vertical.
const VIDEO_BASE = "video";
const LIBRARY_CASES = [
  // ---- Betting (sports / EPL) ----
  { id: "lib-betting-1", vertical: "Betting", label: "Winline · EPL",
    video: `${VIDEO_BASE}/07-winline-epl/winline-epl.mp4` },
  { id: "lib-betting-2", vertical: "Betting", label: "GSB · Football",
    video: `${VIDEO_BASE}/08-gsb-football/gsb-football.mp4` },
  { id: "lib-betting-3", vertical: "Betting", label: "YellowBet · EPL",
    video: `${VIDEO_BASE}/09-yellowbet-epl/yellowbet-epl.mp4` },
  { id: "lib-betting-4", vertical: "Betting", label: "Winline · Campaign",
    video: `${VIDEO_BASE}/10-winline-campaign-a/winline-campaign-a.mp4` },
  { id: "lib-betting-5", vertical: "Betting", label: "Winline · Promo",
    video: `${VIDEO_BASE}/11-winline-campaign-b/winline-campaign-b.mp4` },

  // ---- iGaming (slots / casino) ----
  { id: "lib-igaming-1", vertical: "iGaming", label: "YellowBet · Chicken Road",
    video: `${VIDEO_BASE}/04-yelowbet-chicken-road/yelowbet-chicken-road.mp4` },
  { id: "lib-igaming-2", vertical: "iGaming", label: "YellowBet · Slots",
    video: `${VIDEO_BASE}/05-yellowbet-slots/yellowbet-slots.mp4` },
  { id: "lib-igaming-3", vertical: "iGaming", label: "GSB · Aviator",
    video: `${VIDEO_BASE}/06-gsb-aviator/gsb-aviator.mp4` },
  { id: "lib-igaming-4", vertical: "iGaming", label: "Jugabet · Aviator",
    video: `${VIDEO_BASE}/12-jugabet-aviator-a/jugabet-aviator-a.mp4` },
  { id: "lib-igaming-7", vertical: "iGaming", label: "Jugabet · Aviator II",
    video: `${VIDEO_BASE}/13-jugabet-aviator-b/jugabet-aviator-b.mp4` },

  // ---- Playable (HTML demos with poster frames) ----
  { id: "lib-playable-1", vertical: "Playable", label: "Winline · Crashbox",
    video: `${VIDEO_BASE}/01-winline-crashbox/winline-crashbox.html`,
    poster: `${VIDEO_BASE}/01-winline-crashbox/winline-crashbox-poster.png` },
  { id: "lib-playable-2", vertical: "Playable", label: "Winline · Darts",
    video: `${VIDEO_BASE}/02-winline-darts/winline-darts.html`,
    poster: `${VIDEO_BASE}/02-winline-darts/winline-darts-poster.png` },
  { id: "lib-playable-3", vertical: "Playable", label: "Winline · Fortune",
    video: `${VIDEO_BASE}/03-winline-fortune/winline-fortune.html`,
    poster: `${VIDEO_BASE}/03-winline-fortune/winline-fortune-poster.png` },

  // ---- Ecom ----
  { id: "lib-ecom-1", vertical: "Ecom", label: "IVI · Campaign",
    video: `${VIDEO_BASE}/14-ivi-ecom-a/ivi-ecom-a.mp4` },
  { id: "lib-ecom-2", vertical: "Ecom", label: "IVI · Promo",
    video: `${VIDEO_BASE}/15-ivi-ecom-b/ivi-ecom-b.mp4` },
  { id: "lib-ecom-3", vertical: "Ecom", label: "IVI · Banner",
    video: `${VIDEO_BASE}/16-ivi-ecom-c/ivi-ecom-c.mp4` },

  // ---- Fintech ----
  { id: "lib-fintech-1", vertical: "Fintech", label: "Fintech · Campaign A",
    video: `${VIDEO_BASE}/17-fintech-a/fintech-a.mp4` },
  { id: "lib-fintech-2", vertical: "Fintech", label: "Fintech · Campaign B",
    video: `${VIDEO_BASE}/18-fintech-b/fintech-b.mp4` },
  { id: "lib-fintech-3", vertical: "Fintech", label: "Fintech · Campaign C",
    video: `${VIDEO_BASE}/19-fintech-c/fintech-c.mp4` },

  // ---- Crypto ----
  { id: "lib-crypto-1", vertical: "Crypto", label: "Crypto · Campaign A",
    video: `${VIDEO_BASE}/20-crypto-a/crypto-a.mp4` },
  { id: "lib-crypto-2", vertical: "Crypto", label: "Crypto · Campaign B",
    video: `${VIDEO_BASE}/21-crypto-b/crypto-b.mp4` },
  { id: "lib-crypto-3", vertical: "Crypto", label: "Crypto · Campaign C",
    video: `${VIDEO_BASE}/22-crypto-c/crypto-c.mp4` },
];

const VERTICALS = ["Betting", "iGaming", "Playable", "Ecom", "Fintech", "Crypto"];

function PlayIcon() {
  return (
    <svg viewBox="0 0 32 32" fill="none">
      <circle cx="16" cy="16" r="15" stroke="rgba(255,255,255,0.6)" strokeWidth="1.5"/>
      <path d="M13 11l9 5-9 5z" fill="rgba(255,255,255,0.85)"/>
    </svg>
  );
}

// ---- Video support for Case Library items ----
// Each item can have a video attached. Storage: localStorage keyed by case id.
// Two ways to attach: paste a URL (mp4 / webm / YouTube link) or upload a
// small video file (data-URL stashed locally). Plays in a modal overlay.

function youtubeEmbed(url) {
  const m = String(url).match(/(?:v=|youtu\.be\/|embed\/)([\w-]{6,})/);
  return m ? `https://www.youtube.com/embed/${m[1]}?autoplay=1&rel=0` : null;
}

function VideoModal({ url, onClose }) {
  const yt = youtubeEmbed(url);
  const isHtml = /\.html?(\?|$|#)/i.test(String(url));
  const videoRef = React.useRef(null);

  React.useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [onClose]);

  React.useEffect(() => {
    if (videoRef.current) {
      videoRef.current.play().catch(() => {});
    }
  }, []);

  return ReactDOM.createPortal(
    <div className="video-modal" onClick={onClose} role="dialog" aria-modal="true">
      <div className="video-modal-inner" onClick={(e) => e.stopPropagation()}>
        {isHtml
          ? <iframe className="video-modal-frame" src={url} title="Playable demo"
              frameBorder="0" allow="autoplay; fullscreen; gyroscope; accelerometer"
              allowFullScreen />
          : yt
            ? <iframe className="video-modal-frame" src={yt} title="Case video"
                frameBorder="0" allow="autoplay; encrypted-media; fullscreen"
                allowFullScreen />
            : <video ref={videoRef} className="video-modal-frame" src={url}
                controls autoPlay playsInline muted={false} />}
        <button className="video-modal-close" onClick={onClose} aria-label="Close">×</button>
      </div>
    </div>,
    document.body
  );
}

// Pull a poster frame out of an attached video. For YouTube URLs we use the
// official thumbnail; for direct URLs / uploaded data-URLs we draw the first
// frame onto a canvas. Cross-origin direct URLs will taint the canvas — we
// fall back to no poster in that case (caller surfaces the play overlay).
function youtubeId(url) {
  const m = String(url).match(/(?:v=|youtu\.be\/|embed\/)([\w-]{6,})/);
  return m ? m[1] : null;
}
function generatePoster(url) {
  return new Promise((resolve, reject) => {
    const yt = youtubeId(url);
    if (yt) {
      // hqdefault is 480x360, always exists. The component uses object-fit
      // cover so the letterboxing in hqdefault (16:9 inside 4:3) gets cropped.
      resolve(`https://img.youtube.com/vi/${yt}/hqdefault.jpg`);
      return;
    }
    const v = document.createElement("video");
    v.muted = true;
    v.playsInline = true;
    v.preload = "auto";
    v.crossOrigin = "anonymous";
    let done = false;
    const finish = (val, err) => {
      if (done) return; done = true;
      try { v.removeAttribute("src"); v.load(); } catch {}
      if (err) reject(err); else resolve(val);
    };
    v.addEventListener("loadeddata", () => {
      // Seek deeper into the clip — ad creatives often open on a black
      // intro/logo flash that captured as a blank poster. 1.5s in catches
      // a representative frame for almost every spot, with a safe cap.
      try {
        const dur = v.duration || 6;
        const target = Math.min(dur - 0.2, Math.max(1.5, dur * 0.18));
        v.currentTime = target;
      } catch {}
    });
    v.addEventListener("seeked", () => {
      try {
        const W = v.videoWidth || 640;
        const H = v.videoHeight || 360;
        const max = 640;
        const k = Math.min(1, max / Math.max(W, H));
        const c = document.createElement("canvas");
        c.width = Math.max(1, Math.round(W * k));
        c.height = Math.max(1, Math.round(H * k));
        c.getContext("2d").drawImage(v, 0, 0, c.width, c.height);
        finish(c.toDataURL("image/jpeg", 0.6));
      } catch (e) {
        finish(null, e);
      }
    });
    v.addEventListener("error", () => finish(null, new Error("video load failed")));
    setTimeout(() => finish(null, new Error("poster timeout")), 9000);
    v.src = url;
  });
}

function LibItem({ c }) {
  // v2 prefix: forces a clean slate after re-mapping cases to real video
  // files. Old auto-generated posters in localStorage (some captured stale
  // dev-UI frames) would otherwise persist and override real video posters.
  const vkey = `traffy.libvideo.v2.${c.id}`;
  const pkey = `traffy.libposter.v2.${c.id}`;
  // localStorage (user-uploaded) wins; otherwise fall back to the project's
  // preset video/poster declared on the case (LIBRARY_CASES above).
  const [video, setVideo] = __useState(() => {
    try {
      const stored = localStorage.getItem(vkey);
      if (stored) return stored;
    } catch {}
    return c.video || "";
  });
  const [poster, setPoster] = __useState(() => {
    try {
      const stored = localStorage.getItem(pkey);
      if (stored) return stored;
    } catch {}
    return c.poster || "";
  });
  const [posterLoading, setPosterLoading] = __useState(false);
  const [playing, setPlaying] = __useState(false);
  const [setterOpen, setSetterOpen] = __useState(false);
  const [draft, setDraft] = __useState("");
  const videoRef = React.useRef(null);

  const saveVideo = (val) => {
    try {
      if (val) localStorage.setItem(vkey, val);
      else localStorage.removeItem(vkey);
    } catch {}
    setVideo(val);
  };
  const savePoster = (val) => {
    try {
      if (val) localStorage.setItem(pkey, val);
      else localStorage.removeItem(pkey);
    } catch {}
    setPoster(val || "");
  };

  const isHtml = /\.html?(\?|$|#)/i.test(String(video));
  const isMP4 = !!video && !isHtml;

  // For HTML/Playable items: derive poster from the video if not already cached.
  // For .mp4 items we use an inline autoplay <video> instead — skip poster gen.
  React.useEffect(() => {
    if (!video || isMP4) { if (!video) savePoster(""); return; }
    if (poster) return;
    let cancelled = false;
    setPosterLoading(true);
    generatePoster(video)
      .then(p => { if (!cancelled && p) savePoster(p); })
      .catch(() => {})
      .finally(() => { if (!cancelled) setPosterLoading(false); });
    return () => { cancelled = true; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [video]);

  // Play/pause the inline preview video based on viewport visibility.
  // This fires automatically when the parent CaseCard opens and the card scrolls in.
  React.useEffect(() => {
    if (!isMP4 || !videoRef.current) return;
    const v = videoRef.current;
    const obs = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) v.play().catch(() => {});
        else v.pause();
      },
      { threshold: 0.1 }
    );
    obs.observe(v);
    return () => obs.disconnect();
  }, [isMP4, video]);

  const onUpload = (e) => {
    const f = e.target.files && e.target.files[0];
    e.target.value = "";
    if (!f) return;
    if (f.size > 8 * 1024 * 1024) {
      alert("Video too large (>8MB). Paste a URL instead.");
      return;
    }
    const reader = new FileReader();
    reader.onload = () => {
      const url = String(reader.result);
      // clear stale poster so the effect regenerates from the new clip
      savePoster("");
      saveVideo(url);
      setSetterOpen(false);
    };
    reader.readAsDataURL(f);
  };

  const stop = (e) => e.stopPropagation();
  const hasVideo = !!video;

  return (
    <div className={`lib-item${hasVideo ? " has-video" : ""}${isMP4 ? " has-preview-video" : ""}${!isMP4 && poster ? " has-poster" : ""}`} onClick={stop}>
      <image-slot id={c.id} shape="rect" placeholder={c.label}></image-slot>
      {isMP4 && (
        <video
          ref={videoRef}
          className="lib-preview-video"
          src={video}
          muted
          loop
          playsInline
          preload="none"
        />
      )}
      {!isMP4 && poster && (
        <img loading="lazy" decoding="async" className="lib-poster" src={poster} alt="" draggable="false" />
      )}
      <div className="label">{c.label}</div>

      {/* Play overlay — opens modal with sound on click */}
      <button
        className="lib-play"
        type="button"
        onClick={(e) => { stop(e); if (hasVideo) setPlaying(true); else setSetterOpen(true); }}
        aria-label={hasVideo ? "Play video" : "Add video"}
      >
        <PlayIcon />
      </button>

      {!isMP4 && posterLoading && <div className="lib-poster-loading">generating preview…</div>}

      {/* Dev controls (Edit/Remove + URL/file setter) removed from public UI.
          The author can still wire localStorage keys traffy.libvideo.v2.* and
          traffy.libposter.v2.* directly for ad-hoc overrides. */}
      {false && (
        <div className="lib-video-ctl" onClick={stop}>
          <button type="button" onClick={() => { setDraft(video); setSetterOpen(true); }}>
            {hasVideo ? "Edit video" : "+ Video"}
          </button>
          {hasVideo && (
            <button type="button" onClick={() => { savePoster(""); saveVideo(""); }}>Remove</button>
          )}
        </div>
      )}

      {/* Setter pop-over (dev only — gated off above) */}
      {false && setterOpen && (
        <div className="lib-video-setter" onClick={stop}>
          <div className="lib-video-setter-h">Attach video</div>
          <label className="lib-video-setter-row">
            <span>URL</span>
            <input
              type="text"
              placeholder="https://...  or  YouTube link"
              value={draft}
              onChange={(e) => setDraft(e.target.value)}
              autoFocus
            />
          </label>
          <div className="lib-video-setter-row">
            <span>File</span>
            <label className="lib-video-setter-upload">
              <input type="file" accept="video/*" onChange={onUpload} />
              <span>Upload (≤8MB)</span>
            </label>
          </div>
          <div className="lib-video-setter-foot">
            <button type="button" onClick={() => setSetterOpen(false)}>Cancel</button>
            <button
              type="button"
              className="primary"
              onClick={() => {
                const v = draft.trim();
                savePoster("");      // refresh poster for the new url
                saveVideo(v);
                setSetterOpen(false);
              }}
            >Save</button>
          </div>
        </div>
      )}

      {playing && <VideoModal url={video} onClose={() => setPlaying(false)} />}
    </div>
  );
}

function CaseCard({ title, summary, isOpen, isHidden, onToggle, children }) {
  const innerRef = React.useRef(null);
  const [contentH, setContentH] = __useState(1800);

  // Measure panel content height and track it via ResizeObserver so
  // the panel max-height exactly matches the rendered content — no
  // fixed 1800px ceiling that leaves wasted space.
  React.useEffect(() => {
    if (!innerRef.current) return;
    const update = () => { if (innerRef.current) setContentH(innerRef.current.scrollHeight); };
    update();
    const ro = new ResizeObserver(update);
    ro.observe(innerRef.current);
    return () => ro.disconnect();
  }, []);

  return (
    <div
      className={`case-card${isOpen ? " is-open" : ""}${isHidden ? " is-hidden" : ""}`}
      onClick={onToggle}
      role="button"
      tabIndex={0}
      onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onToggle(); }}}
    >
      <div className="head">
        <h3 dangerouslySetInnerHTML={{ __html: title }} />
        <div className="glyph" aria-hidden="true">+</div>
      </div>
      {summary && <p className="summary">{summary}</p>}
      <div className="panel" style={isOpen ? { maxHeight: `${contentH + 64}px` } : {}}>
        <div className="panel-inner" ref={innerRef} onClick={(e) => e.stopPropagation()}>
          {children}
        </div>
      </div>
    </div>
  );
}

// Y-fork: a single line drops in from above, splits into two graceful curves
// landing on the top of each card. Renders always (the silhouette is part of
// the section, not state). Coordinates are in a 1200×320 viewBox, scaled
// horizontally to match the cards (cards span ~80-560 and ~640-1120 with a
// 32px gap, mirrored around x=600).
function CasesForkSvg() {
  return (
    <svg className="cases-fork" viewBox="0 0 1200 320" preserveAspectRatio="none" aria-hidden="true">
      <defs>
        <filter id="cf-glow" x="-50%" y="-50%" width="200%" height="200%">
          <feGaussianBlur stdDeviation="1.6"/>
        </filter>
      </defs>
      <g fill="none" stroke="#7a4ee0" strokeWidth="1.6" filter="url(#cf-glow)" strokeLinecap="round">
        {/* main stem from very top */}
        <path d="M 600 0 V 150"/>
        {/* left branch — flows into top-center of library card */}
        <path d="M 600 150 C 600 230, 350 240, 300 320"/>
        {/* right branch — mirror */}
        <path d="M 600 150 C 600 230, 850 240, 900 320"/>
      </g>
    </svg>
  );
}

// ----- CASE STUDY body: structured spec sheet -----
// Left column: client + general info. Right column: KPI frames stacked.
// Frames use the same surface-adaptive tokens as case-card so they don't
// "break out" when the surface tweak switches between dark / grey / light.

function StudyArrow() {
  return (
    <svg className="study-arrow" viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <path d="M7 17 L17 7" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/>
      <path d="M9 7 L17 7 L17 15" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/>
    </svg>
  );
}

function StudyDollarGlyph() {
  // Refined CPI glyph — flat rounded square with a sharp typographic $ stroke.
  // Reads cleaner and more "spec sheet" than the circle-bezier version, and
  // matches the rest of the Traffy ui (radius 8, 1px hairline, no glow).
  return (
    <span className="study-glyph" aria-hidden="true">
      <svg viewBox="0 0 28 28" fill="none">
        <rect x="1" y="1" width="26" height="26" rx="8"
              fill="rgba(122,78,224,0.14)" stroke="rgba(122,78,224,0.45)" strokeWidth="1"/>
        {/* Vertical stroke through the glyph */}
        <path d="M14 6.5 V21.5" stroke="#cdb6ff" strokeWidth="1.6" strokeLinecap="round"/>
        {/* The double-curl: top loop opens right, bottom loop opens left */}
        <path d="M18.2 10.2 C 18.2 8.5, 16.5 7.6, 14 7.6 C 11.5 7.6, 9.8 8.9, 9.8 10.6
                 C 9.8 12.3, 11.5 13, 14 13.5 C 16.5 14, 18.2 14.7, 18.2 16.4
                 C 18.2 18.1, 16.5 19.4, 14 19.4 C 11.5 19.4, 9.8 18.5, 9.8 16.8"
              stroke="#cdb6ff" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/>
      </svg>
    </span>
  );
}

function StudyRetentionGlyph() {
  return (
    <span className="study-glyph" aria-hidden="true">
      <svg viewBox="0 0 24 24" fill="none">
        <circle cx="12" cy="12" r="11" fill="rgba(122,78,224,0.18)" stroke="rgba(122,78,224,0.45)" strokeWidth="1"/>
        <circle cx="12" cy="12" r="6" stroke="#a78bfa" strokeWidth="1.4"/>
        <path d="M12 6 a6 6 0 0 1 6 6" stroke="#a78bfa" strokeWidth="1.6" strokeLinecap="round"/>
        <circle cx="12" cy="12" r="1.5" fill="#a78bfa"/>
      </svg>
    </span>
  );
}

function StudyMetaRow({ label, children }) {
  return (
    <div className="study-meta-row">
      <dt>{label}</dt>
      <dd>{children}</dd>
    </div>
  );
}

function CaseStudyBody({ t }) {
  // Two flagship case studies — collapsed tiles with logos by default;
  // clicking a tile expands its full KPI breakdown in place.
  const cases = [
    {
      id: "study-binance",
      logo: "../../logo/Case study/Binance.png",
      clientName: "Binance",
      summary:   t("study.binance.summary"),
      period:    "2024 Q3 — Q3 2025",
      client:    "Binance · global crypto exchange",
      objective: t("study.binance.objective"),
      geo:       "Tier-1 / LATAM",
      sources:   t("study.binance.sources"),
      platform:  "Android, iOS",
      model:     "CPA (FTD)",
      cpiAndroid: "1.4–2",
      cpiIos:     "2.8–4.2",
      d7:         "16–20",
      d7Foot:     t("study.binance.d7foot"),
      funnel: [
        { num: "30–38", label: t("study.binance.f1"), w: "62%" },
        { num: "40–48", label: t("study.binance.f2"), w: "78%" },
        { num: "13–18", label: t("study.binance.f3"), w: "32%" },
      ],
    },
    {
      id: "study-bybit",
      logo: "../../logo/Case study/bybit.png",
      clientName: "Bybit",
      summary:   t("study.bybit.summary"),
      period:    "Q1 — Q3 2025",
      client:    "Bybit · tier-1 crypto exchange",
      objective: t("study.bybit.objective"),
      geo:       "RU / CIS",
      sources:   t("study.bybit.sources"),
      platform:  "Android, iOS",
      model:     "CPA",
      cpiAndroid: "1.6–2.3",
      cpiIos:     "3.2–4.8",
      d7:         "14–17",
      d7Foot:     t("study.bybit.d7foot"),
      funnel: [
        { num: "26–34", label: t("study.bybit.f1"), w: "55%" },
        { num: "36–44", label: t("study.bybit.f2"), w: "72%" },
        { num: "11–15", label: t("study.bybit.f3"), w: "28%" },
      ],
    },
  ];
  // Track which nested study is open (one at a time). Tiles stay collapsed
  // until clicked so the frames never overlap each other.
  const [openId, setOpenId] = __useState(null);
  const toggle = (id) => setOpenId((prev) => prev === id ? null : id);
  return (
    <div className="study-cases study-cases-stacked">
      {cases.map((d) => (
        <StudyTile
          key={d.id}
          d={d}
          t={t}
          isOpen={openId === d.id}
          onToggle={() => toggle(d.id)}
        />
      ))}
    </div>
  );
}

// Collapsed tile: logo + client name + one-line summary + chevron.
// Expanded: shows the full StudyCase KPI body inline.
function StudyTile({ d, isOpen, onToggle, t }) {
  return (
    <div
      className={`study-tile${isOpen ? " is-open" : ""}`}
      onClick={onToggle}
      role="button"
      tabIndex={0}
      onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onToggle(); }}}
    >
      <div className="study-tile-head">
        <div className="study-tile-logo">
          {d.logo && <img loading="lazy" decoding="async" src={d.logo} alt={d.clientName} draggable="false" />}
        </div>
        <div className="study-tile-meta">
          <div className="study-tile-name">{d.clientName}</div>
          <div className="study-tile-summary">{d.summary}</div>
        </div>
        <div className="study-tile-glyph" aria-hidden="true">{isOpen ? "−" : "+"}</div>
      </div>
      {isOpen && (
        <div className="study-tile-body" onClick={(e) => e.stopPropagation()}>
          <StudyCase d={d} t={t} />
        </div>
      )}
    </div>
  );
}

function StudyCase({ d, t }) {
  return (
    <div className="study-case">
      <div className="study-grid">
        {/* LEFT — client + general info */}
        <div className="study-col study-col-left">
          <div className="study-frame study-client-frame">
            <div className="study-label">{t("study.our_client")}</div>
            <div className="study-client-logo">
              {d.logo
                ? <img loading="lazy" decoding="async" src={d.logo} alt={d.clientName} draggable="false" />
                : <image-slot id={`${d.id}-client-logo`} shape="rect" fit="contain" placeholder="Client logo"></image-slot>}
            </div>
            {d.clientName && <div className="study-client-name">{d.clientName}</div>}
          </div>

          <div className="study-frame study-info-frame">
            <h4 className="study-section-h"><StudyArrow /> <strong>{t("study.general")}</strong> {t("study.info")}</h4>
            <dl className="study-meta">
              <StudyMetaRow label={t("study.period")}>{d.period}</StudyMetaRow>
              <StudyMetaRow label={t("study.client_lbl")}>{d.client}</StudyMetaRow>
              <StudyMetaRow label={t("study.objective")}>{d.objective}</StudyMetaRow>
              <StudyMetaRow label={t("study.geo")}>{d.geo}</StudyMetaRow>
              <StudyMetaRow label={t("study.sources_lbl")}>{d.sources}</StudyMetaRow>
              <StudyMetaRow label={t("study.platform")}>{d.platform}</StudyMetaRow>
              <StudyMetaRow label={t("study.model")}>{d.model}</StudyMetaRow>
            </dl>
          </div>
        </div>

        {/* RIGHT — KPI frames */}
        <div className="study-col study-col-right">
          <div className="study-frame study-kpi-frame">
            <h4 className="study-section-h">
              <StudyArrow /> <strong>{t("study.cpi")}</strong> <span className="kpi-sub">{t("study.cpi_sub")}</span>
            </h4>
            <div className="study-kpi-row">
              <StudyDollarGlyph />
              <div className="study-kpi-text">
                <div className="study-kpi-num">{d.cpiAndroid}</div>
                <div className="study-kpi-label">Android</div>
              </div>
            </div>
            <div className="study-kpi-row">
              <StudyDollarGlyph />
              <div className="study-kpi-text">
                <div className="study-kpi-num">{d.cpiIos}</div>
                <div className="study-kpi-label">iOS</div>
              </div>
            </div>
          </div>

          <div className="study-frame study-kpi-frame">
            <h4 className="study-section-h">
              <StudyArrow /> <strong>{t("study.d7")}</strong> {t("study.d7_sub")}
            </h4>
            <div className="study-kpi-row">
              <StudyRetentionGlyph />
              <div className="study-kpi-text">
                <div className="study-kpi-num">{d.d7}<span className="pct">%</span></div>
              </div>
            </div>
            <div className="study-kpi-foot">{d.d7Foot}</div>
          </div>

          <div className="study-frame study-kpi-frame">
            <h4 className="study-section-h">
              <StudyArrow /> <strong>{t("study.funnel")}</strong> <span className="kpi-sub">{t("study.funnel_sub")}</span>
            </h4>
            <div className="study-funnel">
              {d.funnel.map((row, i) => (
                <div className="study-funnel-row" key={i}>
                  <div className="study-funnel-num">{row.num}<span className="pct">%</span></div>
                  <div className="study-funnel-label">{row.label}</div>
                  <div className="study-funnel-bar" style={{ "--w": row.w }}></div>
                </div>
              ))}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

function Cases({ t }) {
  const [open, setOpen] = __useState(null);
  const [vertical, setVertical] = __useState("All");
  const toggle = (which) => setOpen(prev => prev === which ? null : which);

  // When a specific vertical is selected, show only that vertical's row.
  // For "All", show each vertical as its own labelled row so creatives stay
  // grouped (3 cards per row) instead of melting into one long carousel.
  const libGroups = vertical === "All"
    ? VERTICALS.map(v => ({ vertical: v, items: LIBRARY_CASES.filter(c => c.vertical === v) }))
    : [{ vertical, items: LIBRARY_CASES.filter(c => c.vertical === vertical) }];

  return (
    <section className={`section cases cases-state-${open || "closed"}`} id="cases" data-screen-label="06 Cases">
      <SectionRule />
      <div className="section-inner">
        <div className="cases-head reveal">
          <DotLabel>{t("cases.kicker")}</DotLabel>
          <h2>{t("cases.title")}</h2>
        </div>

        <div className="cases-split reveal">
          <CaseCard
            title={t("cases.lib.title")}
            summary={t("cases.lib.summary")}
            isOpen={open === "library"}
            isHidden={open === "study"}
            onToggle={() => toggle("library")}
          >
            {/* Heavy video content mounts only while the card is open — keeping
                ~19 autoplay <video> elements out of the DOM (and off the network)
                when the Library is collapsed removes the page-load and scroll jank. */}
            {open === "library" && (
              <>
                {/* Verticals tab bar */}
                <div className="lib-tabs" onClick={(e) => e.stopPropagation()}>
                  <button
                    className={`lib-tab${vertical === "All" ? " is-active" : ""}`}
                    onClick={() => setVertical("All")}
                  >{t("cases.tab.all") || "All"}</button>
                  {VERTICALS.map(v => (
                    <button
                      key={v}
                      className={`lib-tab${vertical === v ? " is-active" : ""}`}
                      onClick={() => setVertical(v)}
                    >{v}</button>
                  ))}
                </div>

                <div className="lib-groups">
                  {libGroups.map(g => (
                    <div className="lib-group" key={g.vertical}>
                      <div className="lib-group-head">{g.vertical}</div>
                      <div className="lib-row">
                        {g.items.map(c => <LibItem key={c.id} c={c} />)}
                      </div>
                    </div>
                  ))}
                </div>
              </>
            )}
          </CaseCard>

          <CaseCard
            title={t("cases.study.title")}
            summary={t("cases.study.summary")}
            isOpen={open === "study"}
            isHidden={open === "library"}
            onToggle={() => toggle("study")}
          >
            <CaseStudyBody t={t} />
          </CaseCard>
        </div>
      </div>
    </section>
  );
}

// ----- TRAFFIC SOURCES: now its own standalone section -----
// Cards (brand mark + name) reinterpreted in the dark Traffy aesthetic.
// Real logos load from project/logo/ when available; the monogram chip
// stays as a fallback for any source whose file is missing or fails to load.
function sourceMonogram(name) {
  const cleaned = name.replace(/[^A-Za-z0-9]/g, "");
  return (cleaned || name).slice(0, 2);
}
function sourceSrc(s) {
  // Bundled standalone HTML may inject blob URLs onto window.__resources keyed
  // by `source-<basename>`. Otherwise fall back to the relative path served by
  // the dev http server (../../logo/<file>).
  if (!s.file) return null;
  const fname = s.file.split("/").pop().split(".")[0];
  const rid = `source-${fname}`;
  return (window.__resources && window.__resources[rid]) || s.file;
}
function SourceMark({ src, name, tint, tile }) {
  const [failed, setFailed] = __useState(!src);
  return (
    <span className={`source-mark${tile === "dark" ? " mark--dark" : ""}`} aria-hidden="true">
      {!failed && src && (
        <img loading="lazy" decoding="async"
          className={tint === "black" ? "is-mono" : undefined}
          src={src}
          alt=""
          draggable="false"
          onError={() => setFailed(true)}
        />
      )}
      {failed && <span className="source-mono">{sourceMonogram(name)}</span>}
    </span>
  );
}
function TrafficSources({ t }) {
  return (
    <section className="section sources-section" id="sources" data-screen-label="Traffic Sources">
      <SectionRule />
      <div className="section-inner">
        <div className="sources-section-head reveal">
          <h2>{t("sources.title")}</h2>
        </div>
        <div className="sources-cards">
          {SOURCES.map((s, i) => (
            <div className="source-card reveal" style={{ "--rv-d": `${i * 40}ms` }} key={`src-${i}`}>
              <SourceMark src={sourceSrc(s)} name={s.name} tint={s.tint} tile={s.tile} />
              <span className="source-name">{s.name}</span>
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}

// Source entries: name shown to the user + relative file path under
// project/logo/. File is null where the client hasn't supplied a logo yet —
// the SourceMark falls back to the 2-letter monogram in that case.
const SOURCES = [
  { name: "Bigo",       file: "../../logo/bigo.png" },
  { name: "Kwai",       file: "../../logo/kwai.png" },
  { name: "Snapchat",   file: "../../logo/SNAP.svg" },
  { name: "TikTok",     file: "../../logo/TIKTOK_NOTE_WHITE.png" },
  { name: "Facebook",   file: "../../logo/Facebook.png" },
  { name: "X.com",      file: "../../logo/x.com (ex Twitter).png" },
  { name: "Yandex",     file: "../../logo/yandex.png" },
  { name: "AppLovin",   file: "../../logo/AppLovin.jpeg" },
  { name: "AppNext",    file: "../../logo/Appnext_.png" },
  { name: "Bidease",    file: "../../logo/bidease.png", tint: "black" },
  { name: "Petal",      file: "../../logo/Petal.png" },
  { name: "Jampp",      file: "../../logo/Jampp.png", tile: "dark" },
  { name: "LiftOff",    file: "../../logo/liftoff.png" },
  { name: "Mintegral",  file: "../../logo/mintegral.png" },
  { name: "Moloco",     file: "../../logo/moloko.png" },
  { name: "NativeX",    file: "../../logo/NativeX.png" },
  { name: "Persona.ly", file: "../../logo/Persona.ly.jpeg" },
  { name: "Unity",      file: "../../logo/unity.png" },
  { name: "Xiaomi",     file: "../../logo/xiami.png" },
  { name: "Xapads",     file: "../../logo/Xapads_Media.png" },
  { name: "Kayzen",     file: "../../logo/kayzen.jpeg" },
  { name: "Adikteev",   file: "../../logo/Adikteev.png" },
  { name: "Adlure",     file: "../../logo/Adlure.png" },
  { name: "Smadex",     file: "../../logo/smadex.jpeg" },
  { name: "Aarki",      file: "../../logo/aarki.png" },
  { name: "ShareIT",    file: "../../logo/SHAREit.png" },
];

const CLIENTS = [
  { name: "Магнит",          file: "clients/magnit.png" },
  { name: "Лига Ставок",     file: "clients/ligastavok.png" },
  { name: "LEON",            file: "clients/leon.png" },
  { name: "Winline",         file: "clients/winline.png" },
  { name: "Fonbet",          file: "clients/fonbet.png" },
  { name: "Ubet",            file: "clients/ubet.png" },
  { name: "1xBet",           file: "clients/1xbet.png" },
  { name: "Bybit",           file: "clients/bybit.png" },
  { name: "PARI",            file: "clients/pari.png" },
  { name: "BETERA",          file: "clients/betera.png" },
  { name: "MAXLINE",         file: "clients/maxline.png" },
  { name: "Pin-Up Global",   file: "clients/pinup.png" },
  { name: "Делимобиль",     file: "clients/delimobil.png" },
];
// Split into two rows so each row scrolls opposite directions.
// Distribution alternates to keep visual rhythm in each row.
const CLIENTS_ROW_A = CLIENTS.filter((_, i) => i % 2 === 0);
const CLIENTS_ROW_B = CLIENTS.filter((_, i) => i % 2 === 1);

// Resolve a client logo URL — preferring a bundled blob URL (standalone
// HTML export) and falling back to the original relative path.
function clientSrc(c) {
  const fname = c.file.split("/").pop().split(".")[0];
  const rid = `client-${fname}`;
  return (window.__resources && window.__resources[rid]) || c.file;
}

// ---- CountUpNum: animates a numeric stat from 0 to its value on scroll enter ----
function CountUpNum({ raw }) {
  const ref = React.useRef(null);
  const [display, setDisplay] = __useState(raw);
  const started = React.useRef(false);

  React.useEffect(() => {
    if (!ref.current) return;
    const m = raw.match(/^(\d+(?:\.\d+)?)(M|K|B)?([%+]+)?$/);
    if (!m) return;
    const target = parseFloat(m[1]);
    const suffix = (m[2] || "") + (m[3] || "");
    const observer = new IntersectionObserver(([entry]) => {
      if (!entry.isIntersecting || started.current) return;
      started.current = true;
      observer.disconnect();
      const duration = 1800;
      const startTime = performance.now();
      const easeOut = (t) => 1 - Math.pow(1 - t, 3);
      const frame = (now) => {
        const progress = Math.min(1, (now - startTime) / duration);
        setDisplay(`${Math.round(easeOut(progress) * target)}${suffix}`);
        if (progress < 1) requestAnimationFrame(frame);
      };
      requestAnimationFrame(frame);
    }, { threshold: 0.3 });
    observer.observe(ref.current);
    return () => observer.disconnect();
  }, [raw]);

  return <span ref={ref}>{display}</span>;
}

// ---- MagneticCTA: follows cursor with smooth lerp, spring-snaps back on leave ----
function MagneticCTA({ children, className, onClick }) {
  const ref = React.useRef(null);
  const [tx, setTx] = __useState(0);
  const [ty, setTy] = __useState(0);
  const leaving = React.useRef(false);

  const onMouseMove = (e) => {
    leaving.current = false;
    const rect = ref.current?.getBoundingClientRect();
    if (!rect) return;
    setTx((e.clientX - (rect.left + rect.width / 2)) * 0.35);
    setTy((e.clientY - (rect.top + rect.height / 2)) * 0.35);
  };
  const onMouseLeave = () => {
    leaving.current = true;
    setTx(0);
    setTy(0);
  };
  const onMouseDown = (e) => {
    if (!ref.current) return;
    const rect = ref.current.getBoundingClientRect();
    const ripple = document.createElement("span");
    const size = Math.max(rect.width, rect.height) * 2;
    Object.assign(ripple.style, {
      position: "absolute",
      borderRadius: "50%",
      width: `${size}px`,
      height: `${size}px`,
      left: `${e.clientX - rect.left - size / 2}px`,
      top: `${e.clientY - rect.top - size / 2}px`,
      background: "rgba(255,255,255,0.22)",
      transform: "scale(0)",
      animation: "cta-ripple 0.6s linear",
      pointerEvents: "none",
    });
    ref.current.appendChild(ripple);
    setTimeout(() => ripple.remove(), 640);
  };

  return (
    <button
      ref={ref}
      className={className}
      style={{
        transform: `translate(${tx}px, ${ty}px)`,
        transition: leaving.current
          ? "transform 0.45s cubic-bezier(0.2, 0.8, 0.2, 1)"
          : "transform 0.12s ease",
        position: "relative",
        overflow: "hidden",
      }}
      onMouseMove={onMouseMove}
      onMouseLeave={onMouseLeave}
      onMouseDown={onMouseDown}
      onClick={onClick}
    >
      {children}
    </button>
  );
}

function WhyTraffy({ t }) {
  // 7 stats with kicker labels — 30M+ is the feature tile (spans 2 columns).
  // Stat #4 folds 360° + anti-fraud into the SKAdNetwork tile (one quality).
  const STAT_CONFIG = [
    { key: "s2", feature: true  }, // 30M+ — FEATURE (no kicker)
    { key: "s1" },                  // 50+ GEOs
    { key: "s3" },                  // 50+ in-app platforms
    { key: "s4", wide: true     }, // 70%+ SKAdNetwork (combined)
    { key: "s5" },                  // Premium
    { key: "s6" },                  // CPI/CPA
    { key: "s7" },                  // Studio
  ];
  const stats = STAT_CONFIG.map(({ key, feature, wide }) => ({
    num:    t(`why.${key}.num`),
    desc:   t(`why.${key}.desc`),
    kicker: t(`why.${key}.kicker`),
    feature, wide,
  }));

  return (
    <section className="section why" id="why" data-screen-label="07 Why Traffy">
      <SectionRule />
      <div className="section-inner">
        <div className="why-head reveal">
          <DotLabel>{t("why.kicker")}</DotLabel>
          <h2 dangerouslySetInnerHTML={{ __html: t("why.title") }} />
        </div>

        <div className="why-tiles">
          {stats.map((s, i) => (
            <div
              className={[
                "why-tile reveal",
                s.feature ? "why-tile-feature" : "",
                s.wide ? "why-tile-wide" : "",
              ].filter(Boolean).join(" ")}
              style={{ "--rv-d": `${i * 65}ms` }}
              key={i}
            >
              <div className="why-tile-num"><CountUpNum raw={s.num} /></div>
              <div className="why-tile-desc">{s.desc}</div>
            </div>
          ))}
        </div>

      </div>
    </section>
  );
}

function Clients({ t }) {
  return (
    <section className="section clients-section" id="clients" data-screen-label="07 Clients">
      <div className="section-inner">
        <div className="clients-section-head reveal">
          <DotLabel>{t("why.clients")}</DotLabel>
        </div>
        <div className="clients-marquees">
          <div className="logo-marquee">
            <div className="logo-marquee-track">
              {[...CLIENTS, ...CLIENTS].map((c, i) => (
                <div className="marquee-logo client-logo" key={`cli-${i}`} title={c.name}>
                  <img loading="lazy" decoding="async" src={clientSrc(c)} alt={c.name} draggable="false" />
                </div>
              ))}
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}

function ArrowUpRight() {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <line x1="7" y1="17" x2="17" y2="7"/>
      <polyline points="9 7 17 7 17 15"/>
    </svg>
  );
}

function FinalCTA({ t, onLeaveRequest }) {
  return (
    <section className="contacts" id="contacts" data-screen-label="08 Contacts">
      <div className="contacts-inner">

        {/* 1. CTA — headline + buttons come first */}
        <div className="cta-stack reveal">
          <div className="cta-copy">
            <p>{t("contacts.headline1")}</p>
            <p dangerouslySetInnerHTML={{ __html: t("contacts.headline2") }} />
          </div>
          <div className="get-started-row">
            <button className="cta-pill huge" onClick={onLeaveRequest}>{t("contacts.cta")}</button>
            <button className="cta-arrow" onClick={onLeaveRequest} aria-label={t("contacts.cta")}>
              <ArrowUpRight />
            </button>
          </div>
        </div>

        {/* 2. People — SALES + PR */}
        <div className="contacts-people reveal">
          <div className="person-card">
            <div className="person-card-role">Sales</div>
            <div className="person-card-main">
              <div className="person-photo">
                <img loading="lazy" decoding="async" src="photo/Pavel.jpg" alt="Pavel Kotov" />
              </div>
              <div className="person-card-info">
                <div className="person-card-name">Pavel Kotov</div>
                <div className="person-card-subrole">Head of Sales</div>
              </div>
            </div>
            <div className="person-card-divider" />
            <a className="person-card-email" href="mailto:sales@traffy.com">sales@traffy.com</a>
            <div className="person-card-links">
              <a href="https://www.linkedin.com/in/pavelkotovbdm/" target="_blank" rel="noopener noreferrer">LinkedIn →</a>
              <a href="https://t.me/Pavel_Traffy" target="_blank" rel="noopener noreferrer">Telegram →</a>
            </div>
          </div>
          <div className="person-card">
            <div className="person-card-role">PR</div>
            <div className="person-card-main">
              <div className="person-photo">
                <img loading="lazy" decoding="async" src="photo/Veronica.jpg" alt="Veronika Neumerzhitckaia" />
              </div>
              <div className="person-card-info">
                <div className="person-card-name">Veronika Neumerzhitckaia</div>
                <div className="person-card-subrole">PR Manager</div>
              </div>
            </div>
            <div className="person-card-divider" />
            <a className="person-card-email" href="mailto:pr@traffy.com">pr@traffy.com</a>
            <div className="person-card-links">
              <a href="https://www.linkedin.com/in/nikabizdev/" target="_blank" rel="noopener noreferrer">LinkedIn →</a>
              <a href="https://t.me/VeronikaNeumerzhitckaia_Traffy" target="_blank" rel="noopener noreferrer">Telegram →</a>
            </div>
          </div>
        </div>

        {/* 3. Footer bar */}
        <div className="contacts-footer">
          <img loading="lazy" decoding="async" className="brand-mark" src={(window.__resources && window.__resources.traffyLogo) || "traffy-logo.png"} alt="traffy" />
          <div className="center-links">
            <a href="https://t.me/traffy_agency" target="_blank" rel="noopener noreferrer">Telegram</a>
            <a href="https://www.linkedin.com/company/traffy-official/" target="_blank" rel="noopener noreferrer">LinkedIn</a>
            <a href="https://www.instagram.com/traffy_agency/" target="_blank" rel="noopener noreferrer">Instagram</a>
            <a href="https://x.com/traffy_agency" target="_blank" rel="noopener noreferrer">X</a>
          </div>
          <span className="copy">©2026</span>
          <div className="footer-legal">
            {t("contacts.legal.name")} · {t("contacts.legal.company")} · {t("contacts.legal.date")} · {t("contacts.legal.address")}
          </div>
        </div>
      </div>
    </section>
  );
}

Object.assign(window, { Creatives, Cases, TrafficSources, WhyTraffy, Clients, FinalCTA, CLIENTS, clientSrc });
