// PHYSIO SAMOS — About page (Σχετικά: ιστορία, βιογραφικά, κριτικές Google)

const AboutHero = () => {
  const { useRef } = React;
  const [t] = useT();
  const headingRef = useRef(null);
  return (
    <section className="page-hero" data-screen-label="About hero">
      <HeroAurora />
      <div className="wrap page-hero__inner">
        <span className="eyebrow">{t.about.eyebrow}</span>
        <h1 ref={headingRef} className="page-hero__title">
          <window.VariableProximity label={t.about.title_a.replace(/[,.]\s*$/, "")}
                                    containerRef={headingRef} {...window.PS.PROX} />{" "}
          <span className="serif" style={{ color: "var(--sage-dark)" }}>
            <window.VariableProximity label={t.about.title_b.replace(/\.\s*$/, "") + "."}
                                      containerRef={headingRef} {...window.PS.PROX} />
          </span>
        </h1>
        <p className="page-hero__lead">{t.about.lead}</p>
        <div className="page-hero__actions">
          <button className="btn btn-primary" onClick={() => window.PS.openBooking()}>
            {t.nav.book}<span className="arrow">→</span>
          </button>
          <a href="tel:+302273078420" className="btn btn-ghost">
            <Icon name="phone" size={14} /> 22730 78420
          </a>
        </div>
      </div>
    </section>
  );
};

const BioCard = ({ member, idx, photoSrc, reverse }) => {
  const [t, lang] = useT();
  const accentColor = idx === 0 ? "var(--sage-dark)" : "var(--terra)";
  // The accent and which way the offset rule leans are the only per-person parts; the frame
  // itself is in main.css. The rule leans away from the text column, so the two portraits
  // mirror each other across the page.
  const Photo = (
    <div className="bio-photo" style={{ "--bio-accent": accentColor, "--bio-offset": reverse ? "16px" : "-16px" }}>
      <div className="img-overlay" style={{
        position: "relative", aspectRatio: "4/5", borderRadius: "var(--radius-lg)",
        overflow: "hidden", background: "var(--bg-3)",
      }}>
        {/* No caption. It carried `member.name` and `member.role`, and both are already in
            this card: the name is the h2 a few pixels to the side (and again under the
            quote), the role is the line directly beneath it. A caption exists to name a
            subject the page does not otherwise name, which is not the case for a portrait
            sitting next to an 88px heading of that person's name. `credentials` is the only
            member field still unrendered, and it is a 6-8 item list rather than a title and
            a subtitle — so it needs its own treatment, not this slot. */}
        <window.Img name={photoSrc} w={1200} sizes="(max-width: 900px) 100vw, 40vw"
                    position="center center" alt={member.name}
                    style={{ width: "100%", height: "100%", objectFit: "cover" }} />
      </div>
    </div>
  );
  // Both bios read the same. They used to alternate grounds to separate themselves; they sit
  // on the same below level now, so that job belongs to the level.
  return (
    <section data-screen-label={`Bio ${idx + 1}`} style={{
      paddingTop: idx === 0 ? 40 : 80, paddingBottom: 80,
    }}>
      <div className="wrap">
        <div className="g g--bio" style={{ "--cols": reverse ? "1.1fr 0.9fr" : "0.9fr 1.1fr", gap: 80, alignItems: "start" }}>
          {!reverse && Photo}
          <div>
            <span className="eyebrow" style={{ color: accentColor }}>{member.focus}</span>
            <h2 className="bio-name" style={{ marginTop: 18, marginBottom: 14 }}>
              <window.BlurText text={member.name.split(" ")[0]} />{" "}
              <span className="serif" style={{ color: accentColor }}><window.BlurText text={member.name.split(" ").slice(1).join(" ")} /></span>
            </h2>
            <div style={{ fontFamily: "var(--font-display)", fontSize: 18, color: "var(--ink-3)", marginBottom: 32 }}>{member.role}</div>

            <span className="eyebrow">{t.team.bio_label}</span>
            {/* Left-aligned, ragged right (owner's call, 2026-09-05). It was justified with
                hyphens: auto — the hyphenation existed to close the word gaps a justified
                335px column opens on long Greek compounds, and it went with the justification:
                hyphenated ragged text reads as a typesetting accident. */}
            <p style={{ marginTop: 14, fontSize: 18, lineHeight: 1.65, color: "var(--ink-2)",
                        fontFamily: "var(--font-display)", fontWeight: 400,
                        textAlign: "left" }}>{member.bio}</p>

            <div style={{ marginTop: 32, padding: 28, borderRadius: "var(--radius)",
              background: "var(--bg-2)", border: "1px solid var(--line-strong)" }}>
              <span style={{ fontFamily: "var(--font-display)", fontSize: 36, color: accentColor, lineHeight: 0.7, display: "block" }}>&ldquo;</span>
              <p style={{ fontFamily: "var(--font-serif)", fontStyle: "italic", fontSize: 22, lineHeight: 1.4, color: "var(--ink)", marginTop: 4 }}>{member.quote}</p>
              <div style={{ marginTop: 18, paddingTop: 16, borderTop: "1px solid var(--line)", fontSize: 12, color: "var(--ink-3)" }}>— {member.name}</div>
            </div>

            <div className="bio-actions" style={{ marginTop: 36, display: "flex", gap: 12, flexWrap: "wrap" }}>
              <button className="btn btn-primary" onClick={() => window.PS.openBooking()}>
                {t.ui.book_with} {member.name.split(" ")[0]} →
              </button>
              <a href="/services" className="btn btn-ghost">
                {t.ui.see_services}
              </a>
            </div>
          </div>
          {reverse && Photo}
        </div>
      </div>
    </section>
  );
};

