// public/story-slides.jsx: window.StorySlides, one renderer per slide type in content/story.json.
// StoryPage (public/story-page.jsx) wraps each one in its <section> and passes:
//   slide    the slide's entry from window.STORY
//   lang     "en" or "es" (text falls back to English through window.pickLang)
//   inView   the slide is at least 30% on screen (drives counters, the drift, the rings and the wave)
//   labels   window.STORY.labels
//   go       go(key) scrolls to a slide key or "offerings"
//   next     the key of the slide after this one
(function () {
  const { useState, useEffect, useLayoutEffect, useRef } = React;
  const reduced = () => !!(window.MOTION && window.MOTION.reduce);
  const pick = (field, lang) => window.pickLang(field, lang);

  // Only a pointer that really moves counts as the visitor choosing a category: when the page scrolls
  // under a resting cursor the browser fires hover events too, and those must not switch it.
  let ptX = null, ptY = null, ptMoved = false;
  document.addEventListener("pointermove", (ev) => {
    ptMoved = !(ev.clientX === ptX && ev.clientY === ptY);
    ptX = ev.clientX; ptY = ev.clientY;
  }, true);

  function Img({ image, lang, className, style }) {
    return (
      <img className={className} src={image.src} width={image.width} height={image.height}
        alt={pick(image.alt, lang) || ""} loading="lazy" decoding="async" style={style} />
    );
  }

  // Counts up from zero each time the slide arrives; shows the final number with reduced motion.
  function Count({ to, run }) {
    const [v, setV] = useState(() => (reduced() ? to : 0));
    useEffect(() => {
      if (!run) return undefined;
      if (reduced()) { setV(to); return undefined; }
      const t0 = performance.now();
      let raf = 0;
      const step = (t) => {
        const k = Math.min(1, (t - t0) / 1400), e = 1 - Math.pow(1 - k, 3);
        setV(Math.round(to * e));
        if (k < 1) raf = requestAnimationFrame(step);
      };
      raf = requestAnimationFrame(step);
      return () => cancelAnimationFrame(raf);
    }, [run, to]);
    return <span className="rs-count">{v.toLocaleString("en-US")}</span>;
  }

  // A text with one phrase set apart (the "emphasis"): lime on the cover, blue in the ledger. Plain
  // text when the phrase is not in it.
  function Emph({ text, em, cls }) {
    const i = em ? text.indexOf(em) : -1;
    if (i < 0) return text;
    return <>{text.slice(0, i)}<em className={cls || "rs-em"}>{em}</em>{text.slice(i + em.length)}</>;
  }

  // Keeps a hyphenated word (Rivero-Borrell) on one line.
  function NoBreak({ text }) {
    return text.split(/(\S+-\S+)/).map((t, i) => (i % 2 ? <span key={i} className="rs-nobr">{t}</span> : t));
  }

  // The cover: Reurbano's purpose on its own, as a manifesto, and the two ways in. A tailored copy
  // dedicates it ("Prepared for" and the recipient's logo) in place of the kicker line.
  function Cover({ slide: s, lang, go, next }) {
    const pf = s.preparedFor;
    return (
      <div className="rs-cv">
        <div className="rs-cv-head">
          {pf ? (
            <p className="rs-kick rs-cv-for"><span>{pick(pf.label, lang)}</span><Img image={pf.logo} lang={lang} /></p>
          ) : (
            <p className="rs-kick">{pick(s.kicker, lang)} · {pick(s.place, lang)}</p>
          )}
          <p className="rs-cv-title">{pick(s.title, lang)}</p>
        </div>
        <h1 className="rs-cv-purpose"><Emph text={pick(s.purpose, lang)} em={s.emphasis ? pick(s.emphasis, lang) : ""} /></h1>
        <div className="rs-ctas">
          <button type="button" className="rs-btn rs-btn-lime" onClick={() => go(next)}>{pick(s.storyButton, lang)}</button>
          <button type="button" className="rs-btn rs-btn-light" onClick={() => go("offerings")}>{pick(s.offeringsButton, lang)}</button>
        </div>
      </div>
    );
  }

  // Our work: a lead-in and the three commitments every project shares, beside the projects in one
  // mosaic (each once, captioned), the track record in four numbers, and the press on a strip at the foot.
  function Work({ slide: s, lang, inView }) {
    return (
      <>
        <div className="rs-sin rs-wk">
          <div className="rs-wk-text">
            <h2 className="rs-h2">{pick(s.title, lang)}</h2>
            <p className="rs-wk-copy">{pick(s.copy, lang)}</p>
            <ol className="rs-lenses">
              {s.lenses.map((x, i) => (
                <li key={i}>
                  <h3>{pick(x.title, lang)}</h3>
                  {x.body ? <p>{pick(x.body, lang)}</p> : null}
                </li>
              ))}
            </ol>
            <a className="rs-wk-link" href={s.link.href} target="_blank" rel="noopener"><span>{pick(s.link.label, lang)}</span> →</a>
          </div>
          <div className="rs-mosaic">
            {s.projects.map((p, i) => (
              <figure key={i} className={p.size ? "rs-m-" + p.size : undefined}>
                <Img image={p.image} lang={lang} />
                <figcaption>{pick(p.name, lang)}</figcaption>
              </figure>
            ))}
          </div>
        </div>
        <div className="rs-sin rs-stats">
          {s.stats.map((st, i) => (
            <div key={i}>
              <div className="rs-stat-v"><span>{st.prefix}</span><Count to={st.value} run={inView} /><span>{st.suffix}</span></div>
              <div className="rs-stat-l">{pick(st.label, lang)}</div>
            </div>
          ))}
        </div>
        <div className="rs-pressbar">
          <div className="rs-sin">
            <p className="rs-kick">{pick(s.pressLabel, lang)}</p>
            <div className="rs-press">
              {s.press.map((p, i) => <Img key={i} image={p.image} lang={lang} style={{ height: `calc(var(--rs-pk) * ${p.scale})` }} />)}
            </div>
          </div>
        </div>
      </>
    );
  }

  // Team: the founders on cream, their rows lined up across the columns (photo, name, first and second
  // bio line: CSS subgrid), and the advisory board on a full-height blue panel beside them.
  function Team({ slide: s, lang }) {
    return (
      <div className="rs-tm">
        <div className="rs-tm-f">
          <h2 className="rs-h2">{pick(s.title, lang)}</h2>
          <div className="rs-tm-grid">
            {s.people.map((p, i) => (
              <article className="rs-tm-p" key={i}>
                <div className="rs-tm-ph"><Img image={p.photo} lang={lang} /></div>
                <h3 className="rs-tm-n"><NoBreak text={pick(p.name, lang)} /></h3>
                {p.bio.map((b, j) => <p key={j}>{pick(b, lang)}</p>)}
              </article>
            ))}
          </div>
        </div>
        <div className="rs-tm-b">
          <h2 className="rs-h2">{pick(s.boardTitle, lang)}</h2>
          {s.board.map((p, i) => (
            <div className="rs-tm-a" key={i}>
              <div className="rs-tm-aph"><Img image={p.photo} lang={lang} /></div>
              <div>
                <h3 className="rs-tm-an">{pick(p.name, lang)}</h3>
                <p className="rs-tm-role">{pick(p.role, lang)}</p>
                {p.logos && p.logos.length ? (
                  <div className="rs-tm-logos">
                    {p.logos.map((l, j) => <Img key={j} image={l.image} lang={lang} style={{ height: `calc(34px * ${l.scale})` }} />)}
                  </div>
                ) : null}
              </div>
            </div>
          ))}
        </div>
      </div>
    );
  }

  // Impact as one ledger: two columns (planet, people) whose rows line up, one row per lever or area
  // (its icon and name, the research claim with its figure in blue, the source), and an optional
  // measurement line at the foot.
  function Ledger({ slide: s, lang }) {
    return (
      <div className="rs-sin rs-ld">
        <h2 className="rs-h2">{pick(s.title, lang)}</h2>
        <div className="rs-ld-grid">
          {s.columns.map((c, i) => (
            <div className="rs-ld-col" key={i}>
              <div className="rs-ld-hd"><h3>{pick(c.title, lang)}</h3><p>{pick(c.sub, lang)}</p></div>
              {c.rows.map((r, j) => (
                <div className="rs-ld-row" key={j}>
                  <p className="rs-ld-l"><Img image={r.icon} lang={lang} />{pick(r.label, lang)}</p>
                  <div className="rs-ld-e">
                    <p><Emph text={pick(r.claim, lang)} em={r.emphasis ? pick(r.emphasis, lang) : ""} cls="rs-ld-fig" /></p>
                    <p className="rs-ld-src">{pick(r.source, lang)}</p>
                  </div>
                </div>
              ))}
            </div>
          ))}
        </div>
        {s.measure ? <p className="rs-ld-measure">{pick(s.measure, lang)}</p> : null}
      </div>
    );
  }

  // The last slide: one line, and the way on to the offerings.
  function Closing({ slide: s, lang, go }) {
    return (
      <div className="rs-sin rs-cl">
        <p className="rs-cl-line">{pick(s.line, lang)}</p>
        <div className="rs-ctas">
          <button type="button" className="rs-btn rs-btn-lime" onClick={() => go("offerings")}>{pick(s.offeringsButton, lang)}</button>
        </div>
      </div>
    );
  }

  // The problem: an optional lead line under the title (its emphasis in blue), then three or four
  // circles that drift apart as the slide arrives.
  function Apart({ slide: s, lang }) {
    return (
      <div className="rs-sin">
        <h2 className="rs-h2">{pick(s.title, lang)}</h2>
        {s.lead ? (
          <p className="rs-ap-lead"><Emph text={pick(s.lead, lang)} em={s.leadEmphasis ? pick(s.leadEmphasis, lang) : ""} cls="rs-ap-em" /></p>
        ) : null}
        <div className={"rs-apart" + (s.items.length === 4 ? " rs-apart-4" : "")}>
          {s.items.map((it, i) => (
            <div className={"rs-it rs-i" + i} key={i}>
              <Img image={it.image} lang={lang} className="rs-circ" />
              <div><h3>{pick(it.title, lang)}</h3><p>{pick(it.body, lang)}</p></div>
            </div>
          ))}
        </div>
      </div>
    );
  }

  // Our answer: the title and body beside the photo. With "moves", the body is a lead line and the
  // moves follow as a list.
  function Together({ slide: s, lang }) {
    return (
      <>
        <div className="rs-tg-txt">
          <h2 className="rs-h2">{pick(s.title, lang)}</h2>
          {s.moves ? (
            <>
              <p className="rs-tg-lead">{pick(s.body, lang)}</p>
              <ul className="rs-tg-moves">
                {s.moves.map((m, i) => (
                  <li key={i}><span aria-hidden="true" /><div><h3>{pick(m.title, lang)}</h3><p>{pick(m.body, lang)}</p></div></li>
                ))}
              </ul>
            </>
          ) : (
            <p>{pick(s.body, lang)}</p>
          )}
        </div>
        <div className="rs-tg-ph"><Img image={s.image} lang={lang} /></div>
      </>
    );
  }

  // Tree rings, outer to inner (the trunk drawing from the deck's icon). The inner five show from the
  // start; each further ring starts 4.5 s after the previous and takes 4 s, about 45 s in all. Time
  // only counts while the slide is on screen; with reduced motion every ring shows at once.
  const RING_PATHS = [
    "M500 110C660 108 790 180 855 310 905 410 905 500 880 580 870 615 880 640 905 665 935 695 925 735 870 755 820 775 800 805 815 840 825 870 780 895 700 895 600 895 470 905 360 855 250 805 170 700 145 560 120 420 175 290 290 210 360 160 425 115 500 110Z",
    "M500 165C640 168 760 235 815 355 855 445 850 525 825 595 815 625 825 655 850 680 870 700 855 725 815 735 780 745 770 770 785 795 790 815 750 830 685 830 595 830 480 840 385 795 295 755 225 665 200 545 180 420 230 310 330 245 395 205 445 168 500 165Z",
    "M500 210C625 215 730 275 780 385 815 465 808 540 785 605 778 628 788 652 808 672 820 685 808 705 778 712 752 720 745 742 758 762 762 778 728 790 672 790 590 790 488 798 405 758 325 720 265 640 240 535 222 425 268 328 358 272 415 238 455 212 500 210Z",
    "M500 250C610 254 705 308 748 408 778 478 770 548 748 608 740 628 750 650 766 665 776 675 766 690 742 697 720 704 715 722 728 738 730 752 700 762 652 762 580 762 492 768 420 732 350 698 297 628 275 532 260 432 300 348 380 298 432 268 462 250 500 250Z",
    "M500 290C595 294 678 340 718 428 745 490 735 552 715 608 708 625 717 644 730 656 738 664 730 676 712 682 695 688 692 702 702 715 705 728 680 738 638 738 575 738 498 742 432 710 372 680 325 618 305 532 292 442 328 368 398 322 445 295 470 290 500 290Z",
    "M500 330C580 333 650 372 685 448 708 500 698 555 682 604 676 618 684 633 695 643 700 650 692 659 678 664 665 668 663 679 670 690 672 702 652 712 618 712 568 712 502 714 445 688 392 662 350 610 332 532 320 452 350 388 412 348 452 322 478 330 500 330Z",
    "M500 370C568 372 625 402 655 468 675 512 665 560 652 602 647 615 654 627 662 634 665 640 658 648 648 652 638 655 637 664 642 672 645 682 630 688 602 688 562 688 510 690 460 668 415 646 378 602 362 535 352 465 378 408 428 375 462 355 485 370 500 370Z",
    "M500 410C555 412 600 435 625 488 642 525 632 565 622 598 618 608 624 618 630 624 632 629 626 635 619 638 612 640 612 648 615 654 618 662 608 666 588 666 555 666 518 668 478 650 440 632 408 595 395 538 388 478 408 432 448 405 472 390 490 410 500 410Z",
    "M500 450C543 452 580 470 600 510 612 540 602 572 595 594 592 602 596 610 600 614 602 618 598 623 593 625 588 627 588 633 590 638 593 644 585 648 572 648 548 648 525 650 495 635 465 620 438 590 425 545 420 498 438 460 470 438 488 428 495 450 500 450Z",
    "M500 488C532 490 560 505 575 535 583 555 575 580 570 595 568 602 570 608 573 610 575 613 572 617 568 618 564 620 565 624 567 627 568 632 562 635 553 635 535 635 518 635 498 624 478 612 460 590 452 558 448 520 462 492 488 478 498 472 500 488 500 488Z",
    "M502 522C522 524 540 535 552 558 558 572 553 590 550 602 549 607 550 612 553 613 554 615 553 618 550 619 547 620 548 623 550 625 551 628 547 631 540 631 528 631 516 630 503 623 488 614 478 598 472 575 470 548 480 528 498 520 502 517 502 522 502 522Z",
    "M504 548C518 550 530 558 538 572 542 582 538 594 535 600 534 603 535 606 537 607 538 609 537 611 535 612 533 612 534 614 535 615 536 617 533 619 528 619 520 619 512 618 502 613 492 608 484 598 480 582 478 565 486 552 498 548 502 546 504 548 504 548Z",
    "M506 568C514 570 522 575 526 583 528 590 526 597 524 600 523 602 524 604 525 605 526 606 525 607 524 608 523 609 523 610 524 611 524 612 522 613 519 613 514 613 510 613 504 610 498 606 494 600 491 590 490 580 495 572 503 569 506 568 506 568 506 568Z",
    "M508 585C513 586 517 590 518 594 519 597 517 600 515 601 514 602 515 603 516 604 516 605 514 606 511 606 507 606 504 605 502 602 499 598 501 591 506 587 508 585 508 585 508 585Z",
    "M510 594C512 594 514 596 514 598 514 600 512 601 510 601 508 601 506 599 507 597 508 595 510 594 510 594Z",
  ];
  const RING = { PRE: 5, START: 0.8, EVERY: 4.5, DUR: 4 };

  function Rings({ run }) {
    const svgRef = useRef(null);
    const st = useRef(null);
    useEffect(() => {
      const paths = Array.from(svgRef.current.querySelectorAll("path")).reverse();
      paths.forEach((p, i) => {
        const len = p.getTotalLength();
        p.style.strokeDasharray = String(len);
        p.style.strokeDashoffset = String(i < RING.PRE || reduced() ? 0 : len);
      });
      st.current = { paths, t: 0 };
    }, []);
    useEffect(() => {
      const s = st.current;
      if (!s || !run || reduced()) return undefined;
      let raf = 0, last = null;
      const tick = (now) => {
        if (last != null) s.t += (now - last) / 1000;
        last = now;
        let done = true;
        s.paths.forEach((p, i) => {
          if (i < RING.PRE) return;
          const k = Math.max(0, Math.min(1, (s.t - RING.START - (i - RING.PRE) * RING.EVERY) / RING.DUR));
          if (k < 1) done = false;
          const e = k < 0.5 ? 2 * k * k : 1 - Math.pow(-2 * k + 2, 2) / 2;
          p.style.strokeDashoffset = String(p.getTotalLength() * (1 - e));
        });
        if (!done) raf = requestAnimationFrame(tick);
      };
      raf = requestAnimationFrame(tick);
      return () => cancelAnimationFrame(raf);
    }, [run]);
    const svg = (cls, ref) => (
      <svg className={cls} ref={ref} viewBox="0 0 1000 1000" aria-hidden="true" fill="none" stroke="#111111" strokeLinecap="round" strokeLinejoin="round">
        {RING_PATHS.map((d, i) => <path key={i} d={d} />)}
      </svg>
    );
    return <div className="rs-ringbox">{svg("rs-rings rs-ghost")}{svg("rs-rings", svgRef)}</div>;
  }

  // Eleven by eight squares; a wave crosses them corner to corner every 2 s (CSS keyframes in
  // story.css), paused while the slide is off screen.
  function GridWave() {
    const cells = [];
    for (let y = 0; y < 8; y++) {
      for (let x = 0; x < 11; x++) {
        cells.push(<rect key={x + "-" + y} x={x * 18} y={y * 18} width="13" height="13" style={{ animationDelay: ((x + y) * 0.009).toFixed(3) + "s" }} />);
      }
    }
    return <svg className="rs-gridw" viewBox="0 0 193 139" aria-hidden="true">{cells}</svg>;
  }

  function Philosophy({ slide: s, lang, inView }) {
    return (
      <div className="rs-sin">
        <h2 className="rs-h2">{pick(s.title, lang)}</h2>
        <div className="rs-cards2">
          <div className="rs-slow"><Rings run={inView} /><h3>{pick(s.slow.title, lang)}</h3><p>{pick(s.slow.body, lang)}</p></div>
          <div className="rs-fast"><GridWave /><h3>{pick(s.fast.title, lang)}</h3><p>{pick(s.fast.body, lang)}</p></div>
        </div>
      </div>
    );
  }

  // The model: one slide in two steps over one drawing that holds still. First its definition on a blue
  // card; then the card gives way to the categories (Categories, below). On a desktop the slide is two
  // screens tall with its frame pinned under the header and a snap stop for each step, and how far the
  // visitor has scrolled through it (--mp, 0 to 1) fades one step out and the other in; only the step
  // on screen takes the pointer and the keyboard. On tablets and phones the steps simply follow each
  // other: the card, the drawing, the active category's labels, the categories.
  function Model({ slide: s, lang }) {
    const rootRef = useRef(null);
    const [mode, setMode] = useState({ pinned: false, step: 1 });
    useEffect(() => {
      const root = rootRef.current;
      const sec = root && root.closest(".rs-slide");
      const stick = root && root.querySelector(".rs-mx-stick");
      if (!sec || !stick) return undefined;
      let raf = 0;
      const update = () => {
        raf = 0;
        const pinned = getComputedStyle(stick).position === "sticky";
        let p = 1;
        if (pinned) {
          const r = sec.getBoundingClientRect(), top = parseFloat(getComputedStyle(stick).top) || 0;
          p = Math.min(1, Math.max(0, (top - r.top) / Math.max(1, r.height - stick.offsetHeight)));
        }
        const reduce = !!(window.MOTION && window.MOTION.reduce);
        root.style.setProperty("--mp", (reduce ? (p >= 0.5 ? 1 : 0) : p).toFixed(3));
        const step = p >= 0.5 ? 1 : 0;
        setMode((m) => (m.pinned === pinned && m.step === step ? m : { pinned, step }));
      };
      const onScroll = () => { if (!raf) raf = requestAnimationFrame(update); };
      window.addEventListener("scroll", onScroll, { passive: true });
      window.addEventListener("resize", onScroll);
      update();
      return () => {
        window.removeEventListener("scroll", onScroll);
        window.removeEventListener("resize", onScroll);
        cancelAnimationFrame(raf);
      };
    }, []);
    const off = (on) => (mode.pinned && !on ? { inert: "", "aria-hidden": "true" } : {});
    return (
      <div className="rs-mx" ref={rootRef} data-step={mode.step}>
        <div className="rs-mx-a" aria-hidden="true" />
        <div className="rs-mx-stick">
          <div className="rs-mv-card" {...off(mode.step === 0)}>
            <h2 className="rs-h2">{pick(s.title, lang)}</h2>
            <ul>{s.lines.map((l, i) => <li key={i}>{pick(l.text, lang)}</li>)}</ul>
          </div>
          <Categories slide={s} lang={lang} attrs={off(mode.step === 1)} />
        </div>
        <div className="rs-mx-next" aria-hidden="true" />
      </div>
    );
  }

  // A leader line: an S-curve from a point on a label's edge to a spot, its control points halfway along
  // the main axis. Lines leaving the same edge spread along it in the order of their spots, so they never
  // cross each other. box: the label's edges and spots' points in the same pixel space.
  function leaders(box, spots) {
    const out = [];
    ["top", "bottom", "left", "right"].forEach((edge) => {
      const mine = spots.filter((q) => q.edge === edge);
      const across = edge === "top" || edge === "bottom";
      mine.sort((p, q) => (across ? p.x - q.x : p.y - q.y));
      mine.forEach((q, k) => {
        const n = mine.length;
        let ax, ay, c1, c2;
        if (across) {
          ax = box.l + (box.r - box.l) * (n > 1 ? 0.2 + (0.6 * k) / (n - 1) : 0.5);
          ay = edge === "top" ? box.t : box.b;
          const m = (q.y - ay) / 2;
          c1 = [ax, ay + m]; c2 = [q.x, q.y - m];
        } else {
          ay = box.t + (box.b - box.t) * (n > 1 ? 0.3 + (0.4 * k) / (n - 1) : 0.5);
          ax = edge === "left" ? box.l : box.r;
          const m = (q.x - ax) / 2;
          c1 = [ax + m, ay]; c2 = [q.x - m, q.y];
        }
        const f = (v) => v.toFixed(1);
        out.push("M" + f(ax) + " " + f(ay) + " C" + f(c1[0]) + " " + f(c1[1]) + " " + f(c2[0]) + " " + f(c2[1]) + " " + f(q.x) + " " + f(q.y));
      });
    });
    return out;
  }

  // The model's categories (the model slide's second step): the drawing large with the categories beside
  // it. The active one (the first to begin with; hover or keyboard focus on a desktop, a tap on a phone)
  // opens its description, and its labels appear around the drawing, each joined to its spots by a
  // curved blue line (a label with no spots has no line). Where the labels cannot float (phones and
  // tablets) they are listed under the drawing, numbered like its spots; a label with no spots has no
  // number.
  function Categories({ slide: s, lang, attrs }) {
    const [active, setActive] = useState(0);
    const [drawn, setDrawn] = useState({ paths: [], dots: [] });
    const overRef = useRef(null);
    const labRefs = useRef([]);
    const cat = s.categories[active];
    // Each label's number, counting only the labels that have spots (0: no spots, so no number).
    let count = 0;
    const nums = cat.labels.map((l) => (l.spots && l.spots.length ? ++count : 0));

    // Lines and dots from the labels' measured boxes. Only where the labels float (desktop).
    useLayoutEffect(() => {
      const over = overRef.current;
      if (!over) return undefined;
      const draw = () => {
        const A = over.getBoundingClientRect();
        const paths = [], dots = [];
        if (A.width > 0 && getComputedStyle(over).position === "absolute") {
          cat.labels.forEach((l, i) => {
            const el = labRefs.current[i];
            if (!el) return;
            const b = el.getBoundingClientRect();
            const box = { l: b.left - A.left, t: b.top - A.top, r: b.right - A.left, b: b.bottom - A.top };
            const spots = (l.spots || []).map((q) => ({ x: q.x * A.width, y: q.y * A.height, edge: q.edge || l.box.edge }));
            leaders(box, spots).forEach((d) => paths.push({ d, i }));
            spots.forEach((q) => dots.push([q.x, q.y]));
          });
        }
        setDrawn({ paths, dots });
      };
      draw();
      const ro = new ResizeObserver(draw);
      ro.observe(over);
      let live = true;
      if (document.fonts && document.fonts.ready) document.fonts.ready.then(() => { if (live) draw(); });
      return () => { live = false; ro.disconnect(); };
    }, [active, lang]);

    // Focus and a click or tap pick a category at once. Hover picks it once the pointer has rested on it
    // for 100ms, so sweeping across the list does not flip through every category, and only when the
    // pointer really moves (not when the page scrolls under a resting cursor, or the rows move under it).
    const hoverRow = useRef(-1);
    const pending = useRef(-1);
    const timer = useRef(0);
    useEffect(() => () => clearTimeout(timer.current), []);
    const pickNow = (i) => () => { clearTimeout(timer.current); pending.current = -1; setActive(i); };
    const hover = (i) => () => {
      if (!ptMoved) return;
      hoverRow.current = i;
      if (i === active) { clearTimeout(timer.current); pending.current = -1; return; }
      if (pending.current === i) return;
      clearTimeout(timer.current);
      pending.current = i;
      timer.current = setTimeout(() => { pending.current = -1; if (hoverRow.current === i) setActive(i); }, 100);
    };
    const leave = () => { hoverRow.current = -1; pending.current = -1; clearTimeout(timer.current); };

    return (
      <>
        <div className="rs-sin rs-ex" {...attrs}>
          <h2 className="rs-h2">{pick(s.exploreTitle, lang)}</h2>
          <ul className="rs-ex-cats" onPointerLeave={leave}>
            {s.categories.map((c, i) => (
              <li key={i} className={i === active ? "rs-on" : undefined} onPointerMove={hover(i)}>
                <button type="button" aria-pressed={i === active} onFocus={pickNow(i)} onClick={pickNow(i)}>{pick(c.name, lang)}</button>
                <div className="rs-ex-desc" aria-hidden={i === active ? undefined : "true"}><p>{pick(c.body, lang)}</p></div>
              </li>
            ))}
          </ul>
        </div>
        <div className="rs-draw rs-ex-draw">
          <div className="rs-draw-art">
            <Img image={s.drawing} lang={lang} />
            {cat.labels.map((l, i) => (l.spots || []).map((q, j) => (
              <span key={active + "-" + i + "-" + j} className="rs-ex-dot" aria-hidden="true" style={{ left: q.x * 100 + "%", top: q.y * 100 + "%" }}>{nums[i]}</span>
            )))}
          </div>
        </div>
        <div className="rs-ex-over" ref={overRef} {...(attrs && attrs.inert !== undefined ? { "aria-hidden": "true" } : {})}>
          <svg className="rs-ex-lines" aria-hidden="true" key={"svg" + active}>
            {drawn.paths.map((p, k) => <path key={k} d={p.d} data-label={p.i} />)}
            {drawn.dots.map((p, i) => <circle key={i} cx={p[0]} cy={p[1]} r="5.5" />)}
          </svg>
          <ol className="rs-ex-labels" key={"labels" + active}>
            {cat.labels.map((l, i) => (
              <li key={i} ref={(el) => { labRefs.current[i] = el; }} data-edge={l.box.edge}
                style={{ left: l.box.x * 100 + "%", top: l.box.y * 100 + "%" }}>
                {nums[i] ? <span className="rs-ex-n" aria-hidden="true">{nums[i]}</span> : null}{pick(l.label, lang)}
              </li>
            ))}
          </ol>
        </div>
      </>
    );
  }

  function Screens({ slide: s, lang }) {
    return (
      <div className="rs-sin">
        <h2 className="rs-h2">{pick(s.title, lang)}</h2>
        <p className="rs-lead rs-ai-intro">{pick(s.intro, lang)}</p>
        <div className="rs-three rs-screens">
          {s.items.map((it, i) => (
            <div key={i}>
              <div className="rs-scr">
                <div className="rs-bar" aria-hidden="true"><i /><i /><i /></div>
                <div className="rs-shot"><Img image={it.screen} lang={lang} /></div>
              </div>
              <h3>{pick(it.title, lang)}</h3>
              <p>{pick(it.body, lang)}</p>
            </div>
          ))}
        </div>
        {s.note ? <p className="rs-note">{pick(s.note, lang)}</p> : null}
      </div>
    );
  }

  window.StorySlides = {
    cover: Cover, work: Work, team: Team, apart: Apart, together: Together, model: Model,
    philosophy: Philosophy, screens: Screens, ledger: Ledger, closing: Closing,
  };
})();
