/* Pages — composed from blocks. All copy resolved via useLang() / t. */

const PageHome = ({ setRoute }) => {
  const { t } = useLang();
  const H = t.home;
  return (
    <>
      <HeroCarousel setRoute={setRoute} />
      <VerticalsHero setRoute={setRoute} />

      {/* Client trust strip — desaturated logos, marquee */}
      <ClientStrip />

      {/* Vision memo — photo full-bleed on the left, everything else stacked
          on the right (pullquote + stats + rule + foot). No more container
          constraint on the photo column. */}
      <section className="section--memo">
        <div className="memo">
          <Reveal className="memo__photo">
            <img src="assets/hero_santander_noche.jpg" alt="" loading="lazy" />
            {H.memo.photoCredit && (
              <span className="memo__credit">{H.memo.photoCredit}</span>
            )}
          </Reveal>
          <div className="memo__col">
            <Reveal className="memo__l">
              <span className="pullquote" dangerouslySetInnerHTML={{
                __html: H.memo.pullquote.replace(/\{(\d+)\}/g, '<span class="accent">$1</span>')
              }} />
            </Reveal>
            <div className="memo__r">
              {H.stats.map((s,i) => (
                <Reveal key={i} delay={i * 100}>
                  <Stat {...s} />
                </Reveal>
              ))}
            </div>
          </div>
        </div>
      </section>

      {/* Certifications — static grid of every accreditation the firm
          holds + every Uptime Tier seal earned on a real project.
          Two rows on desktop (7 columns × 2), wraps freely on
          narrower viewports. Each logo is sized large enough that
          the small print on the Tier seals (project name + date)
          stays legible. */}
      {H.credentials && H.credentials.items && H.credentials.items.length > 0 && (
        <section className="section credentials-strip">
          <div className="container">
            <Reveal><span className="eyebrow credentials-strip__eyebrow">{H.credentials.eyebrow}</span></Reveal>
            <Reveal delay={120}>
              <div className="credentials-strip__list">
                {H.credentials.items.map((c, i) => (
                  <span key={i} className="credentials-strip__item" title={c.label}>
                    <img src={c.logo} alt={c.label} loading="lazy" />
                  </span>
                ))}
              </div>
            </Reveal>
          </div>
        </section>
      )}

      {/* Insights — first 3 entries of the unified news source */}
      <section className="section section--alt">
        <div className="container">
          <Reveal className="section__head">
            <Eyebrow>{H.insightsEyebrow}</Eyebrow>
            <h2 className="section__title">{H.insightsTitle}</h2>
          </Reveal>
          <div className="grid-3">
            {t.news.slice(0, 3).map((a,i) => (
              <Reveal key={i} delay={i * 100}>
                <ArticleCard a={a} onClick={() => setRoute({route:"article", articleId:a.id})} />
              </Reveal>
            ))}
          </div>
          <Reveal delay={400} className="grid-3__cta">
            <ArrowCTA onClick={() => setRoute("newsletter")}>{H.viewAllNews}</ArrowCTA>
          </Reveal>
        </div>
      </section>

      <SocialStrip />
    </>
  );
};

const PageQuienes = () => {
  const { t } = useLang();
  const Q = t.quienes;
  return (
    <>
      {/* Banner hero — same single-photo treatment as Personas (ken-burns
          short banner, title bottom-left, scroll-down hint pointing at
          the editorial section below). Uses the existing Santander
          night shot. Headline tightened from the long parallax slug to
          the page label (matches Proyectos / Personas convention of
          one short title in the banner; long copy lives in the section
          immediately below). */}
      <PersonasHero
        heroImg="assets/hero_santander_noche.jpg"
        title={Q.eyebrow}
        scrollHint={Q.scrollHint}
        objectPosition="center center" />

      <section className="section section--light">
        <div className="container editorial">
          <SectionLabel>{Q.sectionLabel}</SectionLabel>
          <div className="editorial__body">
            {/* The long tagline previously lived in the parallax banner
                ("Más de seis décadas..."). With the banner moved to the
                Personas-style format, this headline takes its proper
                place as the opening h2 of the editorial section so the
                copy still leads with the firm's positioning line. */}
            <Reveal>
              <h2 className="section__title" style={{marginBottom: "var(--sp-5)"}}>{Q.heroTitle}</h2>
            </Reveal>
            {Q.body.map((html, i) => (
              <Reveal key={i} delay={(i + 1) * 80}>
                <p className="body-lg" dangerouslySetInnerHTML={{__html: html}} />
              </Reveal>
            ))}
          </div>
        </div>
      </section>

      {/* Historia — timeline 1961 → hoy. Cada hito puede llevar imagen de
          archivo (assets/history/) con caption en mono. */}
      {Q.history && (
        <section className="section section--alt">
          <div className="container">
            <Reveal className="section__head">
              <Eyebrow>{Q.historyEyebrow}</Eyebrow>
              <h2 className="section__title">{Q.historyTitle}</h2>
            </Reveal>
            <div className="timeline">
              {Q.history.map((h, i) => (
                <Reveal key={i} delay={i * 60} className="tl-item">
                  <div className="tl-item__year">{h.year}</div>
                  <div className="tl-item__body">
                    <h3 className="tl-item__t">{h.t}</h3>
                    <p>{h.p}</p>
                    {h.img && (
                      <figure className="tl-item__fig">
                        <img src={h.img} alt={h.cap || h.t} loading="lazy" />
                        {h.cap && <figcaption>{h.cap}</figcaption>}
                      </figure>
                    )}
                  </div>
                </Reveal>
              ))}
            </div>
          </div>
        </section>
      )}

      {/* El fundador — retrato de archivo + bloque editorial. */}
      {Q.founder && (
        <section className="section section--light">
          <div className="container founder">
            <Reveal className="founder__photo">
              <img src={Q.founder.img} alt={Q.founder.name} loading="lazy" />
            </Reveal>
            <Reveal delay={120} className="founder__text">
              <Eyebrow>{Q.founderEyebrow}</Eyebrow>
              <h2 className="section__title">{Q.founder.name}</h2>
              <p className="founder__role">{Q.founder.role}</p>
              {Q.founder.body.map((p, i) => <p key={i}>{p}</p>)}
            </Reveal>
          </div>
        </section>
      )}

      <section className="section section--dark">
        <div className="container">
          <Reveal className="section__head"><Eyebrow onDark>{Q.pillarsEyebrow}</Eyebrow></Reveal>
          <div className="pillars">
            {Q.pillars.map((p, i) => (
              <Reveal key={i} delay={i * 120} className="pillar">
                <div className="pillar__num">{p.num}</div>
                <h3 className="pillar__t">{p.t}</h3>
                <p>{p.p}</p>
              </Reveal>
            ))}
          </div>
        </div>
      </section>

      {/* Valores — anclados en hechos de la historia; cierran con los códigos. */}
      {Q.values && (
        <section className="section section--light">
          <div className="container">
            <Reveal className="section__head"><Eyebrow>{Q.valuesEyebrow}</Eyebrow></Reveal>
            <div className="values-grid">
              {Q.values.map((v, i) => (
                <Reveal key={i} delay={i * 80} className="value">
                  <h3 className="value__t">{v.t}</h3>
                  <p>{v.p}</p>
                </Reveal>
              ))}
            </div>
          </div>
        </section>
      )}

      <section className="section section--alt">
        <div className="container codes">
          <Reveal>
            <Eyebrow>{Q.codeEthicEyebrow}</Eyebrow>
            <ul className="code-list">
              {Q.ethicalCode.map((e,i) => <li key={i}>{e}</li>)}
            </ul>
          </Reveal>
          <Reveal delay={120}>
            <Eyebrow>{Q.codeProfEyebrow}</Eyebrow>
            <ul className="code-list">
              {Q.professionalCode.map((e,i) => <li key={i}>{e}</li>)}
            </ul>
          </Reveal>
        </div>
      </section>

      <section className="section section--light">
        <div className="container">
          <Reveal className="section__head"><Eyebrow>{Q.accredEyebrow}</Eyebrow></Reveal>
          <div className="accred-grid">
            {t.accreditations.map((a,i) => (
              <Reveal key={i} delay={i * 50} className="accred">
                <div className="accred__label">{a.label}</div>
                <div className="accred__sub">{a.sub}</div>
              </Reveal>
            ))}
          </div>
        </div>
      </section>

      <section className="section section--dark">
        <div className="container">
          <Reveal className="section__head">
            <Eyebrow onDark>{Q.servicesEyebrow}</Eyebrow>
            {Q.servicesIntro && <p className="svc-intro">{Q.servicesIntro}</p>}
          </Reveal>
          <ul className="svc-list">
            {Q.services.map((s,i) => (
              <Reveal key={i} delay={i * 30} as="li">
                <span className="svc-list__n">{String(i+1).padStart(2,"0")}</span>{s}
              </Reveal>
            ))}
          </ul>
        </div>
      </section>
    </>
  );
};