const BiosSection = () => {
  const [t] = useT();
  // The same two portraits the home page's team preview uses, at the owner's request.
  // They suit this box far better than the reception shots did: the box is 4/5 and these are
  // 3:4, so `cover` trims ~6% of the height and nothing else, where the landscape reception
  // frames had to be cropped hard and then rescued with a hand-tuned object-position.
  const photos = ["team-maria-portrait", "team-stamatina-portrait"];
  // No section head. It read "Η ομάδα" / "Οι άνθρωποι πίσω από το κέντρο." and came off at
  // the owner's request; the two bio cards introduce themselves. Its three strings are left
  // in the dictionaries rather than deleted — they are owner copy, and the orphan check in
  // AGENTS.md is what surfaces them if they are ever wanted again.
  //
  // Their key names are deliberately not written out here: that check tests whether a key's
  // last segment appears anywhere in the JSX, so naming them in a comment marks them as used
  // and hides them from it.
  return (
    <section data-screen-label="Bios" style={{ paddingBottom: 0 }}>
      {t.team.members.map((m, i) => (
        <BioCard key={i} member={m} idx={i} photoSrc={photos[i]} reverse={i === 1} />
      ))}
    </section>
  );
};

// Testimonials, laid out after a React Bits-style vertical carousel: the heading and the
// aggregate sit still on the left while the cards scroll themselves on the right.
//
// The auto-scroll is a rAF loop over a real `overflow-y: auto` container rather than a CSS
// marquee, because the brief was both "scrollable" and "scrolling by itself" — a keyframe
// translate gives the second and takes away the first. The list is rendered twice and the
// loop subtracts one copy's height on the wrap, so the seam never shows.
const ReviewsColumn = ({ items, avatarColors }) => {
  const { useRef, useEffect } = React;
  const scrollRef = useRef(null);

  useEffect(() => {
    const el = scrollRef.current;
    if (!el) return;
    if (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;

    const SPEED = 26;                 // px per second: slow enough to read a card in passing
    let raf, last = 0, paused = false, pos = 0, lastSet = -1;

    const step = (ts) => {
      if (!last) last = ts;
      const dt = Math.min((ts - last) / 1000, 0.05);   // a backgrounded tab must not lurch
      last = ts;
      // If anything else moved the scroller — a wheel, a drag, a focus jump — take its
      // position as the new truth instead of fighting it.
      if (lastSet >= 0 && Math.abs(el.scrollTop - lastSet) > 2) pos = el.scrollTop;
      if (!paused) {
        const half = el.scrollHeight / 2;
        pos += SPEED * dt;
        if (half > 0 && pos >= half) pos -= half;
        el.scrollTop = pos;
        lastSet = el.scrollTop;
      }
      raf = requestAnimationFrame(step);
    };
    raf = requestAnimationFrame(step);

    const pause = () => { paused = true; };
    const resume = () => { paused = false; last = 0; pos = el.scrollTop; };
    const on = [["mouseenter", pause], ["mouseleave", resume], ["focusin", pause],
                ["focusout", resume], ["touchstart", pause], ["touchend", resume]];
    on.forEach(([e, h]) => el.addEventListener(e, h, { passive: true }));
    return () => {
      cancelAnimationFrame(raf);
      on.forEach(([e, h]) => el.removeEventListener(e, h));
    };
  }, [items.length]);

  const doubled = [...items, ...items];
  return (
    <div className="rcol" ref={scrollRef} tabIndex={0} aria-label="Google reviews">
      {doubled.map((r, i) => (
        <article key={i} className="rcard" aria-hidden={i >= items.length ? "true" : undefined}>
          <div className="rcard__top">
            <span className="rcard__avatar" style={{ background: avatarColors[i % avatarColors.length] }}>
              {r.initial}
            </span>
            <div className="rcard__who">
              <div className="rcard__name">{r.name}</div>
              <div className="rcard__when">{r.when}</div>
            </div>
            <span className="rcard__quote" aria-hidden="true"><Icon name="quote" size={18} /></span>
          </div>
          <window.Stars n={r.stars} size={14} />
          <p className="rcard__text">{r.text}</p>
        </article>
      ))}
    </div>
  );
};

const ReviewsSection = () => {
  const [t] = useT();
  const avatarColors = ["#68a29b", "#d97757", "#2d4a47", "#c8956d", "#86c1ba", "#3a6b65"];
  return (
    <section data-screen-label="Reviews">
      <div className="wrap">
        <div className="g g--stack" style={{ "--cols": "1fr 1fr", gap: 60, alignItems: "center" }}>
          <div className="split-head">
            <span className="eyebrow">{t.reviews.eyebrow}</span>
            <h2 className="reviews-title" style={{ marginTop: 18 }}>
              <window.BlurText text={t.reviews.title_a} /><br />
              <span className="serif" style={{ color: "var(--sage-dark)" }}><window.BlurText text={t.reviews.title_b} /></span>
            </h2>
            <div className="rscore">
              <div className="rscore__num">{t.reviews.score}</div>
              <div className="rscore__meta">
                <window.Stars n={5} size={17} />
                {/* The count string already names Google in every language, so the
                    hardcoded prefix said it twice. */}
                <div className="mono rscore__count">{t.reviews.count}</div>
              </div>
            </div>
          </div>
          <ReviewsColumn items={t.reviews.items} avatarColors={avatarColors} />
        </div>
      </div>
    </section>
  );
};

// Closing the page on the two therapists' qualifications.
//
// **Every string here already existed in the dictionaries, fully translated, rendering
// nowhere.** `team.members[].credentials` and the `team.credentials_label` that titles it were
// both orphans. Nothing was transcribed, reworded or sourced from elsewhere — these are
// credentials on a medical clinic's site, so they are the owner's text verbatim or they do not
// go up. The list mixes degrees with specialisms, which is why it carries the owner's own
// label ("Πιστοποιήσεις & εκπαίδευση") rather than being called certifications outright.
//
// The accents match the bios above: `--sage-dark` for the first therapist, `--terra` for the
// second, so each woman keeps one colour down the whole page.
// The 16 certificate scans, built through the media pipeline like every other image here
// (media-src/manifest.tsv -> build-images.sh -> PS_MEDIA), so the wall gets this site's own
// derivatives rather than hotlinking the originals. `PS.img` resolves a slug to a URL.
// Measured off the built derivatives: ten are 800x520 and six are 800x1231.
const CERT_PORTRAIT = new Set([6, 7, 8, 10, 11, 13]);

// Kept in step with the `certOut` keyframe in main.css — if they disagree the dialog either
// disappears mid-animation or lingers as a dead overlay.
const DIALOG_EXIT_MS = 220;

const CERT_TILES = Array.from({ length: 16 }, (_, i) => {
  const slug = `cert-maria-${String(i + 1).padStart(2, "0")}`;
  // Read the widths out of PS_MEDIA rather than asking PS.img for a size: PS.img falls back to
  // a hardcoded [1200] for a slug it does not know, and these were built at 480/800 only, so a
  // stale manifest silently yields a URL that 404s instead of failing loudly.
  const widths = (window.PS_MEDIA && window.PS_MEDIA[slug]) || [];
  const full = `media/${slug}-${widths[widths.length - 1] || 800}.jpg`;
  return {
    image: full,
    full,
    // The scans come in exactly two shapes; the tile takes its height from this, so a portrait
    // certificate gets a portrait box instead of being letterboxed into a landscape one.
    ratio: CERT_PORTRAIT.has(i + 1) ? 800 / 1231 : 800 / 520,
    srcSet: widths.map((w) => `media/${slug}-${w}.jpg ${w}w`).join(", "),
    sizes: "190px",
  };
});

const CREDENTIAL_ACCENTS = ["var(--sage-dark)", "var(--terra)"];

// The wall's geometry per column width. The desktop keeps the pitched, oversized plane; below
// 900px the plane is laid out to the column it is given — all five columns, tiles sized so the
// untransformed plane is exactly the container's width, no turn, a
// slight tilt, scale 1 — so every column is on screen instead of the two outer ones being cut
// by the viewport (owner's report, 2026-09-05). Measured with a ResizeObserver; a change of
// mode or width remounts the wall through its key, which is rare (a rotation) and cheap.
const DESKTOP_WALL = { columns: 5, tileWidth: 190, tileHeight: 128, gap: 16, tilt: 14, turn: -12, perspective: 1150, depth: 110, scale: 1.18 };
const useWallLayout = (ref) => {
  const { useState, useEffect } = React;
  const [box, setBox] = useState({ w: 0, small: false });
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const mq = window.matchMedia("(max-width: 900px)");
    const measure = () => setBox({ w: el.clientWidth, small: mq.matches });
    measure();
    const ro = window.ResizeObserver ? new ResizeObserver(measure) : null;
    if (ro) ro.observe(el); else window.addEventListener("resize", measure);
    mq.addEventListener("change", measure);
    return () => { if (ro) ro.disconnect(); else window.removeEventListener("resize", measure); mq.removeEventListener("change", measure); };
  }, []);
  if (!box.small || !box.w) return { key: "desktop", props: DESKTOP_WALL };
  // All five columns at every width (owner's call, 2026-09-05 — three on a phone and four on a
  // tablet were tried first). The gap tightens on a phone so the tiles keep what width there is.
  const columns = 5, gap = box.w < 520 ? 8 : 12;
  // 5% under the column's share: even at 4deg of tilt the perspective magnifies the rows nearest
  // the viewer by ~3% (the track runs well past the host), and at 6deg the outer columns
  // overhung the host by 14px on a 375 phone. Measured, not guessed.
  const tileWidth = Math.floor(((box.w - (columns - 1) * gap) / columns) * 0.95);
  return {
    key: `${columns}-${tileWidth}`,
    props: { columns, gap, tileWidth, tileHeight: Math.round(tileWidth * 128 / 190), tilt: 4, turn: 0, perspective: 1150, depth: 60, scale: 1 },
  };
};

