/* =========================================================
   Testimonials — full-bleed band with a centered headline and
   a masonry-style grid of review cards.

   Hard-isolated from the rest of the page:
   - Constant renamed to TST_STYLE to avoid colliding with the
     `STYLE` constant in tools.jsx (Babel scripts share global
     scope; two top-level `const STYLE` declarations conflict
     and break whichever script loads second).
   - Prefix `.fr-tst-` (no overlap with `.frt-` toolkit or `.tsh-` legacy)
   - `isolation: isolate` creates an independent stacking context
   - Background painted via a `::before` pseudo-element so the section
     never changes its own width and never introduces horizontal scroll
   - All CSS lives inside this file and is scoped to `.fr-tst-…`

   Data: `t.tests` from i18n.jsx — { eyebrow, title, items[] }
   where each item is { q, name, role }.
   ========================================================= */

const TST_STYLE = `
/* Outer band — full document width, padding only vertical.
   Background lives on a ::before pseudo so the section box stays clean. */
.fr-tst-band {
  position: relative;
  width: 100%;
  padding: clamp(80px, 10vw, 140px) 0;
  isolation: isolate;
  overflow: hidden;
  box-sizing: border-box;
}
.fr-tst-band::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--surface);
  z-index: -1;
}
[data-theme="dark"] .fr-tst-band::before {
  background: #1a1a1a;
}

/* Decorative color blobs — soft brand-tinted halos sitting behind the
   grid so the section never feels flat. Two blobs in complementary
   palette colors, blurred and at low opacity so they read as ambient
   light rather than shapes. Sit on z-index 0 — above the bg layer at
   -1 but below the head/grid which paint on their own positive layers.

   Each blob has its own translate driven by a scroll-progress CSS
   variable (--p, 0–1) set on the section by JS as the band crosses
   the viewport. The blobs move at different rates / directions for
   a soft parallax that breathes when you scroll. */
.fr-tst-blob {
  position: absolute;
  border-radius: 50%;
  filter: blur(90px);
  pointer-events: none;
  z-index: 0;
  will-change: transform;
  transition: transform 0.18s linear;
}
.fr-tst-blob-a {
  width: 520px;
  height: 520px;
  background: var(--accent);
  opacity: 0.18;
  top: -160px;
  left: -120px;
  transform: translate3d(
    calc(var(--p, 0) * 80px),
    calc(var(--p, 0) * 60px),
    0
  );
}
.fr-tst-blob-b {
  width: 460px;
  height: 460px;
  background: var(--accent-2);
  opacity: 0.28;
  bottom: -180px;
  right: -100px;
  transform: translate3d(
    calc(var(--p, 0) * -90px),
    calc(var(--p, 0) * -50px),
    0
  );
}
.fr-tst-blob-c {
  width: 320px;
  height: 320px;
  background: var(--accent-4);
  opacity: 0.18;
  top: 40%;
  right: 18%;
  transform: translate3d(
    calc(var(--p, 0) * 50px),
    calc(var(--p, 0) * -120px),
    0
  );
}
[data-theme="dark"] .fr-tst-blob-a,
[data-theme="dark"] .fr-tst-blob-b,
[data-theme="dark"] .fr-tst-blob-c { opacity: 0.14; }

@media (prefers-reduced-motion: reduce) {
  .fr-tst-blob { transition: none; transform: none !important; }
}

/* Oversized decorative quote mark sitting behind the title area —
   pure typography, brand-tinted, low-opacity. Adds visual rhythm
   without slop-iconography or stock imagery. */
.fr-tst-quotemark {
  position: absolute;
  top: 24px;
  left: 50%;
  transform: translateX(-50%);
  font-family: var(--font-display);
  font-size: clamp(220px, 28vw, 380px);
  line-height: 0.6;
  font-weight: 800;
  color: color-mix(in srgb, var(--accent) 12%, transparent);
  pointer-events: none;
  user-select: none;
  z-index: 0;
}
[data-theme="dark"] .fr-tst-quotemark {
  color: color-mix(in srgb, var(--accent) 18%, transparent);
}

/* Centered head */
.fr-tst-head {
  position: relative;
  z-index: 1;
  max-width: 1200px;
  margin: 0 auto clamp(48px, 6vw, 80px);
  padding: 0 clamp(20px, 4vw, 64px);
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 18px;
  box-sizing: border-box;
}

/* Eyebrow — match the global .eyebrow pattern (mono caps + orange
   leading dot) used in every other section of the site. */
.fr-tst-badge {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-mute);
}
.fr-tst-badge::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
  display: inline-block;
}

.fr-tst-title {
  font-family: var(--font-display);
  font-size: clamp(40px, 6vw, 88px);
  font-weight: 800;
  line-height: 1.02;
  letter-spacing: -0.03em;
  color: var(--text);
  margin: 0;
  text-wrap: balance;
  word-break: normal;
  overflow-wrap: break-word;
  hyphens: none;
}
[data-theme="dark"] .fr-tst-title { color: #f3ede2; }

/* Carousel wrap — holds arrows + viewport in one row. */
.fr-tst-carousel {
  position: relative;
  z-index: 1;
  max-width: 1380px;
  margin: 0 auto;
  padding: 24px clamp(20px, 4vw, 64px);
  display: flex;
  align-items: center;
  gap: clamp(12px, 1.5vw, 20px);
  box-sizing: border-box;
}

.fr-tst-arrow {
  flex: 0 0 auto;
  width: 48px;
  height: 48px;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: var(--bg);
  color: var(--text);
  font-size: 18px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  font-family: inherit;
  transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease;
  z-index: 3;
}
.fr-tst-arrow:hover:not(:disabled) {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
  transform: translateY(-2px);
  box-shadow: 0 10px 24px -10px rgba(232, 98, 74, 0.5);
}
.fr-tst-arrow:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}
.fr-tst-arrow:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}
[data-theme="dark"] .fr-tst-arrow {
  background: #1f1f1f;
  border-color: rgba(255, 255, 255, 0.08);
  color: #f3ede2;
}

/* Viewport — fixed window that crops the track. Padding plus a soft
   alpha mask on all four sides give the centre card's tall shadow
   plenty of room to bleed out softly into the section background.
   The mask uses two gradients composited with intersect so both the
   horizontal AND the vertical edges fade instead of hard-cutting. */
.fr-tst-viewport {
  flex: 1 1 auto;
  min-width: 0;
  position: relative;
  --visible: 3;
  --gap: 20px;
  --card-w: calc((100% - (var(--visible) - 1) * var(--gap)) / var(--visible));
  --idx: 0;
  overflow: hidden;
  padding: 64px 36px 80px;
  -webkit-mask-image:
    linear-gradient(to right,
      transparent 0,
      black 36px,
      black calc(100% - 36px),
      transparent 100%),
    linear-gradient(to bottom,
      black 0,
      black calc(100% - 60px),
      transparent 100%);
  -webkit-mask-composite: source-in;
          mask-image:
    linear-gradient(to right,
      transparent 0,
      black 36px,
      black calc(100% - 36px),
      transparent 100%),
    linear-gradient(to bottom,
      black 0,
      black calc(100% - 60px),
      transparent 100%);
          mask-composite: intersect;
}
@media (max-width: 1099px) and (min-width: 760px) {
  .fr-tst-viewport {
    --visible: 2;
    --gap: 16px;
    padding: 48px 28px 72px;
  }
}
@media (max-width: 759px) {
  .fr-tst-viewport {
    --visible: 1;
    --gap: 0px;
    padding: 36px 16px 64px;
  }
}

/* Track — lays the full set of cards in a flex row, translates as the
   user moves through the carousel. Smooth easing on transform for the
   slide animation. */
.fr-tst-track {
  display: flex;
  gap: var(--gap);
  width: 100%;
  transform: translateX(calc(var(--idx) * (var(--card-w) + var(--gap)) * -1));
  transition: transform 0.7s cubic-bezier(0.2, 0.7, 0.2, 1);
  will-change: transform;
}
@media (prefers-reduced-motion: reduce) {
  .fr-tst-track { transition: none; }
}

.fr-tst-card {
  position: relative;
  flex: 0 0 var(--card-w);
  width: var(--card-w);
  background: var(--bg);
  border: 1px solid transparent;
  border-radius: 28px;
  padding: 32px;
  box-shadow: var(--shadow);
  display: flex;
  flex-direction: column;
  gap: 12px;
  box-sizing: border-box;
  min-height: 320px;
  transition:
    transform 0.6s cubic-bezier(0.2, 0.7, 0.2, 1),
    box-shadow 0.6s cubic-bezier(0.2, 0.7, 0.2, 1),
    border-color 0.45s ease,
    opacity 0.45s ease;
}
[data-theme="dark"] .fr-tst-card { background: #222; }

/* Narrow-screen card polish: tighter padding so the lone card on
   mobile doesn't feel airy and the quote does the visual work. */
@media (max-width: 759px) {
  .fr-tst-card {
    padding: 24px;
    border-radius: 22px;
    min-height: 260px;
    max-width: 520px;
    margin: 0 auto;
  }
}

/* Centre / side states only apply where 3 cards fit (wide desktop). At
   narrower widths the highlight is suppressed so the 2-up tablet and
   1-up phone layouts feel balanced. */
@media (min-width: 1100px) {
  .fr-tst-card.is-side {
    transform: scale(0.97);
    opacity: 0.92;
  }
  .fr-tst-card.is-center {
    transform: translateY(-8px) scale(1.04);
    z-index: 2;
    border-color: color-mix(in srgb, var(--accent) 30%, transparent);
    box-shadow:
      0 60px 100px -30px rgba(20, 18, 14, 0.28),
      0 24px 48px -24px rgba(20, 18, 14, 0.16),
      0 0 0 1px color-mix(in srgb, var(--accent) 18%, transparent);
  }
  .fr-tst-card.is-center::after {
    content: "";
    position: absolute;
    inset: -1px;
    border-radius: 28px;
    pointer-events: none;
    background: linear-gradient(180deg,
      color-mix(in srgb, var(--accent) 8%, transparent) 0%,
      transparent 35%);
    z-index: 0;
  }
  [data-theme="dark"] .fr-tst-card.is-center {
    box-shadow:
      0 60px 100px -30px rgba(0, 0, 0, 0.6),
      0 24px 48px -24px rgba(0, 0, 0, 0.4),
      0 0 0 1px color-mix(in srgb, var(--accent) 30%, transparent);
  }
  /* Cards outside the visible window keep the base style but stay flat. */
  .fr-tst-card.is-offscreen { opacity: 0.4; pointer-events: none; }
}
.fr-tst-card > * { position: relative; z-index: 1; }

/* Dots indicator below the track so the user can see progress. */
.fr-tst-dots {
  display: flex;
  justify-content: center;
  gap: 8px;
  margin-top: 8px;
  position: relative;
  z-index: 1;
}
.fr-tst-dot {
  width: 28px;
  height: 4px;
  border-radius: 99px;
  background: var(--line);
  border: 0;
  cursor: pointer;
  transition: background 0.25s ease, width 0.25s ease;
  padding: 0;
}
.fr-tst-dot:hover { background: var(--text-mute); }
.fr-tst-dot.active { background: var(--accent); width: 48px; }

/* Stars row — 5 layered glyphs supporting full / half / empty fills.
   Each star renders as a translucent base “★” with an absolutely
   positioned overlay “★” in accent colour, clipped to a percentage
   of the star’s width. So rating = 4.5 lights 4 full + 1 half. */
.fr-tst-stars {
  display: inline-flex;
  gap: 3px;
  margin-bottom: 12px;
  font-size: 16px;
  line-height: 1;
}
.fr-tst-star {
  position: relative;
  display: inline-block;
  width: 1em;
  height: 1em;
  line-height: 1;
}
.fr-tst-star .bg {
  color: color-mix(in srgb, var(--accent) 22%, transparent);
}
.fr-tst-star .fg {
  color: var(--accent);
  position: absolute;
  inset: 0;
  overflow: hidden;
  white-space: nowrap;
  display: inline-block;
}
.fr-tst-rating-num {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.04em;
  color: var(--text-mute);
  margin-left: 8px;
  align-self: center;
}

.fr-tst-quote {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(16px, 1.4vw, 20px);
  line-height: 1.35;
  letter-spacing: -0.015em;
  color: var(--text);
  margin: 0 0 8px;
  text-wrap: pretty;
}
[data-theme="dark"] .fr-tst-quote { color: #f3ede2; }

.fr-tst-body {
  font-size: 14px;
  line-height: 1.55;
  color: var(--text-2);
  margin: 0 0 8px;
}
[data-theme="dark"] .fr-tst-body { color: #9b9a96; }

.fr-tst-author {
  margin-top: auto;
  padding-top: 20px;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.fr-tst-name {
  font-weight: 600;
  font-size: 14px;
  color: var(--text);
}
[data-theme="dark"] .fr-tst-name { color: #f3ede2; }
.fr-tst-company {
  font-size: 13px;
  color: var(--text-2);
}
[data-theme="dark"] .fr-tst-company { color: #9b9a96; }
`;