/* Per-sector hover-preview clip. Each tile mounts its <video> paused on the
   poster image; mouseenter rewinds to frame 0 and starts playback, mouseleave
   pauses + rewinds. The clips are silent (no audio track) so play() never
   blocks on autoplay policy. */
const TILE_VIDEO_BY_ID = {
  "data-centers":          "assets/videos/proj_tile_dc.mp4",
  "farma-bio":             "assets/videos/proj_tile_farma.mp4",
  "fabricacion-avanzada":  "assets/videos/proj_tile_fab.mp4",
  "edificios-singulares":  "assets/videos/proj_tile_edif.mp4",
  "hospitales":            "assets/videos/proj_tile_hos.mp4",
  "sostenibilidad":        "assets/videos/proj_tile_sos.mp4",
};
/* Per-tile poster = first frame of the tile's preview clip (extracted via
   ffmpeg). Using the actual frame instead of the sector's legacy hero image
   means the static poster and the playing video share the same scene/style,
   so the hover transition feels continuous instead of swapping to a totally
   different photograph. Falls back to sector.img if no poster exists. */
const TILE_POSTER_BY_ID = {
  "data-centers":          "assets/videos/proj_tile_dc.jpg",
  "farma-bio":             "assets/videos/proj_tile_farma.jpg",
  "fabricacion-avanzada":  "assets/videos/proj_tile_fab.jpg",
  "edificios-singulares":  "assets/videos/proj_tile_edif.jpg",
  "hospitales":            "assets/videos/proj_tile_hos.jpg",
  "sostenibilidad":        "assets/videos/proj_tile_sos.jpg",
};

const SectorTile = ({ sector, setRoute, isWide }) => {
  const videoRef = React.useRef(null);
  const src    = TILE_VIDEO_BY_ID[sector.id];
  const poster = TILE_POSTER_BY_ID[sector.id] || sector.img;
  /* `<video poster>` doesn't render reliably across browsers when preload=
     "metadata" — Chrome and Safari often paint a black box until the first
     frame is decoded, which means the user briefly sees nothing on tiles
     they haven't hovered yet. We render an <img> underneath as the always-
     visible base and fade the <video> in on top only while it's playing.
     On mouseleave the video fades back out, returning the poster img — so
     the tile is never empty regardless of preload state. */
  const [playing, setPlaying] = React.useState(false);
  const playFromStart = () => {
    const v = videoRef.current;
    if (!v) return;
    v.currentTime = 0;
    setPlaying(true);
    const p = v.play();
    if (p && typeof p.catch === "function") p.catch(() => setPlaying(false));
  };
  const rewindPause = () => {
    const v = videoRef.current;
    if (!v) return;
    v.pause();
    try { v.currentTime = 0; } catch (_) { /* iOS quirk: ignore */ }
    setPlaying(false);
  };
  return (
    <a className={"sector-stage__tile"
                  + (isWide ? " sector-stage__tile--wide" : "")
                  + (playing ? " is-playing" : "")}
       onClick={() => setRoute({route:"sector", sectorId: sector.id})}
       onMouseEnter={playFromStart}
       onMouseLeave={rewindPause}
       onFocus={playFromStart}
       onBlur={rewindPause}
       tabIndex={0}
       aria-label={sector.label}>
      <img className="sector-stage__poster" src={poster} alt="" loading="lazy" />
      {src && (
        <video ref={videoRef}
               className="sector-stage__video"
               src={src}
               muted playsInline loop preload="metadata"
               aria-hidden="true" />
      )}
      <div className="sector-stage__overlay" />
      <div className="sector-stage__label">{sector.label}</div>
    </a>
  );
};

/* ProyectosHero — 100vh image carousel at the top of /proyectos.
 *
 * Four photos cycle every 6s (auto), the page title "Projects" sits as a
 * constant minimalist overlay (no per-slide labels — the verticals are
 * named further down by the sector tiles, so the hero stays purely
 * atmospheric). A circular dot strip shows position 1/4, 2/4, etc., with a
 * thin progress ring driven by a CSS keyframe over the slide duration
 * (videos drive their own duration in the home carousel — here it's a
 * fixed 6s per slide). A small "↓" + label at the bottom-centre hints
 * that the sector grid lives below, so users know to scroll. */
const ProyectosHero = () => {
  const { t } = useLang();
  const slides = (t.proyectos && t.proyectos.heroSlides) || [];
  const [i, setI] = React.useState(0);
  // Autoplay — self-rescheduling 6s setTimeout. Each slide change cancels
  // any pending advance and schedules a fresh 6s timer, so manual dot
  // clicks restart the cycle from now.
  React.useEffect(() => {
    if (slides.length <= 1) return;
    const tm = setTimeout(() => setI(x => (x + 1) % slides.length), 6000);
    return () => clearTimeout(tm);
  }, [i, slides.length]);
  // Scroll hint click → smooth-scroll to the sector grid below.
  const scrollToSectors = () => {
    const el = document.querySelector(".sector-stage");
    if (el && typeof el.scrollIntoView === "function") {
      el.scrollIntoView({behavior: "smooth", block: "start"});
    }
  };
  return (
    <section className="proj-hero" aria-label={t.proyectos.title}>
      {slides.map((s, idx) => (
        <div key={idx} className={"proj-hero__slide " + (idx === i ? "is-active" : "")}>
          <img className="proj-hero__img"
               src={s.img}
               alt=""
               loading={idx === 0 ? "eager" : "lazy"}
               style={s.objectPosition ? {objectPosition: s.objectPosition} : null} />
        </div>
      ))}
      <div className="proj-hero__overlay" />
      <div className="container proj-hero__inner">
        <h1 className="proj-hero__title">{t.proyectos.title}</h1>
      </div>
      {/* Circular dot strip — same vocabulary as the Data Centers stage hero.
          The active dot's ring SVG only renders for the active dot; on
          slide change the SVG unmounts/remounts and the CSS keyframe
          restarts from 0, so the ring perfectly tracks the autoplay timer. */}
      {slides.length > 1 && (
        <div className="proj-hero__dots" role="tablist" aria-label="Slides">
          {slides.map((_, idx) => {
            const isActive = idx === i;
            return (
              <button key={idx}
                      type="button"
                      role="tab"
                      aria-selected={isActive}
                      className={"proj-hero__dot " + (isActive ? "is-on" : "")}
                      onClick={() => setI(idx)}
                      aria-label={"Slide " + (idx + 1)}>
                <span className="proj-hero__dot-core" />
              </button>
            );
          })}
        </div>
      )}
      {/* Scroll-down hint — sits below the dots, points at the sector tiles
          which live in the section directly underneath. Clickable: smooth-
          scrolls to .sector-stage so users can jump without scrolling
          manually. */}
      <button type="button" className="proj-hero__scroll" onClick={scrollToSectors} aria-label={t.proyectos.scrollHint}>
        <span className="proj-hero__scroll-label">{t.proyectos.scrollHint}</span>
        <span className="proj-hero__scroll-arrow" aria-hidden="true">↓</span>
      </button>
      {/* Architect credit for the active slide — bottom-right at the
          same vertical level as the scroll hint. Renders only when the
          slide defines `architect` (slide 4 = Sede Santander · Ayala). */}
      {slides[i] && slides[i].architect && (
        <span className="proj-hero__credit" aria-label={slides[i].architect}>
          {slides[i].architect}
        </span>
      )}
    </section>
  );
};

const PageProyectos = ({ setRoute }) => {
  const { t } = useLang();
  return (
    <>
      {/* 1. Hero carousel — 4 photo slides + minimalist "Projects" overlay +
            circular dot ring + scroll-down hint pointing to the sectors. */}
      <ProyectosHero />

      {/* 2. Sector stage — F+P-style editorial grid on a deep navy band. The
            title now lives in the hero above, so this section opens directly
            with the tile grid (no duplicate "Projects" heading). */}
      <section className="sector-stage">
        <div className="container">
          {/* Chevron pattern in a 3-col grid — wide tiles (span 2 cols) alternate
              with narrow tiles (1 col) to create an asymmetric F+P-style
              rhythm. Idx 0,3,4 are wide; 1,2,5 are narrow. Each row sums to
              3 cols. Wide and narrow tiles share the same row height (their
              aspect-ratios are tuned so the height stays constant). */}
          <div className="sector-stage__grid">
            {(t.sectors || []).map((s, i) => {
              const isWide = i === 0 || i === 3 || i === 4;
              return (
                <SectorTile key={s.id}
                            sector={s}
                            setRoute={setRoute}
                            isWide={isWide}
                            delay={(i % 3) * 80} />
              );
            })}
          </div>
        </div>
      </section>

      {/* Client trust strip closes the page */}
      <ClientStrip />
    </>
  );
};