const CredentialsSection = () => {
  const { useState, useEffect, useCallback, useRef } = React;
  const [t] = useT();
  const wallRef = useRef(null);
  const wall = useWallLayout(wallRef);
  const [open, setOpen] = useState(null);
  const [closing, setClosing] = useState(false);
  const closeTimer = useRef(0);

  // Closing is a state, not an unmount. React removes the node the instant `open` goes null,
  // so an exit animation needs the element to outlive the decision to close: `is-closing`
  // plays the reverse, and the unmount waits for it. Reduced motion skips the wait entirely
  // rather than sitting through a delay for an animation that will not play.
  const requestClose = useCallback(() => {
    if (closeTimer.current) return;                       // already on the way out
    const instant = window.matchMedia
      && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    if (instant) { setOpen(null); return; }
    setClosing(true);
    closeTimer.current = setTimeout(() => {
      closeTimer.current = 0;
      setClosing(false);
      setOpen(null);
    }, DIALOG_EXIT_MS);
  }, []);

  useEffect(() => () => { if (closeTimer.current) clearTimeout(closeTimer.current); }, []);

  // Escape closes, and the page behind must not scroll while a certificate is up.
  useEffect(() => {
    if (!open) return;
    const onKey = (e) => { if (e.key === "Escape") requestClose(); };
    window.PS.lockScroll(true);
    window.addEventListener("keydown", onKey);
    return () => {
      window.removeEventListener("keydown", onKey);
      window.PS.lockScroll(false);
      window.PS.syncScroll();
    };
  }, [open, requestClose]);

  // The dialog's accessible name drops the heading's terminal stop: a name is a label, not a
  // sentence, and the site's convention gives labels no terminal punctuation.
  const dialogName = t.team.credentials_label.replace(/\.$/, "");

  return (
    <section data-screen-label="Credentials">
      <div className="wrap">
        <div className="sec-head">
          {/* `team.focus_label`, not `team.eyebrow`: "Η ομάδα" belonged to this section when it
              held both women's credential cards, and reads wrong now that it is a wall of
              certificates. Another orphaned key given a home — no new copy either way. */}
          <span className="eyebrow">{t.team.focus_label}</span>
          <h2 className="sec-head__title">
            <window.BlurText text={t.team.credentials_label} />
          </h2>
        </div>
        {/* The scans are Maria's — the clinic's public site carries 16 for her and none for
            Stamatina. They are shown unattributed at the owner's request (2026-08-31), which
            works here because the section no longer sets anyone's credentials beside them: the
            per-person cards that used to sit underneath are gone, so the wall reads as the
            centre's training rather than as a claim about either woman. Put the attribution
            back if per-person copy ever returns to this section. */}
        <div className="cert-wall" ref={wallRef}>
          <window.DriftWall key={wall.key} items={CERT_TILES} {...wall.props}
                            speed={26} variance={0.45} parallax={0.5}
                            lift={54} fade={0.62} dim={0.88}
                            onSelect={(item) => setOpen(item)} />
        </div>
      </div>

      {/* Portalled to <body>, the same reason the mobile nav sheet is. `.lvl-above` is
          `position: relative; z-index: 1`, which makes it a stacking context — rendered in
          place, this dialog's `z-index: 100` was scoped *inside* that level, so the nav
          (z-index 50, a root-level sibling) painted straight over the top of it. A portal puts
          the dialog back at the root where its z-index means what it says. */}
      {open && ReactDOM.createPortal(
        <div className={`cert-view${closing ? " is-closing" : ""}`} role="dialog" aria-modal="true"
             aria-label={dialogName}
             onClick={requestClose}>
          <button type="button" className="cert-view__close"
                  aria-label={t.ui.close || "Close"}
                  onClick={requestClose}>
            <Icon name="close" size={22} />
          </button>
          {/* Only the backdrop closes: a click that lands on the certificate itself must not,
              or dragging to look at a detail would dismiss it. */}
          <img className="cert-view__img" src={open.full} alt="" draggable={false}
               onClick={(e) => e.stopPropagation()} />
        </div>,
        document.body
      )}
    </section>
  );
};

const App = () => {
  window.PS.useReveal();
  return (
    <>
      <PageBackdrop />
      <Nav current="about" />
      {/* Reviews sit directly under the hero, sharing its slab: what other patients say is
          the thing a visitor wants before the two long biographies, not after them. */}
      <Level at="above"><AboutHero /><ReviewsSection /></Level>
      <Level at="below"><BiosSection /></Level>
      <Level at="above"><CredentialsSection /></Level>
      <Level at="below"><Footer /></Level>
      <BookingModal />
    </>
  );
};
window.AboutApp = App;