/* ---------- Helpers ---------- */
// Split a long quote into a bold headline + softer body when the first
// sentence ends with . ! or ?. Otherwise the whole quote is the headline.
function tstSplitQuote(q) {
  const s = String(q || "");
  const m = s.match(/^([^.!?\n]+[.!?])\s+(.+)$/);
  if (m) return { headline: m[1], body: m[2] };
  return { headline: s, body: "" };
}

/* ---------- Component ---------- */
const { useRef: useRefTST, useEffect: useEffectTST, useState: useStateTST } = React;

// Star row supporting fractional ratings (0..5). `value` is a number.
const StarRow = ({ value, label }) => {
  const v = Math.max(0, Math.min(5, Number(value) || 0));
  const stars = [];
  for (let i = 0; i < 5; i++) {
    const fill = Math.max(0, Math.min(1, v - i));
    stars.push(
      <span key={i} className="fr-tst-star" aria-hidden="true">
        <span className="bg">★</span>
        <span className="fg" style={{ width: `${fill * 100}%` }}>★</span>
      </span>
    );
  }
  const valueStr = Number.isInteger(v) ? v.toFixed(0) : v.toFixed(1);
  return (
    <div className="fr-tst-stars" aria-label={label || `${valueStr} out of 5 stars`}>
      {stars}
      <span className="fr-tst-rating-num">{valueStr}</span>
    </div>
  );
};