/* PageSector — sector landing page. Reached via setRoute({route:"sector", sectorId}).
 *
 * Layout (adopted from the Aguilera Ingenieros Design System bundle, Claude
 * Design handoff):
 *   1. SectorHero — split panel. Left: eyebrow, h1, sub, 4-stat strip.
 *      Right: framed media panel with engineering-grid background and corner
 *      brackets. Holds the isometric line-drawing (cpd / farma / edif) or a
 *      photo (fab / hos / sos) — the panel is reserved as a future video slot.
 *   2. "Qué diseñamos" — 4 numbered discipline cards on a 2-col bordered grid.
 *   3. "Capacidades técnicas" — dark section with a numbered spec list.
 *   4. "Certificaciones y estándares" — 4-cell bordered grid.
 *   5. CTA accent band → contacto.
 */
/* SectorHeroPanel — the framed media panel on the right of every sector hero.
 *
 * Three flavours, picked in priority order:
 *   1. video   (sector.video present) — full-bleed clip inside the frame. Two
 *              shapes are supported:
 *                video: { src, poster?, cap? }                  → single loop
 *                video: { clips:[{src,cap?}, …], poster? }      → auto-cycling reel
 *              In the reel shape, each clip plays end-to-end and the panel
 *              advances on the `ended` event — so the rotation matches each
 *              clip's natural duration without us hardcoding a timer.
 *   2. drawing (sector.building present) — isometric line-drawing on the
 *              engineered grid background; the panel reads as a technical
 *              elevation.
 *   3. photo   (fallback) — sector.img dropped in full-bleed with a dimming
 *              overlay so the caption strip stays legible.
 *
 * The chrome (frame, corners, caption strip, video CTA) is identical across
 * all three flavours so the visual language of every sector page stays in sync. */
const SectorHeroPanel = ({ sector, common }) => {
  const useVideo    = !!sector.video;
  const useBuilding = !useVideo && !!sector.building;
  const usePhoto    = !useVideo && !useBuilding;

  const v = sector.video;
  const b = sector.building;
  // Normalise both video shapes into a uniform `clips[]` array of length ≥1.
  // If `sector.video.src` is present we treat it as a single-clip reel; the
  // playlist logic below loops automatically once it advances back to index 0.
  const clips = useVideo
    ? (Array.isArray(v.clips) && v.clips.length > 0
        ? v.clips
        : [{ src: v.src, cap: v.cap }])
    : [];
  const isReel = useVideo && clips.length > 1;
  const [clipIdx, setClipIdx] = React.useState(0);
  const videoRef = React.useRef(null);

  // On clip change, kick the video to (re)load the new src and play. The
  // browser won't autoplay across src swaps without this on Safari/iOS.
  React.useEffect(() => {
    if (!useVideo) return;
    const el = videoRef.current;
    if (!el) return;
    // The <video> src is bound declaratively below so React will swap it for
    // us; we just need to nudge the element to load and play afterwards.
    el.load();
    const p = el.play();
    if (p && typeof p.catch === "function") p.catch(() => {});
  }, [clipIdx, useVideo]);

  const onEnded = () => {
    if (!isReel) return;
    setClipIdx((i) => (i + 1) % clips.length);
  };

  const currentClip = clips[clipIdx] || {};
  const photoStyle = usePhoto ? { backgroundImage: `url(${sector.img})` } : null;
  const variant    = useVideo ? "shero__media--video"
                    : useBuilding ? "shero__media--drawing"
                    : "shero__media--photo";
  const cap = useVideo ? (currentClip.cap || sector.label.toUpperCase())
            : useBuilding ? b.cap
            : sector.label.toUpperCase();

  return (
    <Reveal delay={160} className="shero__right">
      <div className={"shero__media " + variant}
           data-video-slot={sector.cat}
           style={photoStyle}>
        {useVideo && (
          /* muted + playsInline + autoPlay is the standard contract for a
             silent loop browsers won't block. `loop` only applies in the
             single-clip case; for a reel we let `ended` fire so we can advance
             the index. `poster` paints instantly so the panel isn't blank
             while the next clip buffers. */
          <video ref={videoRef}
                 className="shero__video"
                 src={currentClip.src}
                 autoPlay
                 muted
                 playsInline
                 preload="auto"
                 loop={!isReel}
                 onEnded={onEnded}
                 aria-hidden="true" />
        )}
        {useBuilding && <div className="shero__grid-bg" />}
        {useBuilding && (
          <img className="shero__building"
               src={b.src} alt=""
               style={{aspectRatio: String(b.ratio)}} />
        )}
        {/* Tick row — one notch per clip in a reel; the active clip's notch
            is bright. Sits inside the panel near the bottom so it reads as
            "you are on shot N of M" without dominating the frame. */}
        {isReel && (
          <div className="shero__reel-ticks" aria-hidden="true">
            {clips.map((_, i) => (
              <span key={i} className={"shero__reel-tick " + (i === clipIdx ? "is-on" : "")} />
            ))}
          </div>
        )}
        <div className="shero__media-foot">
          <span className="shero__media-cap">{cap}</span>
          <span className="shero__media-video">
            <span className="shero__play">
              <svg width="11" height="13" viewBox="0 0 11 13" fill="currentColor"><path d="M0 0l11 6.5L0 13z"/></svg>
            </span>
            {common.videoLabel}
          </span>
        </div>
        <div className="shero__corner shero__corner--tl" />
        <div className="shero__corner shero__corner--tr" />
        <div className="shero__corner shero__corner--bl" />
        <div className="shero__corner shero__corner--br" />
      </div>
    </Reveal>
  );
};

/* SectorStageHero — full-bleed 100vh video hero in the Foster + Partners
 * cadence. Used when sector.heroVariant === "stage" (Data Centers).
 *
 * Layout:
 *   - Background <video> at object-fit:cover, autoplaying through sector.video.clips
 *   - Top-to-bottom darkening gradient for legibility under the nav + over the copy
 *   - Bottom-left copy column: Eyebrow + h1 + sub
 *   - Bottom-centre dot strip: one small filled dot per clip; the active dot is
 *     surrounded by a thin SVG ring whose stroke-dashoffset fills clockwise as
 *     the clip plays. The fill is driven by requestAnimationFrame mutating the
 *     circle's style.strokeDashoffset directly — no React re-render per frame.
 *
 * Clicking a dot jumps to that clip; the rAF loop resets and the new clip's
 * ring starts from empty. The `ended` event advances index automatically. */
const SectorStageHero = ({ sector, common, setRoute }) => {
  const v = sector.video || {};
  const clips = Array.isArray(v.clips) && v.clips.length > 0
    ? v.clips
    : (v.src ? [{ src: v.src, cap: v.cap }] : []);
  const [clipIdx, setClipIdx] = React.useState(0);
  const videoRef = React.useRef(null);
  const ringRef  = React.useRef(null);
  const stageRef = React.useRef(null);
  /* scrollPastHero — clicking the heroHint smooth-scrolls past the
     full-bleed stage into the sector's first content section (the
     section directly after .shero--stage in the DOM). Mirrors the
     "Ver sectores" pattern on the Proyectos hero. */
  const scrollPastHero = () => {
    const next = stageRef.current && stageRef.current.nextElementSibling;
    if (next && typeof next.scrollIntoView === "function") {
      next.scrollIntoView({behavior: "smooth", block: "start"});
    }
  };
  const RADIUS = 9;
  const RING_C = 2 * Math.PI * RADIUS;   // ~56.55 — full-circle stroke length

  // Load + play the current clip when clipIdx changes, and drive the progress
  // ring via rAF. Mutating the SVG circle's style directly avoids 60 React
  // re-renders per second; the only state change is on `ended` (clip swap).
  React.useEffect(() => {
    const vid = videoRef.current;
    if (!vid) return;
    vid.load();
    const p = vid.play();
    if (p && typeof p.catch === "function") p.catch(() => {});
    // Reset ring to empty at the start of the new clip.
    if (ringRef.current) ringRef.current.style.strokeDashoffset = String(RING_C);
    let raf;
    const tick = () => {
      const v2 = videoRef.current;
      const r  = ringRef.current;
      if (v2 && r && v2.duration > 0) {
        const pct = Math.min(1, Math.max(0, v2.currentTime / v2.duration));
        r.style.strokeDashoffset = String(RING_C * (1 - pct));
      }
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [clipIdx, RING_C]);

  const onEnded = () => {
    if (clips.length <= 1) return;
    setClipIdx((i) => (i + 1) % clips.length);
  };

  const currentClip = clips[clipIdx] || {};
  return (
    <section ref={stageRef} className="shero shero--stage" aria-label={sector.label}>
      {/* Per-clip objectPosition override — same pattern as the
          ProyectosHero photo slides. When a clip's source isn't
          centred for the wide stage container (e.g. a tall portrait
          tower shot that loses its crown to a default center crop),
          the data record sets objectPosition: "center top" and the
          inline style flips object-fit's anchor for that clip. */}
      <video ref={videoRef}
             className="shero__bg-video"
             src={currentClip.src}
             autoPlay
             muted
             playsInline
             preload="auto"
             loop={clips.length <= 1}
             onEnded={onEnded}
             style={currentClip.objectPosition ? {objectPosition: currentClip.objectPosition} : undefined}
             aria-hidden="true" />
      {/* Top-to-bottom darkening pass: heavier at top (so the nav + corner brackets
          read) and at the bottom (so the copy stays legible regardless of footage). */}
      <div className="shero__bg-overlay" />

      <div className="container shero__stage-inner">
        <div className="shero__stage-content">
          {/* Stage hero copy stays tight on purpose — just an eyebrow and a
              short headline. Sector descriptions live below the hero, not on
              top of the footage. The `sub` field on the sector is preserved
              for the framed hero variants (other sectors) but not rendered
              here, so we can shorten without touching the schema. */}
          <Reveal><Eyebrow onDark>{sector.eyebrow || sector.label}</Eyebrow></Reveal>
          <Reveal delay={90}><h1 className="shero__stage-h1">{sector.h1 || sector.label}</h1></Reveal>
        </div>
      </div>

      {/* Dot strip — one per clip in the reel. The active dot wears the
          progress-ring SVG; the others are static filled dots. */}
      {clips.length > 1 && (
        <div className="shero__stage-dots" role="tablist" aria-label="Reel clips">
          {clips.map((_, i) => {
            const isActive = i === clipIdx;
            return (
              <button key={i}
                      type="button"
                      role="tab"
                      aria-selected={isActive}
                      className={"shero__stage-dot " + (isActive ? "is-on" : "")}
                      onClick={() => setClipIdx(i)}
                      aria-label={"Clip " + (i + 1) + " de " + clips.length}>
                <span className="shero__stage-dot-core" />
                {isActive && (
                  <svg className="shero__stage-dot-ring" width="28" height="28" viewBox="0 0 28 28" aria-hidden="true">
                    <circle ref={ringRef}
                            cx="14" cy="14" r={RADIUS}
                            fill="none"
                            stroke="#FFFFFF"
                            strokeWidth="1.5"
                            strokeLinecap="round"
                            strokeDasharray={RING_C}
                            strokeDashoffset={RING_C}
                            style={{transform: "rotate(-90deg)", transformOrigin: "50% 50%"}} />
                  </svg>
                )}
              </button>
            );
          })}
        </div>
      )}

      {/* Tiny "Discover our …" hint under the dots — only renders when
          the sector defines heroHint. Mirrors the Proyectos "Ver
          sectores" pattern but smaller and quieter; click smooth-
          scrolls past the stage into the content below. */}
      {sector.heroHint && (
        <button type="button"
                className="shero__stage-hint"
                onClick={scrollPastHero}
                aria-label={sector.heroHint}>
          <span className="shero__stage-hint-label">{sector.heroHint}</span>
          <span className="shero__stage-hint-arrow" aria-hidden="true">↓</span>
        </button>
      )}

      {/* Architect credit — bottom-right corner at the same vertical
          level as the heroHint. Only renders when the active clip
          carries an `architect` field. Same font/size/colour as the
          hint label so the two read as a matched pair across the
          bottom of the stage. */}
      {currentClip.architect && (
        <span className="shero__stage-credit" aria-label={currentClip.architect}>
          {currentClip.architect}
        </span>
      )}
    </section>
  );
};

/* SectorDeckHero — three-card deck layout for sectors with
 * heroVariant: "deck". Three square clips are visible at once: the active
 * one sits centred (full size + full brightness), flanked left and right by
 * the previous and next clips (scaled down + dimmed). Clicking a dot or
 * letting autoplay advance shifts the whole row — visually the centred
 * clip slides away and the next one rotates into the forefront.
 *
 * Implementation: instead of mounting N videos at a time, we always render
 * exactly 3 video elements (one per slot). Each slot's clip index is
 * computed as (activeIdx + offset + N) % N where offset ∈ {-1, 0, +1}.
 * Updating activeIdx re-keys each <video> with its new src; the React
 * effect below kicks load() + play() on the active one so the centre
 * clip is always actually playing (sides are paused on frame 0). */
const SectorDeckHero = ({ sector, common, setRoute }) => {
  const v = sector.video || {};
  const clips = Array.isArray(v.clips) && v.clips.length > 0
    ? v.clips
    : (v.src ? [{ src: v.src, cap: v.cap }] : []);
  const [activeIdx, setActiveIdx] = React.useState(0);
  /* Every clip mounts its own <video>; we keep refs so we can play/pause
     each one individually. When activeIdx changes, the CSS variable
     `--offset` on each card transitions — the boxes themselves slide to
     their new positions instead of the videos swapping in fixed slots. */
  const videoRefs = React.useRef([]);

  React.useEffect(() => {
    videoRefs.current.forEach((vid, idx) => {
      if (!vid) return;
      if (idx === activeIdx) {
        vid.currentTime = 0;
        const p = vid.play();
        if (p && typeof p.catch === "function") p.catch(() => {});
      } else {
        vid.pause();
      }
    });
  }, [activeIdx]);

  const advance = () => {
    if (clips.length <= 1) return;
    setActiveIdx(i => (i + 1) % clips.length);
  };

  /* wrapOffset turns the raw index delta (i - activeIdx) into the
     shortest-path delta around a ring of N cards. Without this, the
     card at the far end of the list sits at offset N-1 forever; with
     it, that card crosses over to offset -1 once activeIdx gets close,
     so the deck loops both ways with no dead edges. */
  const N = clips.length;
  const wrapOffset = (raw) => {
    if (N === 0) return 0;
    let o = raw;
    if (o >  N / 2) o -= N;
    if (o <= -N / 2) o += N;
    return o;
  };

  /* With N=3 every step forces one card to teleport from offset -1 to
     +1 (or back) — otherwise CSS would slide it straight through the
     active card. We detect that case per render and tag the warping
     card with `is-warp`; the CSS rule kills its transition for that
     one frame, so it snaps invisibly to the far side instead of
     crossing the centre. The ref tracks previous-frame offsets so
     subsequent renders can compare and only flag the cards that
     actually jumped > 1 slot. */
  const offsets = clips.map((_, i) => wrapOffset(i - activeIdx));
  const prevOffsetsRef = React.useRef([]);
  const warpFlags = offsets.map((o, i) => {
    const p = prevOffsetsRef.current[i];
    return p !== undefined && Math.abs(o - p) > 1;
  });
  React.useEffect(() => { prevOffsetsRef.current = offsets; });

  return (
    <section className="shero shero--deck" aria-label={sector.label}>
      <div className="shero-deck__inner">
        {/* Centred deck title — prefers the sector's heroTitle (longer
            invitational copy like "Descubre nuestros hospitales") and
            falls back to the plain label. Both are localized via the
            sector record itself, so ES/EN switch happens for free. */}
        <h1 className="shero-deck__title">{sector.heroTitle || sector.label}</h1>
        {/* Stage holds all cards absolutely positioned; their --offset
            CSS variable drives the transform. React just updates the
            offsets on activeIdx change — CSS transitions handle the slide. */}
        <div className="shero-deck__stage">
          {clips.map((clip, i) => {
            const offset = offsets[i];
            const role = offset === 0 ? "is-active"
                       : offset === -1 ? "is-prev"
                       : offset === 1  ? "is-next"
                       : "is-far";
            const warpClass = warpFlags[i] ? " is-warp" : "";
            return (
              <button key={i}
                      type="button"
                      className={"shero-deck__card " + role + warpClass}
                      style={{"--offset": offset}}
                      onClick={offset === 0 ? undefined : () => setActiveIdx(i)}
                      aria-label={"Clip " + (i + 1)}
                      aria-current={offset === 0 ? "true" : undefined}
                      tabIndex={offset === 0 ? -1 : 0}>
                {/* No `loop` — onEnded must fire to advance to the next
                    clip. The effect on activeIdx pauses the outgoing
                    video and resets the incoming one to t=0. */}
                <video ref={el => { videoRefs.current[i] = el; }}
                       src={clip.src}
                       muted playsInline preload="metadata"
                       onEnded={offset === 0 ? advance : undefined}
                       aria-hidden="true" />
              </button>
            );
          })}
        </div>

        {clips.length > 1 && (
          <DeckDots clips={clips}
                    activeIdx={activeIdx}
                    onPick={setActiveIdx}
                    activeVideoRef={() => videoRefs.current[activeIdx]} />
        )}
      </div>
    </section>
  );
};

/* DeckDots — one circular dot per clip. The active dot wears a progress
 * ring whose stroke-dashoffset is mutated each frame from the active
 * video's currentTime/duration ratio (rAF). Re-mounts the ring SVG on
 * activeIdx change so the fill restarts cleanly. */
const DeckDots = ({ clips, activeIdx, onPick, activeVideoRef }) => {
  const ringRef = React.useRef(null);
  const RING_R = 9;
  const RING_C = 2 * Math.PI * RING_R;
  React.useEffect(() => {
    if (ringRef.current) ringRef.current.style.strokeDashoffset = String(RING_C);
    let raf;
    const tick = () => {
      const v = activeVideoRef && activeVideoRef();
      const r = ringRef.current;
      if (v && r && v.duration > 0) {
        const pct = Math.min(1, Math.max(0, v.currentTime / v.duration));
        r.style.strokeDashoffset = String(RING_C * (1 - pct));
      }
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [activeIdx, RING_C, activeVideoRef]);
  return (
    <div className="shero-deck__dots" role="tablist">
      {clips.map((_, i) => {
        const isActive = i === activeIdx;
        return (
          <button key={i}
                  type="button"
                  role="tab"
                  aria-selected={isActive}
                  className={"shero-deck__dot " + (isActive ? "is-on" : "")}
                  onClick={() => onPick(i)}
                  aria-label={"Clip " + (i + 1)}>
            <span className="shero-deck__dot-core" />
            {isActive && (
              <svg className="shero-deck__dot-ring" width="28" height="28" viewBox="0 0 28 28" aria-hidden="true">
                <circle ref={ringRef}
                        cx="14" cy="14" r={RING_R}
                        fill="none" stroke="#FFFFFF" strokeWidth="1.5" strokeLinecap="round"
                        strokeDasharray={RING_C} strokeDashoffset={RING_C}
                        style={{transform:"rotate(-90deg)",transformOrigin:"50% 50%"}} />
              </svg>
            )}
          </button>
        );
      })}
    </div>
  );
};

const PageSector = ({ sectorId, setRoute }) => {
  const { t } = useLang();
  const sector = (t.sectors || []).find(s => s.id === sectorId);
  const common = t.sectorCommon || {};
  if (!sector) {
    return (
      <section className="section section--light">
        <div className="container">
          <p>—</p>
          <a className="back-link" onClick={() => setRoute("proyectos")}>← {t.proyectos.backToProyectos}</a>
        </div>
      </section>
    );
  }
  return (
    <>
      {/* ── 1. Hero ───────────────────────────────────────────────────
          Default: split panel (copy left, framed media right).
          heroVariant === "stage": full-bleed 100vh video stage with
          progress-ring dots (Foster + Partners cadence).
          heroVariant === "deck": 3-card deck rotation (centre clip in
          forefront, flanks scaled down + dimmed — used by Hospitales). */}
      {sector.heroVariant === "stage" ? (
        <SectorStageHero sector={sector} common={common} setRoute={setRoute} />
      ) : sector.heroVariant === "deck" ? (
        <SectorDeckHero sector={sector} common={common} setRoute={setRoute} />
      ) : (
        <section className="shero">
          <div className="shero__inner container">
            <div className="shero__left">
              <Reveal><Eyebrow onDark>{sector.eyebrow || t.proyectos.sectorsEyebrow}</Eyebrow></Reveal>
              <Reveal delay={90}><h1 className="shero__h1">{sector.h1 || sector.label}</h1></Reveal>
              <Reveal delay={180}><p className="shero__sub">{sector.sub || sector.intro}</p></Reveal>
              {sector.stats && sector.stats.length > 0 && (
                <Reveal delay={260}>
                  <div className="shero__stats">
                    {sector.stats.map((s, i) => (
                      <div key={i} className="shero__stat">
                        <div className="shero__stat-v">{s.v}</div>
                        <div className="shero__stat-k">{s.k}</div>
                      </div>
                    ))}
                  </div>
                </Reveal>
              )}
            </div>
            <SectorHeroPanel sector={sector} common={common} />
          </div>
        </section>
      )}


      {/* ── 2. What we engineer — disciplines ──────────────────────── */}
      {sector.engineer && sector.engineer.length > 0 && (
        <section className="section section--light">
          <div className="container">
            <Reveal className="section__head">
              <Eyebrow>{sector.engineerTitle}</Eyebrow>
              {sector.engineerSub && <h2 className="section__title">{sector.engineerSub}</h2>}
            </Reveal>
            <div className="eng-grid">
              {sector.engineer.map((e, i) => (
                <Reveal key={i} delay={i * 80} className="eng-card">
                  <div className="eng-card__n">{String(i + 1).padStart(2, "0")}</div>
                  <h3 className="eng-card__t">{e.t}</h3>
                  <p className="eng-card__d">{e.d}</p>
                </Reveal>
              ))}
            </div>
          </div>
        </section>
      )}

      {/* ── 3. Technical capabilities — spec list on dark ──────────── */}
      {sector.caps && sector.caps.length > 0 && (
        <section className="section section--dark">
          <div className="container caps">
            <Reveal className="caps__head">
              <Eyebrow onDark>{sector.capTitle}</Eyebrow>
            </Reveal>
            <ul className="caps__list">
              {sector.caps.map((c, i) => (
                <Reveal key={i} delay={i * 60} as="li" className="caps__item">
                  <span className="caps__n">{String(i + 1).padStart(2, "0")}</span>
                  <span className="caps__t">{c}</span>
                </Reveal>
              ))}
            </ul>
          </div>
        </section>
      )}

      {/* ── 4. Certifications ──────────────────────────────────────── */}
      {sector.certs && sector.certs.length > 0 && (
        <section className="section section--light">
          <div className="container">
            <Reveal className="section__head"><Eyebrow>{sector.certTitle}</Eyebrow></Reveal>
            <div className="cert-grid">
              {sector.certs.map((c, i) => (
                <Reveal key={i} delay={i * 60} className="cert">
                  <div className="cert__label">{c.label}</div>
                  <div className="cert__sub">{c.sub}</div>
                </Reveal>
              ))}
            </div>
          </div>
        </section>
      )}

      {/* ── 5. CTA accent band ─────────────────────────────────────── */}
      <section className="section section--accent">
        <div className="container cta-band">
          <Reveal><h2 className="section__title section__title--light">{common.ctaTitle}</h2></Reveal>
          <Reveal delay={80}><p className="body-lg" style={{color:"rgba(255,255,255,.88)"}}>{common.ctaSub}</p></Reveal>
          {/* CTA goes straight to mailto: instead of the contact page —
              the subject line pre-fills with the sector label so the
              inbound email is already triaged. */}
          <Reveal delay={160}>
            <ArrowCTA outline dark
                      href={"mailto:sec@aguilera-ingenieros.com?subject=" + encodeURIComponent("Proyecto — " + sector.label)}>
              {common.ctaBtn}
            </ArrowCTA>
          </Reveal>
        </div>
      </section>
    </>
  );
};

/* PersonasHero — single-photo banner in the same vocabulary as ProyectosHero,
   but without the carousel dots (one image, the page title at bottom-left,
   and a scroll-down hint pointing at the first section below). Reuses the
   .proj-hero CSS so layout/typography/animation are exactly aligned.
   Generic by design: the scroll-down arrow finds the section's next
   sibling and scrolls there, so the hero works on any page that drops
   it in (Personas, Quiénes, etc.) without a hard-coded target selector.
   `objectPosition` lets callers nudge the framing (e.g. "center top" for
   portraits where the head should anchor at the top of the crop).
   `bottom` flips the title from bottom-left to top-left when set to
   "false" — left as default since both current consumers want it low. */
const PersonasHero = ({ heroImg, title, scrollHint, objectPosition }) => {
  const heroRef = React.useRef(null);
  const scrollDown = () => {
    const next = heroRef.current && heroRef.current.nextElementSibling;
    if (next && typeof next.scrollIntoView === "function") {
      next.scrollIntoView({behavior: "smooth", block: "start"});
    }
  };
  return (
    <section ref={heroRef} className="proj-hero proj-hero--short" aria-label={typeof title === "string" ? title : undefined}>
      <div className="proj-hero__slide is-active">
        <img className="proj-hero__img"
             src={heroImg}
             alt=""
             loading="eager"
             style={{objectPosition: objectPosition || "center top"}} />
      </div>
      <div className="proj-hero__overlay" />
      <div className="container proj-hero__inner">
        <h1 className="proj-hero__title">{title}</h1>
      </div>
      {scrollHint && (
        <button type="button" className="proj-hero__scroll" onClick={scrollDown} aria-label={scrollHint}>
          <span className="proj-hero__scroll-label">{scrollHint}</span>
          <span className="proj-hero__scroll-arrow" aria-hidden="true">↓</span>
        </button>
      )}
    </section>
  );
};

const PagePersonas = () => {
  const { t } = useLang();
  const P = t.personas;
  return (
    <>
      <PersonasHero heroImg={P.heroImg} title={P.title} scrollHint={P.scrollHint} />

      {/* Team grid on the same deep navy as the Proyectos sector stage —
          continuous editorial canvas, no parallax interruption. */}
      <section className="personas-team">
        <div className="container">
          <Reveal className="section__head"><Eyebrow onDark>{P.teamEyebrow}</Eyebrow></Reveal>
          <div className="team-grid">
            {t.team.map((person,i) => (
              <Reveal key={i} delay={i * 80}>
                <TeamCard t={person} />
              </Reveal>
            ))}
          </div>
        </div>
      </section>

      {/* Quote band — stays on dark, hairline divider above to mark the shift
          from the team grid into the editorial pull-quote. */}
      <section className="personas-quote">
        <div className="container" style={{maxWidth:880,textAlign:"left"}}>
          <Reveal><span className="pullquote pullquote--on-dark">{P.quote}</span></Reveal>
        </div>
      </section>
    </>
  );
};

const PageTalento = ({ setRoute }) => {
  const { t, lang } = useLang();
  const T = t.talento;
  /* Re-key the RetoMount on lang change so the game re-initialises in
     the new locale. CRITICAL: write to localStorage SYNCHRONOUSLY
     during render, BEFORE returning JSX — otherwise the new
     RetoLangProvider mounts and reads the STALE localStorage value
     before the useLayoutEffect inside RetoMount gets to update it.
     This is a controlled side-effect (idempotent on same input) so
     it's safe to run during render. */
  if (typeof window !== "undefined") {
    try { window.localStorage.setItem("reto_lang", lang); } catch (e) {}
  }
  const retoKey = "reto-" + lang;
  return (
    <>
      {/* Banner hero — same single-photo treatment as Personas / Quiénes
          / Newsletter. Title is the page LABEL (T.eyebrow → "Talento"
          / "Careers") so it fits in the bottom-left band without
          overlapping the centred scroll hint. The longer "Únete a
          Aguilera Ingenieros." headline + the T.sub tagline both move
          into the manifesto section below, where the dark band gives
          them their own stage. */}
      <PersonasHero
        heroImg={T.heroImg}
        title={T.eyebrow}
        scrollHint={T.scrollHint} />

      {/* Journey path — scroll-driven minimalist animation that links
          the banner to the game. 6 stops: "Your career at Aguilera"
          → 4 project milestones → "Begins with a challenge". */}
      {T.journey && window.JourneyPath
        ? React.createElement(window.JourneyPath, { data: T.journey })
        : null}

      {/* El Reto — the game itself. No intro panel above; the journey
          path's closing line "Begins with a challenge" already sets it
          up narratively. */}
      <section className="talento-reto" aria-label="El Reto Aguilera">
        <div className="talento-reto__rotate" aria-hidden="true">
          <div className="talento-reto__rotate-inner">
            <svg className="talento-reto__rotate-ic" width="64" height="64" viewBox="0 0 64 64" fill="none" stroke="currentColor" strokeWidth="1.6" aria-hidden="true">
              <rect x="22" y="6" width="20" height="34" rx="3" />
              <rect x="6" y="24" width="34" height="20" rx="3" />
              <path d="M44 14a12 12 0 0 1 8 11" strokeLinecap="round" />
              <path d="M50 22l2 3 3-2" strokeLinecap="round" strokeLinejoin="round" />
            </svg>
            <div className="talento-reto__rotate-title">Gira el dispositivo</div>
            <div className="talento-reto__rotate-body">El reto necesita pantalla horizontal para mostrar el tablero completo.</div>
          </div>
        </div>
        {window.RetoMount
          ? React.createElement(window.RetoMount, { key: retoKey, lang })
          : null}
      </section>

      {/* Open positions — opens with a short lead paragraph that
          adapts to whether there are positions to browse. The
          previous .talento-find hero ("Solve real challenges…") was
          collapsed into this section so the careers page reads as a
          single hand-off from game → roles, not two banner-style
          sections in a row. */}
      <section className="section talento-positions">
        <div className="container">
          {/* "Open positions" rendered as a full section title (white,
              bold, display font) instead of a small eyebrow — this is
              the headline of the careers landing, not a sub-label. */}
          <Reveal className="section__head">
            <h2 className="section__title section__title--light talento-positions__title">{T.positionsEyebrow}</h2>
          </Reveal>
          {(() => {
            /* Only positions with `open: true` show on the public site.
               Legacy entries without the field are treated as open for
               back-compat. Closed positions stay in the data for
               historical record + future re-open. */
            const openPositions = (t.positions || []).filter(p => p.open !== false);
            return openPositions.length > 0;
          })() ? (
            <>
              <Reveal delay={80}>
                <p className="body-lg talento-positions__lead">{T.positionsLead}</p>
              </Reveal>
              <Accordion items={(t.positions || []).filter(p => p.open !== false)} applyLabel={T.applyCta} />
            </>
          ) : (
            <Reveal delay={80}>
              <p className="body-lg talento-positions__lead talento-positions__lead--empty">{T.positionsLeadEmpty}</p>
            </Reveal>
          )}
        </div>
      </section>

      {/* "Got questions?" CTA removed — redundant with the
          existing apply-via-email flow in the WinPanel and the
          contact link in the footer; positions accordion now hands
          off directly into the "Vida en Aguilera" subpage. */}

      {/* Vida en Aguilera CTA — links to the new subpage with the rest
          of the legacy careers narrative (manifesto, pillars, mentor,
          beyond). */}
      {T.vidaCta && (
        <section className="section talento-vidacta">
          <div className="container talento-vidacta__inner">
            <Reveal><Eyebrow onDark>{T.vidaCta.eyebrow}</Eyebrow></Reveal>
            <Reveal delay={80}>
              <h2 className="section__title section__title--light">{T.vidaCta.title}</h2>
            </Reveal>
            <Reveal delay={140}>
              <p className="body-lg talento-vidacta__sub">{T.vidaCta.sub}</p>
            </Reveal>
            <Reveal delay={200}>
              <ArrowCTA outline dark
                        onClick={() => { setRoute("vida"); window.scrollTo({top: 0, behavior: "smooth"}); }}>
                {T.vidaCta.cta}
              </ArrowCTA>
            </Reveal>
          </div>
        </section>
      )}

    </>
  );
};

/* PageVida — "Vida en Aguilera" subpage. Holds the legacy careers
   narrative that used to live above the game (manifesto, pillars,
   moments, mentor, beyond). Reached via the CTA button at the bottom
   of /talento. Same banner shell as Talento for visual continuity. */
const PageVida = ({ setRoute }) => {
  /* Pull `lang` from the hook directly rather than reading `t.lang` —
     the latter only works if every locale block exposes a `lang`
     property, which is fragile. The hook is the single source of
     truth. */
  const { t, lang } = useLang();
  const T = t.talento;
  return (
    <>
      <PersonasHero
        heroImg={T.heroImg}
        /* Break the title across two lines so "Ingenieros" doesn't
           collide with the bottom-right scroll hint at this hero's
           width. PersonasHero now accepts JSX as title and falls
           back to undefined aria-label when title isn't a string. */
        title={lang === "en"
          ? <>Life at Aguilera<br/>Ingenieros</>
          : <>Vida en Aguilera<br/>Ingenieros</>}
        scrollHint={T.scrollHint} />

      {/* Manifesto — just the eyebrow + the quote. The "Join Aguilera
          Ingenieros / We're looking for engineers…" title+sub block
          was redundant with the page's overall framing and got cut,
          so the section reads as a single confident statement now. */}
      <section className="section section--dark section--manifesto">
        <div className="container manifesto">
          <Reveal><Eyebrow onDark>{T.manifestoEyebrow}</Eyebrow></Reveal>
          <Reveal delay={120}>
            <p className="manifesto__quote">{T.manifestoQuote}</p>
          </Reveal>
        </div>
      </section>

      {/* "Why join" pillars section removed — the manifesto quote
          already carries the commitment message, and the moments /
          mentor / beyond sections below cover the substance. The
          numbered pillars felt like a bullet-point recap of what was
          said elsewhere on the same page. */}

      {/* How we work — three moments, three photos */}
      {T.moments && (
        <section className="section section--alt">
          <div className="container">
            <Reveal className="section__head">
              <Eyebrow>{T.momentsEyebrow}</Eyebrow>
              {/* momentsTitle "Three moments from the day-to-day" cut —
                  the eyebrow alone is enough framing and the three
                  cards below speak for themselves. */}
            </Reveal>
            <div className="moments">
              {T.moments.map((m,i) => (
                <Reveal key={i} delay={i * 100} className="moments__item">
                  <div className="moments__ph"><img src={m.img} alt="" loading="lazy" /></div>
                  <h3 className="moments__t">{m.t}</h3>
                  <p className="moments__p">{m.p}</p>
                </Reveal>
              ))}
            </div>
          </div>
        </section>
      )}

      {/* Mentorship — editorial 2-col */}
      {T.mentorImg && (
        <section className="section section--light section--mentor">
          <div className="mentor">
            <Reveal className="mentor__ph">
              <img src={T.mentorImg} alt="" loading="lazy" />
            </Reveal>
            <div className="mentor__col">
              <Reveal><Eyebrow>{T.mentorEyebrow}</Eyebrow></Reveal>
              <Reveal delay={80}><h2 className="section__title" style={{marginTop:12}}>{T.mentorTitle}</h2></Reveal>
              <Reveal delay={160}><p className="body-lg" style={{marginTop:18,color:"var(--fg-mid)",maxWidth:540,lineHeight:1.55}}>{T.mentorBody}</p></Reveal>
            </div>
          </div>
        </section>
      )}

      {/* Beyond the project — two-photo strip + integrated
          "Ready for the challenge?" CTA (used to live in its own
          dark band below, but the whole page already closes on a
          warm note via this section's photography — the CTA reads
          better centred on the same light bg than as a separate
          navy outro). */}
      {T.beyondShots && (
        <section className="section section--alt">
          <div className="container">
            <Reveal className="section__head">
              <Eyebrow>{T.beyondEyebrow}</Eyebrow>
              <h2 className="section__title">{T.beyondTitle}</h2>
            </Reveal>
            <div className="beyond">
              {T.beyondShots.map((s,i) => (
                <Reveal key={i} delay={i * 120} className="beyond__item">
                  <div className="beyond__ph"><img src={s.img} alt="" loading="lazy" /></div>
                  <p className="beyond__cap">{s.caption}</p>
                </Reveal>
              ))}
            </div>

            {/* Centred CTA below the photo strip. */}
            <div className="vida-cta-inline">
              <Reveal>
                <h2 className="section__title vida-cta-inline__title">
                  {lang === "en" ? "Ready to take the challenge?" : "¿Listo para el reto?"}
                </h2>
              </Reveal>
              <Reveal delay={160}>
                <ArrowCTA outline
                          onClick={() => { setRoute("talento"); window.scrollTo({top: 0, behavior: "smooth"}); }}>
                  {lang === "en" ? "Back to Careers" : "Volver a Talento"}
                </ArrowCTA>
              </Reveal>
            </div>
          </div>
        </section>
      )}
    </>
  );
};

/* PageContacto — Foster+Partners-inspired single-city card. With one
   Madrid office, one phone, one email, the page collapses into a
   single rounded card: photo on top, "Madrid" + icon row (map / mail
   / phone) underneath, then the address block, phone, and email. The
   old navy panel + literal map (mapa.jpg) is gone — the photo is a
   real shot from the careers folder, conveying place without the
   ugly satellite render. */
/* ContactMap — small wrapper around the Google Maps <iframe> embed
   that adds a CSS skeleton placeholder while the iframe loads. The
   placeholder reads as "this is going to be a map" so users see
   something other than a blank white panel during the second or two
   the Google Maps JS takes to render. `loading="eager"` is critical:
   the default `lazy` defers the request until the user scrolls past
   it, which on this page is never since the box is already in view. */
const ContactMap = ({ mapEmbedUrl, mapCta }) => {
  const [loaded, setLoaded] = React.useState(false);
  return (
    <div className={"ai-contact-map " + (loaded ? "is-loaded" : "")} aria-label={mapCta}>
      <div className="ai-contact-map__placeholder" aria-hidden="true">
        <Icon.MapPin s={26} c="rgba(255,255,255,0.55)" />
        <span className="ai-contact-map__placeholder-text">{mapCta}</span>
      </div>
      <iframe className="ai-contact-map__frame"
              src={mapEmbedUrl}
              title={mapCta}
              loading="eager"
              onLoad={() => setLoaded(true)}
              referrerPolicy="no-referrer-when-downgrade"
              allowFullScreen />
    </div>
  );
};

const PageContacto = () => {
  const { t } = useLang();
  const C = t.contacto;
  const telHref = "tel:" + (C.phone || "").replace(/[^+\d]/g, "");
  const mailHref = "mailto:" + (C.email || "");
  return (
    <section className="ai-contact">
      <div className="container ai-contact__inner">
        {/* "Contact" page title removed — the card carries its own
            "Madrid" heading and is now the dominant element on the
            page, with its photo banner pulled up under the nav. */}

        {/* 2-column layout: contact card on the left, embedded Google
            Map on the right. Both share the same rounded surface and
            shadow vocabulary so they read as a matched pair. The grid
            collapses to a single column on phones. */}
        <Reveal delay={120}>
          <div className="ai-contact__grid">
          <article className="ai-contact-card">
            <div className="ai-contact-card__photo">
              <img src={C.photo} alt="" loading="eager" />
            </div>
            <div className="ai-contact-card__body">
              <header className="ai-contact-card__head">
                <div className="ai-contact-card__head-text">
                  <h2 className="ai-contact-card__city">{C.city}</h2>
                  <div className="ai-contact-card__company">{C.company}</div>
                </div>
                <div className="ai-contact-card__icons" role="group" aria-label={C.eyebrow}>
                  <a className="ai-contact-card__ic"
                     href={C.mapUrl}
                     target="_blank" rel="noopener noreferrer"
                     aria-label={C.iconAria && C.iconAria.map}>
                    <Icon.MapPin s={16} />
                  </a>
                  <a className="ai-contact-card__ic"
                     href={mailHref}
                     aria-label={C.iconAria && C.iconAria.mail}>
                    <Icon.Mail s={16} />
                  </a>
                  <a className="ai-contact-card__ic"
                     href={telHref}
                     aria-label={C.iconAria && C.iconAria.phone}>
                    <Icon.Phone s={16} />
                  </a>
                </div>
              </header>

              {/* 2-column body row: office (entrance + phone + email)
                  on the left, registered HQ on the right. Side-by-side
                  is the trick that keeps the card short enough to fit
                  in a 720px viewport. Collapses to a single column on
                  phones via the @media block. */}
              <div className="ai-contact-card__addrs">
                <div className="ai-contact-card__col">
                  {C.officeLabel && <div className="ai-contact-card__addr-label">{C.officeLabel}</div>}
                  <address className="ai-contact-card__addr">
                    {(C.addressLines || []).map((line, i) => (
                      <React.Fragment key={i}>
                        {line}
                        {i < C.addressLines.length - 1 && <br />}
                      </React.Fragment>
                    ))}
                  </address>
                  <div className="ai-contact-card__lines">
                    <a className="ai-contact-card__line" href={telHref}>{C.phone}</a>
                    <a className="ai-contact-card__line ai-contact-card__line--mail" href={mailHref}>{C.email}</a>
                  </div>
                </div>

                {C.legal && (
                  <div className="ai-contact-card__col">
                    <div className="ai-contact-card__addr-label">{C.legal.label}</div>
                    <address className="ai-contact-card__legal-addr">
                      {(C.legal.lines || []).map((line, i) => (
                        <React.Fragment key={i}>
                          {line}
                          {i < C.legal.lines.length - 1 && <br />}
                        </React.Fragment>
                      ))}
                    </address>
                  </div>
                )}
              </div>

              {C.addressNote && <div className="ai-contact-card__note">{C.addressNote}</div>}
            </div>
          </article>

          {/* Map box — Google Maps embed of the office address. A CSS
              skeleton placeholder (navy panel + pin icon + "Cargando
              mapa…" caption) renders immediately while the iframe
              fetches; once the iframe fires `onLoad`, we flip the
              `is-loaded` flag and the placeholder fades out behind
              the iframe. `loading="eager"` so the request starts on
              page load instead of waiting for the user to scroll. */}
          {C.mapEmbedUrl && <ContactMap mapEmbedUrl={C.mapEmbedUrl} mapCta={C.mapCta} />}
          </div>
        </Reveal>

        {/* Social strip — tucked under the card, low-key. URLs come
            from window.AI_SOCIAL so the Footer + Contact stay aligned;
            external links open in a new tab. */}
        <Reveal delay={220}>
          <div className="ai-contact__social" aria-label="Social">
            <a className="ai-contact__social-ic"
               href={AI_SOCIAL.linkedin}
               target="_blank" rel="noopener noreferrer"
               aria-label="LinkedIn">
              <Icon.LinkedIn />
            </a>
            <a className="ai-contact__social-ic"
               href={AI_SOCIAL.youtube}
               target="_blank" rel="noopener noreferrer"
               aria-label="YouTube">
              <Icon.YouTube />
            </a>
          </div>
        </Reveal>
      </div>
    </section>
  );
};

/* -------------------- Newsletter -------------------- */

const PageNewsletter = ({ setRoute }) => {
  const { t } = useLang();
  const N = t.newsletter;
  const open = (id) => setRoute({route:"article", articleId:id});
  return (
    <>
      {/* Banner hero — matches Personas / Talento / Quiénes treatment.
          N.eyebrow ("Noticias" / "News") is the banner title; N.sub
          (the publication-frequency tagline) leads the archive section
          below so the editorial framing isn't lost. */}
      <PersonasHero
        heroImg={N.heroImg}
        title={N.eyebrow}
        scrollHint={N.scrollHint} />

      <section className="section section--alt">
        <div className="container newsletter">
          <Reveal className="section__head">
            <Eyebrow>{N.archiveEyebrow}</Eyebrow>
            {N.sub && <p className="body-lg" style={{marginTop:12,maxWidth:680,color:"var(--fg-mid)"}}>{N.sub}</p>}
          </Reveal>

          <ul className="newsletter__archive">
            {t.news.map((a, i) => (
              <Reveal key={i} delay={80 + i * 80}>
                <li className="newsletter__issue" onClick={() => open(a.id)} style={{cursor:"pointer"}}>
                  <a className="newsletter__issue-thumb">
                    <img src={a.img} alt="" loading="lazy" />
                  </a>
                  <div className="newsletter__issue-body">
                    <div className="newsletter__issue-meta">
                      <span className="newsletter__issue-n">{a.n}</span>
                      <span className="newsletter__issue-date">{a.date}</span>
                      <span className="newsletter__issue-tag">{a.tag}</span>
                      <span className="newsletter__issue-read">{a.read}</span>
                    </div>
                    <a className="newsletter__issue-title">{a.title} <span className="arr">→</span></a>
                    <p className="newsletter__issue-excerpt">{a.excerpt}</p>
                  </div>
                </li>
              </Reveal>
            ))}
          </ul>
        </div>
      </section>
    </>
  );
};

/* -------------------- Article body renderer + Article page -------------------- */

/* Renders one block from an article body. Maps block.type → the brand
   typography token. Adding a new type (e.g. "list", "code") is just one case
   here plus a matching .article-block--* CSS rule. The future editor will
   produce this same shape. */
const ArticleBlock = ({ block, i }) => {
  switch (block.type) {
    case "lede":
      return <p className="article-block article-block--lede" key={i}>{block.text}</p>;
    case "p":
      return <p className="article-block article-block--p" key={i}>{block.text}</p>;
    case "h2":
      return <h2 className="article-block article-block--h2" key={i}>{block.text}</h2>;
    case "h3":
      return <h3 className="article-block article-block--h3" key={i}>{block.text}</h3>;
    case "image":
      return (
        <figure className="article-block article-block--image" key={i}>
          <img src={block.src} alt={block.caption || ""} loading="lazy" />
          {block.caption && <figcaption>{block.caption}</figcaption>}
        </figure>
      );
    case "quote":
      return <blockquote className="article-block article-block--quote" key={i}>{block.text}</blockquote>;
    default:
      return null;
  }
};

const PageArticle = ({ articleId, setRoute }) => {
  const { t } = useLang();
  const N = t.newsletter;
  const article = t.news.find(a => a.id === articleId);

  // Defensive — if someone lands here with no/bad id, send them back to the list.
  if (!article) return (
    <section className="section section--alt">
      <div className="container" style={{textAlign:"center",padding:"80px 0"}}>
        <Eyebrow>{N.eyebrow}</Eyebrow>
        <p className="body-lg" style={{marginTop:16}}>{N.notFound}</p>
        <a className="article__back" onClick={() => setRoute("newsletter")} style={{marginTop:24,display:"inline-block"}}>← {N.backToList}</a>
      </div>
    </section>
  );

  return (
    <article className="article">
      <div className="article__hero" style={{backgroundImage:`url(${article.img})`}}>
        <div className="article__hero-ov"/>
        <div className="container article__hero-inner">
          <a className="article__back" onClick={() => setRoute("newsletter")}>← {N.backToList}</a>
          <Reveal delay={120}>
            <div className="article__meta">
              <span className="article__meta-n">{article.n}</span>
              <span className="article__meta-date">{article.date}</span>
              <span className="article__meta-tag">{article.tag}</span>
              <span className="article__meta-read">{article.read}</span>
            </div>
          </Reveal>
          <Reveal delay={200}>
            <h1 className="article__title">{article.title}</h1>
          </Reveal>
        </div>
      </div>

      <div className="container article__body">
        {article.body && article.body.map((block, i) => (
          <Reveal key={i} delay={Math.min(i * 40, 240)}>
            <ArticleBlock block={block} i={i} />
          </Reveal>
        ))}
      </div>
    </article>
  );
};

/* PageLegal — single component that renders any of the long-form legal
   docs (cookies / privacidad / aviso legal). Body content comes from
   t.legalDocs[key] with a fall-back to ES, since the legal text is
   locale-independent (Spanish jurisdiction). Each doc is an array of
   typed BLOCKS — paragraphs, headings, lists, tables — rendered by a
   switch below. */
const PageLegal = ({ legalKey, setRoute }) => {
  const { t, lang } = useLang();
  /* Resolve the doc — prefer the active locale, fall back to ES since
     the legal body is locale-independent (Spanish jurisdiction). If
     the active locale defined an empty/missing doc we'd otherwise
     render nothing; explicitly fall back to ES.blocks when that
     happens so the page is never visually empty. */
  const tDoc  = t.legalDocs && t.legalDocs[legalKey];
  const esDoc = window.AI_I18N && window.AI_I18N.es && window.AI_I18N.es.legalDocs && window.AI_I18N.es.legalDocs[legalKey];
  const doc = (tDoc && Array.isArray(tDoc.blocks) && tDoc.blocks.length > 0) ? tDoc : esDoc;
  const backLabel = lang === "es" ? "Volver" : "Back";
  if (!doc) {
    return (
      <section className="section section--light">
        <div className="container"><p>—</p></div>
      </section>
    );
  }
  const renderBlock = (b, i) => {
    switch (b.type) {
      case "h":
        return <h2 key={i} className="legal__h2">{b.text}</h2>;
      case "h3":
        return <h3 key={i} className="legal__h3">{b.text}</h3>;
      case "p":
        return <p key={i} className="legal__p" dangerouslySetInnerHTML={{__html: b.html}} />;
      case "ul":
        return (
          <ul key={i} className="legal__ul">
            {b.items.map((it, j) => <li key={j} dangerouslySetInnerHTML={{__html: it}} />)}
          </ul>
        );
      case "table":
        return (
          <div key={i} className="legal__table-wrap">
            <table className="legal__table">
              <thead>
                <tr>{b.headers.map((h, j) => <th key={j}>{h}</th>)}</tr>
              </thead>
              <tbody>
                {b.rows.map((row, ri) => (
                  <tr key={ri}>{row.map((cell, ci) => <td key={ci}>{cell}</td>)}</tr>
                ))}
              </tbody>
            </table>
          </div>
        );
      default:
        return null;
    }
  };
  return (
    <section className="legal">
      <div className="container legal__inner">
        <Reveal>
          <a className="legal__back" onClick={() => setRoute("home")}>← {backLabel}</a>
        </Reveal>
        {/* Single title only — the previous eyebrow + h1 pair rendered
            the same string twice ("AVISO LEGAL" + "Aviso Legal") since
            doc.eyebrow and doc.title carry the same value across all
            three legal docs. The h1 alone is enough. */}
        <Reveal delay={60}>
          <h1 className="legal__title">{doc.title}</h1>
        </Reveal>
        {/* Body NOT wrapped in Reveal — the legal text is many viewport
            heights tall, so an IntersectionObserver with threshold 0.15
            on the wrapper would never see 15% of the body visible at
            once and the .is-revealed class would never get added,
            leaving the whole block at opacity 0 forever. Rendering the
            body directly skips the animation but guarantees it's
            visible immediately. */}
        <div className="legal__body">
          {doc.blocks.map(renderBlock)}
        </div>
      </div>
    </section>
  );
};

Object.assign(window, { PageHome, PageQuienes, PageProyectos, PageSector, PagePersonas, PageTalento, PageNewsletter, PageArticle, PageContacto, PageLegal, PageVida });