const Testimonials = ({ t }) => {
  const data = (t && t.tests) || {};
  const items = Array.isArray(data.items) ? data.items : [];

  // Carousel idx — represents the left-most visible card. We render
  // ALL items in a horizontal track and translate the track left by
  // (idx * card width). Visible window: 3 desktop / 2 tablet / 1 phone.
  // Initial idx is the middle of the carousel so the user lands on a
  // central testimonial rather than the first one.
  const initialVisible = (() => {
    if (typeof window === "undefined") return 3;
    if (window.matchMedia("(min-width: 1100px)").matches) return 3;
    if (window.matchMedia("(min-width: 760px)").matches) return 2;
    return 1;
  })();
  const [visibleCount, setVisibleCount] = useStateTST(initialVisible);
  const total = items.length;
  const maxIdx = Math.max(0, total - visibleCount);
  const [idx, setIdxTST] = useStateTST(() =>
    Math.max(0, Math.floor((items.length - initialVisible) / 2))
  );
  // Centre slot — the visually elevated card always sits in the middle
  // of the visible window (slot 1 of 3 on desktop, slot 0 of 1 on mobile).
  const centreSlot = Math.floor(visibleCount / 2);

  // Track responsive visible count via matchMedia with two breakpoints.
  // When the viewport changes width we clamp idx into range but don't
  // re-centre, so the user keeps whatever position they were on.
  useEffectTST(() => {
    if (typeof window === "undefined") return;
    const mqWide = window.matchMedia("(min-width: 1100px)");
    const mqMid = window.matchMedia("(min-width: 760px)");
    const apply = () => {
      const v = mqWide.matches ? 3 : mqMid.matches ? 2 : 1;
      setVisibleCount(v);
      setIdxTST((cur) => Math.min(cur, Math.max(0, total - v)));
    };
    apply();
    mqWide.addEventListener("change", apply);
    mqMid.addEventListener("change", apply);
    return () => {
      mqWide.removeEventListener("change", apply);
      mqMid.removeEventListener("change", apply);
    };
  }, [total]);

  const next = () => setIdxTST((i) => Math.min(maxIdx, i + 1));
  const prev = () => setIdxTST((i) => Math.max(0, i - 1));
  const goTo = (target) => setIdxTST(Math.max(0, Math.min(maxIdx, target)));

  // Scroll-driven background animation. We compute a 0..1 progress value
  // representing how far the section has travelled through the viewport
  // (0 = section just entering from the bottom, 1 = section just exited
  // past the top) and write it to a CSS custom property `--p` on the
  // section element. CSS reads --p on each blob to translate it; each
  // blob has a different direction & magnitude so they drift apart as
  // the user scrolls. rAF-throttled, passive listener, no per-frame work
  // when the section isn't on screen.
  const sectionRef = useRefTST(null);
  useEffectTST(() => {
    const el = sectionRef.current;
    if (!el) return;
    if (typeof window === "undefined") return;

    // Respect prefers-reduced-motion — don't animate, blobs stay put.
    const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
    if (mq.matches) return;

    let raf = 0;
    let onScreen = false;
    const update = () => {
      raf = 0;
      const r = el.getBoundingClientRect();
      const vh = window.innerHeight || 1;
      // p = 0 when section bottom is just touching viewport top (above)
      //   = 1 when section top is just touching viewport bottom (below)
      // We want the inverse-feeling so blobs drift as you scroll the
      // section through view: as r.top decreases (section moves up),
      // p increases.
      const total = r.height + vh;
      const travelled = vh - r.top;
      const p = Math.max(0, Math.min(1, travelled / total));
      el.style.setProperty("--p", p.toFixed(4));
    };
    const onScroll = () => {
      if (!onScreen) return;
      if (raf) return;
      raf = requestAnimationFrame(update);
    };

    // Only listen to scroll while the section intersects the viewport.
    // Cheaper on long pages.
    let io;
    if (typeof IntersectionObserver !== "undefined") {
      io = new IntersectionObserver(
        (entries) => {
          for (const entry of entries) {
            onScreen = entry.isIntersecting;
            if (onScreen) update();
          }
        },
        { threshold: 0, rootMargin: "100px" }
      );
      io.observe(el);
    } else {
      onScreen = true;
    }

    // Prime once and start listening.
    update();
    window.addEventListener("scroll", onScroll, { passive: true });
    window.addEventListener("resize", onScroll);
    return () => {
      window.removeEventListener("scroll", onScroll);
      window.removeEventListener("resize", onScroll);
      if (io) io.disconnect();
      if (raf) cancelAnimationFrame(raf);
    };
  }, []);

  return (
    <section ref={sectionRef} className="fr-tst-band" id="testimonials">
      <style dangerouslySetInnerHTML={{ __html: TST_STYLE }} />

      {/* Decorative color blobs — soft halos behind the grid so the
          section reads with brand warmth instead of flat surface. */}
      <span className="fr-tst-blob fr-tst-blob-a" aria-hidden="true" />
      <span className="fr-tst-blob fr-tst-blob-b" aria-hidden="true" />
      <span className="fr-tst-blob fr-tst-blob-c" aria-hidden="true" />

      {/* Giant decorative quote mark behind the title. */}
      <span className="fr-tst-quotemark" aria-hidden="true">“</span>

      <div className="fr-tst-head">
        <div className="fr-tst-badge reveal">TESTIMONIALS</div>
        <h2 className="fr-tst-title reveal" data-d="100">
          {data.title}
        </h2>
      </div>

      <div className="fr-tst-carousel">
        <button
          type="button"
          className="fr-tst-arrow fr-tst-arrow-prev"
          onClick={prev}
          disabled={idx <= 0}
          aria-label="Previous testimonial"
        >
          ←
        </button>

        <div className="fr-tst-viewport" style={{ "--idx": idx }}>
          <div className="fr-tst-track">
            {items.map((it, i) => {
              const { headline, body } = tstSplitQuote(it.q);
              const role = String(it.role || "");
              const sepIdx = role.indexOf("·");
              const company =
                sepIdx >= 0 ? role.slice(sepIdx + 1).trim() : role.trim();
              const slot = i - idx; // 0..visibleCount-1 if visible
              const visible = slot >= 0 && slot < visibleCount;
              const isCenter = visible && slot === centreSlot;
              const isSide = visible && !isCenter;
              const isOffscreen = !visible;
              const rating = typeof it.rating === "number" ? it.rating : 5;
              return (
                <article
                  key={i}
                  className={
                    "fr-tst-card" +
                    (isCenter ? " is-center" : "") +
                    (isSide ? " is-side" : "") +
                    (isOffscreen ? " is-offscreen" : "")
                  }
                  aria-hidden={isOffscreen ? "true" : undefined}
                >
                  <StarRow value={rating} />
                  <p className="fr-tst-quote">{headline}</p>
                  {body && <p className="fr-tst-body">{body}</p>}
                  <div className="fr-tst-author">
                    <div className="fr-tst-name">{it.name}</div>
                    <div className="fr-tst-company">{company}</div>
                  </div>
                </article>
              );
            })}
          </div>
        </div>

        <button
          type="button"
          className="fr-tst-arrow fr-tst-arrow-next"
          onClick={next}
          disabled={idx >= maxIdx}
          aria-label="Next testimonial"
        >
          →
        </button>
      </div>

      {/* Progress dots — one per reachable position so the user can
          jump straight to any slide. */}
      {maxIdx > 0 && (
        <div className="fr-tst-dots" role="tablist" aria-label="Testimonial pages">
          {Array.from({ length: maxIdx + 1 }).map((_, i) => (
            <button
              key={i}
              type="button"
              className={"fr-tst-dot" + (i === idx ? " active" : "")}
              onClick={() => goTo(i)}
              aria-label={`Go to slide ${i + 1}`}
              aria-selected={i === idx}
            />
          ))}
        </div>
      )}
    </section>
  );
};

window.Testimonials = Testimonials;
