/* =============================================================================
   랩스휘트니스 독산2호점 — 화면 본체
   -----------------------------------------------------------------------------
   글자는 전부 src/content.js 에 있다. 이 파일은 구조와 디자인만 담당한다.
   ========================================================================== */

const C = window.CONTENT;
const { useState, useEffect, useRef } = React;

/* 촬영 모드 — 주소 뒤에 ?shot=1 을 붙였을 때만 켜진다. */
const SHOT = window.location.search.indexOf("shot=1") >= 0;
const LAZY = SHOT ? "eager" : "lazy";

/* ── 공통 유틸 ─────────────────────────────────────────────────────────── */

// 줄바꿈 다듬기(26-09-29) — 글자는 그대로 두고 끊기는 자리만 정한다.
//  · 가운뎃점(·)이 줄 맨 앞에 오지 않게 앞 글자에 붙인다
//  · "2층, 3층" 은 한 덩어리로
//  · 짧은 괄호 "(매너네트빌 맞은편)" 은 통째로 다음 줄로 넘긴다(칸이 좁으면 그 안에서만 끊김)
function fmt(text) {
  if (!text) return text;
  const s = String(text)
    .replace(/ · /g, "\u00A0· ")
    .replace(/(\S)·(?=\S)/g, "$1\u2060·")
    .replace(/·(?=[^\s​])/g, "·" + String.fromCharCode(8203))
    .replace(/ (\d급)/g, String.fromCharCode(160) + "$1")
    .replace(/(\d+층), (\d+층)/g, "$1,\u00A0$2");
  const parts = s.split(/(\([^()]{1,20}\))/);
  if (parts.length === 1) return s;
  return parts.map((p, i) =>
    i % 2 ? <React.Fragment key={i}><wbr /><span className="inline-block">{p}</span></React.Fragment> : p
  );
}

// 스크롤해서 화면에 들어오면 한 번만 떠오르는 효과
function useReveal() {
  useEffect(() => {
    const els = document.querySelectorAll(".reveal:not(.in)");
    if (SHOT) {
      els.forEach((el) => el.classList.add("in"));
      return;
    }
    if (!("IntersectionObserver" in window)) {
      els.forEach((el) => el.classList.add("in"));
      return;
    }
    const io = new IntersectionObserver(
      (entries) => {
        entries.forEach((e) => {
          if (e.isIntersecting) {
            e.target.classList.add("in");
            io.unobserve(e.target);
          }
        });
      },
      { threshold: 0.12, rootMargin: "0px 0px -8% 0px" }
    );
    els.forEach((el) => io.observe(el));
    return () => io.disconnect();
  });
}

// 섹션 상단 라벨 (영문 소문자 스페이싱 + 한글 제목)
function SectionHead({ eyebrow, title, desc, align = "center", light = false, hl, mark }) {
  const lines = Array.isArray(title) ? title : [title];
  // 26-09-28 업체 레퍼런스(형광 강조)를 파란색으로 — hl: 제목 중 파란 글씨로 칠할 부분, mark: 제목 전체를 파란 박스로
  const paint = (l) => {
    if (mark) return <span className="neon-mark">{l}</span>;
    if (!hl || l.indexOf(hl) < 0) return l;
    const i = l.indexOf(hl);
    return <React.Fragment>{l.slice(0, i)}<span className={light ? "text-neonLight" : "text-neon"}>{hl}</span>{l.slice(i + hl.length)}</React.Fragment>;
  };
  const a = align === "left" ? "text-left items-start" : "text-center items-center";
  return (
    <div className={"flex flex-col " + a + " reveal"}>
      {eyebrow ? (
        <p className={"font-label text-[13px] tracking-mega uppercase mb-3.5 " + (light ? "text-neonLight" : "text-neon")}>
          {eyebrow}
        </p>
      ) : null}
      <h2
        className={
          "text-[32px] sm:text-[43px] lg:text-[50px] leading-[1.24] font-bold tracking-[-0.015em] break-keep " +
          (light ? "text-cream" : "text-ink")
        }
      >
        {lines.map((l, i) => (
          <span key={i} className="block">{paint(l)}</span>
        ))}
      </h2>
      {desc ? (
        <p
          className={
            "mt-4 max-w-2xl text-[17.5px] sm:text-[18.5px] leading-[1.8] " +
            (light ? "text-cream/85" : "text-ink/92") +
            (align === "center" ? " mx-auto" : "")
          }
        >
          {desc}
        </p>
      ) : null}
      <div className={"mt-6 h-[2px] w-14 " + (light ? "bg-neonLight" : "bg-neon")} />
    </div>
  );
}

/* 로고 — 별도 벡터 파일이 없어 트레이너 포스터 속 워드마크를 인라인 SVG로 재현.
   남색 둥근 사각형 배지 안에 흰색 "R" + 옆에 "RAPS FITNESS" 텍스트(Anton). */
function Logo({ small, dark }) {
  const h = small ? 26 : 32;
  return (
    <span className="inline-flex items-center gap-2.5 leading-none">
      <img
        src={dark ? C.brand.logoDark : C.brand.logoLight}
        alt={C.brand.name}
        style={{ height: h }}
        className="w-auto"
      />
      <span className={"font-label text-[10px] tracking-mega mt-0.5 " + (dark ? "text-white/60" : "text-accent")}>
        독산2호점
      </span>
    </span>
  );
}

/* ── 헤더 ──────────────────────────────────────────────────────────────── */

const NAV = [
  { id: "facilities", label: "시설" },
  { id: "trainers", label: "트레이너" },
  { id: "location", label: "오시는 길" },
  { hash: "#/column", label: "칼럼" },
];

function Header({ alwaysSolid }) {
  const [scrolled, setScrolled] = useState(false);
  const [open, setOpen] = useState(false);

  useEffect(() => {
    const onScroll = () => setScrolled(window.scrollY > 40);
    onScroll();
    window.addEventListener("scroll", onScroll, { passive: true });
    return () => window.removeEventListener("scroll", onScroll);
  }, []);

  const solid = alwaysSolid || scrolled || open;

  // 칼럼 페이지에 있을 때 홈 섹션을 누르면, 홈으로 먼저 돌아간 뒤 해당 섹션으로 내려간다
  const go = (item) => {
    setOpen(false);
    if (item.hash) {
      window.location.hash = item.hash;
      return;
    }
    const id = item.id || item;
    const jump = () => {
      const el = document.getElementById(id);
      if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
    };
    if (window.location.hash && window.location.hash !== "#/") {
      window.location.hash = "";
      setTimeout(jump, 80);
    } else {
      jump();
    }
  };

  return (
    <header
      className={
        "fixed inset-x-0 top-0 z-50 transition-all duration-500 " +
        (solid ? "bg-cream/90 backdrop-blur-md border-b border-ink/10 py-3" : "py-5")
      }
    >
      <div className="mx-auto max-w-8xl px-5 sm:px-8 flex items-center justify-between gap-6">
        <button
          onClick={() => {
            if (window.location.hash) window.location.hash = "";
            window.scrollTo({ top: 0, behavior: "smooth" });
          }}
          className="text-left"
        >
          <Logo dark={!solid} />
        </button>

        <nav className="hidden lg:flex items-center gap-8">
          {NAV.map((n) => (
            <button
              key={n.id || n.hash}
              onClick={() => go(n)}
              className={
                "text-[16.5px] transition-colors " +
                (solid ? "text-ink/90 hover:text-accent" : "text-white/80 hover:text-white")
              }
            >
              {n.label}
            </button>
          ))}
          <a
            href={"tel:" + C.footer.phone.replace(/[^0-9+]/g, "")}
            className={
              "rounded-full px-5 py-2.5 text-[16px] font-semibold transition-colors " +
              (solid ? "bg-accent text-white hover:bg-accentDark" : "bg-white text-accent hover:bg-tint")
            }
          >
            전화 문의
          </a>
        </nav>

        <button
          onClick={() => setOpen((v) => !v)}
          aria-label="메뉴"
          className="lg:hidden flex flex-col gap-[5px] p-2"
        >
          <span className={"block h-px w-6 transition-transform " + (solid ? "bg-ink" : "bg-white") + " " + (open ? "translate-y-[6px] rotate-45" : "")} />
          <span className={"block h-px w-6 transition-opacity " + (solid ? "bg-ink" : "bg-white") + " " + (open ? "opacity-0" : "")} />
          <span className={"block h-px w-6 transition-transform " + (solid ? "bg-ink" : "bg-white") + " " + (open ? "-translate-y-[6px] -rotate-45" : "")} />
        </button>
      </div>

      {open ? (
        <div className="lg:hidden mt-4 mx-5 rounded-2xl bg-ivory border border-ink/10 p-4 shadow-lg">
          {NAV.map((n) => (
            <button
              key={n.id || n.hash}
              onClick={() => go(n)}
              className="block w-full text-left px-3 py-3 text-[17px] text-ink/90 border-b border-ink/5 last:border-0"
            >
              {n.label}
            </button>
          ))}
          <a
            href={"tel:" + C.footer.phone.replace(/[^0-9+]/g, "")}
            className="mt-3 block w-full text-center rounded-full bg-accent px-5 py-3 text-[16px] font-semibold text-white"
          >
            전화 문의 {C.footer.phone}
          </a>
        </div>
      ) : null}
    </header>
  );
}

/* ── 1. 첫 화면 ───────────────────────────────────────────────────────── */

/* 히어로 통계 숫자가 화면에 들어오면 0에서 실제 값까지 세어 올라간다.
   "2·3층"처럼 숫자가 아닌 문자가 섞여 있어도 숫자 부분만 각각 세고 나머지는 그대로 둔다. */
function CountUpStat({ value, duration = 1600, startDelay = 1600 }) {
  const str = String(value);
  const [display, setDisplay] = useState(() => str.replace(/\d/g, "0"));
  useEffect(() => {
    const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    const nums = [];
    str.replace(/\d+/g, (m, offset) => { nums.push({ offset, len: m.length, target: parseInt(m, 10) }); return m; });
    if (reduceMotion || nums.length === 0) { setDisplay(str); return; }
    let raf;
    // 부모(.hero-in)가 페이드인하는 동안(약 1.5초) 카운트가 끝나버리면 실제로는 눈에 안 보이고
    // 지나간다 — 그래서 페이드인이 다 끝난 뒤에야 숫자가 세어 올라가는 걸 보여준다.
    const timeoutId = setTimeout(() => {
      const start = performance.now();
      const tick = (now) => {
        const t = Math.min((now - start) / duration, 1);
        const eased = 1 - Math.pow(1 - t, 3);
        let out = str;
        nums.forEach((n) => {
          const cur = Math.round(n.target * eased);
          out = out.slice(0, n.offset) + String(cur).padStart(n.len, "0") + out.slice(n.offset + n.len);
        });
        setDisplay(out);
        if (t < 1) raf = requestAnimationFrame(tick);
      };
      raf = requestAnimationFrame(tick);
    }, startDelay);
    return () => { clearTimeout(timeoutId); cancelAnimationFrame(raf); };
  }, [value]);
  return <span>{display}</span>;
}

function Hero() {
  const h = C.hero;
  const go = (id) => {
    const el = document.getElementById(id);
    if (el) el.scrollIntoView({ behavior: "smooth" });
  };
  const step = (n) => ({ animationDelay: n * 140 + "ms" });
  // 26-09-28 업체 무드보드: 흰 박스 + 남색(#002063) 글자
  const btn = "rounded-xl bg-white px-7 py-4 text-[16px] font-semibold text-accent hover:bg-tint transition-colors whitespace-nowrap";

  return (
    <section className="relative min-h-[100svh] flex items-end lg:items-center overflow-hidden bg-accent">
      {/* 흑백 사진 + 남색 그라데이션 (얼굴 없이 팔·덤벨만 보이는 업체 AI 사진) */}
      <img
        src={h.image}
        alt=""
        aria-hidden="true"
        fetchpriority="high"
        className="hero-img hero-ken absolute inset-x-0 top-0 w-full object-cover grayscale"
      />
      <div className="absolute inset-0 hero-tint" />
      <div className="absolute inset-0 hero-navy" />

      <div className="relative mx-auto w-full max-w-8xl px-5 sm:px-8 pt-[46svh] pb-16 lg:py-32">
        <div className="lg:ml-auto lg:w-[52%]">
          <p className="hero-in font-label text-[12.5px] sm:text-[13.5px] tracking-mega uppercase text-neonLight" style={step(0.6)}>
            {h.eyebrow}
          </p>

          <h1
            className="hero-in mt-5 font-semibold text-white text-[32px] sm:text-[46px] lg:text-[54px] leading-[1.28] tracking-[-0.01em] break-keep whitespace-pre-line"
            style={step(1)}
          >
            {h.title.split("\n").map((l, i) => (
              <span key={i} className="block">{i === 1 ? <span className="neon-mark">{l}</span> : l}</span>
            ))}
          </h1>

          <p className="hero-in mt-5 font-thin text-[14px] sm:text-[15px] tracking-mega uppercase text-white/65" style={step(2)}>
            {h.subEn}
          </p>

          <p className="hero-in mt-6 text-[18px] sm:text-[22px] leading-[1.55] font-bold text-white break-keep whitespace-pre-line" style={step(3)}>
            {h.desc}
          </p>

          <div className="hero-in mt-10 grid grid-cols-2 sm:grid-cols-4 gap-5 sm:gap-6 border-t border-white/20 pt-7" style={step(4.2)}>
            {h.stats.map((s, i) => (
              <div key={i}>
                <p className="font-display text-[30px] sm:text-[34px] text-white leading-none">
                  <CountUpStat value={s.num} /><span className="text-[16px] sm:text-[17px] font-label ml-0.5 text-white/70">{s.unit}</span>
                </p>
                <p className="mt-2 text-[13px] sm:text-[13.5px] text-white/60 leading-snug break-keep [text-wrap:balance]">{fmt(s.label)}</p>
              </div>
            ))}
          </div>

          <div className="hero-in mt-9 flex flex-wrap gap-3" style={step(4.6)}>
            {C.branches[0].naverBooking ? (
              <a href={C.branches[0].naverBooking} target="_blank" rel="noreferrer noopener" className={btn}>
                네이버 예약
              </a>
            ) : null}
            {C.branches[0].kakao ? (
              <a href={C.branches[0].kakao} target="_blank" rel="noreferrer noopener" className={btn}>
                카카오톡 상담
              </a>
            ) : null}
            <a href={"tel:" + C.footer.phone.replace(/[^0-9+]/g, "")} className={btn}>
              <svg width="18" height="18" className="inline-block -mt-0.5 mr-1.5"><use href="#icon-phone" /></svg>
              전화 문의 {C.footer.phone}
            </a>
            <button
              onClick={() => go("facilities")}
              className="rounded-xl border border-white/35 px-7 py-4 text-[16px] text-white/85 hover:border-white hover:text-white transition-colors"
            >
              둘러보기 ↓
            </button>
          </div>
        </div>
      </div>
    </section>
  );
}

/* ── 2. 헬스장 시설 (공간 갤러리 + 갖춰진 시설을 한 섹션으로) ─────────────── */

// 사진을 2줄로 나누고, 줄마다 반대 방향으로 흘러가게 해서 "교차"되는 느낌을 낸다.
function GalleryRow({ images, duration, reverse }) {
  const imgs = images.concat(images); // 2배 복제 후 -50% 이동
  return (
    <div className="relative">
      <div
        className="flex gap-4 w-max"
        style={{ animation: `marquee ${duration}s linear infinite`, animationDirection: reverse ? "reverse" : "normal" }}
      >
        {imgs.map((src, i) => (
          <div key={i} className="group w-[200px] sm:w-[260px] aspect-[4/3] rounded-2xl overflow-hidden shrink-0">
            <img src={src} alt="" loading={LAZY} className="h-full w-full object-cover grayscale transition-[filter] duration-500 group-hover:grayscale-0 group-active:grayscale-0" />
          </div>
        ))}
      </div>
    </div>
  );
}

function Facilities() {
  const g = C.gallery;
  const f = C.facilities;
  // 2줄에 골고루 나눠 담는다(순서대로 자르지 않고 i%2으로 섞어서 줄마다 다른 사진이 보이게).
  const rows = [[], []];
  g.images.forEach((src, i) => rows[i % 2].push(src));
  return (
    <section id="facilities" className="bg-cream py-16 sm:py-24 overflow-hidden">
      <div className="mx-auto max-w-8xl px-5 sm:px-8">
        <SectionHead eyebrow={g.eyebrow} title={g.title} desc={g.desc} hl="공간" />
        <div className="mt-12 space-y-4 overflow-hidden">
          <GalleryRow images={rows[0]} duration={50} reverse={false} />
          <GalleryRow images={rows[1]} duration={60} reverse={true} />
        </div>

        {/* 26-09-30 업체 요청: 갖춰진 시설 제목·사진 2장 삭제, 공간 갤러리 바로 아래에 카드로 이어 붙임 */}
        <div className="mt-12 grid sm:grid-cols-3 gap-5">
          {f.equipment.map((e, i) => (
            <div key={i} className="reveal rounded-2xl bg-cream border border-accent/15 p-7" style={{ animationDelay: i * 100 + "ms" }}>
              <p className="font-display text-[15px] tracking-wider2 text-neon">{"0" + (i + 1)}</p>
              <p className="mt-3 text-[19px] font-bold text-accent">{e.name}</p>
              <p className="mt-2 text-[15.5px] text-ink/75 leading-snug">{e.desc}</p>
            </div>
          ))}
        </div>

        <div className="mt-5 grid grid-cols-2 sm:grid-cols-3 gap-3 sm:gap-5">
          {f.amenities.slice(0, 3).map((am, i) => (
            <div key={i} className="reveal rounded-2xl bg-ivory px-5 py-4" style={{ animationDelay: i * 80 + "ms" }}>
              <p className="flex items-center gap-2 text-[15.5px] sm:text-[16.5px] font-semibold text-ink">
                <svg width="16" height="16" className="text-accent shrink-0"><use href="#icon-check" /></svg>
                <span className="[text-wrap:balance]">{fmt(am.name)}</span>
              </p>
            </div>
          ))}
        </div>

        <div className="reveal mt-10 rounded-2xl bg-accent p-7 sm:p-8 grid sm:grid-cols-3 gap-5 text-[15.5px] text-white/85">
          {[
            { title: "주차 안내", items: ["외벽 주차 및 근처 공영주차장 사용가능"] },
            { title: "이용 안내", items: ["전국 모든 지점 동시 이용가능", "일일 입장 제한 없음"] },
            { title: "기타", items: ["휴대폰 앱으로 편리한 출입"] },
          ].map((b) => (
            <div key={b.title}>
              <p className="font-bold text-white text-[16.5px]">{b.title}</p>
              <ul className="mt-2 space-y-1">
                {b.items.map((t) => <li key={t}>*{t}</li>)}
              </ul>
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}

/* ── 3. 트레이너 ──────────────────────────────────────────────────────── */

function Trainers() {
  const t = C.trainers;
  return (
    <section id="trainers" className="bg-ivory py-16 sm:py-24">
      <div className="mx-auto max-w-8xl px-5 sm:px-8">
        <SectionHead eyebrow={t.eyebrow} title={t.title} desc={t.desc} />
        <div className="mt-14 grid min-[1360px]:grid-cols-2 gap-5 sm:gap-6">
          {t.list.map((tr, i) => (
            <div
              key={tr.id}
              className="reveal rounded-[28px] overflow-hidden bg-[#0B2B74] grid grid-cols-1 sm:grid-cols-2"
              style={{ animationDelay: i * 120 + "ms" }}
            >
              <div className="p-6 sm:p-8 flex flex-col justify-center text-cream order-2 sm:order-none">
                <p className="font-label text-[11px] tracking-mega uppercase text-accentSky">{tr.role}</p>
                <p className="mt-3 text-[17px] min-[400px]:text-[19px] sm:text-[22px] font-bold text-white leading-[1.4] break-keep">
                  <span className="block">안녕하세요,</span>
                  <span className="block">랩스휘트니스 독산2호점</span>
                  <span className="block">{tr.name} 트레이너입니다.</span>
                </p>
                {tr.education ? <p className="mt-4 text-[12.5px] text-cream/65 leading-snug [text-wrap:balance]">{fmt(tr.education)}</p> : null}
                <ul className="mt-4 space-y-1.5">
                  {tr.certs.map((c, ci) => (
                    <li key={ci} className="flex items-start gap-1.5 text-[13px] sm:text-[12.5px] text-cream/85 leading-snug">
                      <svg width="11" height="11" className="text-accentSky shrink-0 mt-1"><use href="#icon-check" /></svg>
                      <span>{fmt(c)}</span>
                    </li>
                  ))}
                </ul>
                {tr.awards ? (
                  <p className="mt-4 pt-4 border-t border-cream/20 text-[12px] text-cream/60 leading-snug [text-wrap:balance]">{fmt(tr.awards)}</p>
                ) : null}
              </div>
              {tr.image ? (
                // 네 장 원본 비율이 제각각이라, 칸 크기는 통일하되 사진은 잘리지 않게
                // object-contain으로 전신을 그대로 담는다(빈 공간은 남색으로 채운다).
                // 모바일은 사진을 위, 글을 아래로 쌓아 사진이 좁아지거나 잘리지 않게 한다.
                <div className="relative h-[420px] sm:h-auto sm:aspect-[3/4] bg-ink order-1 sm:order-none">
                  <img src={tr.image} alt={tr.name + " 트레이너"} loading={LAZY} className={"absolute inset-0 h-full w-full object-contain " + (tr.imgRight ? "object-right" : "")} />
                </div>
              ) : (
                <div className="aspect-[3/4] bg-ink/40 flex items-center justify-center">
                  <p className="text-[12px] text-cream/60 font-label tracking-wide text-center px-3">{t.photoPlaceholder}</p>
                </div>
              )}
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}

/* ── 4. 찾아오는 길 · 주차 ──────────────────────────────────────────────────
   흑백 사진을 어둡게 깔고, 정보는 흰 박스 + 남색 글자(업체 무드보드 2번).
   주소·운영시간·주차 문구는 기존 footer 값을 그대로 쓴다.
   ────────────────────────────────────────────────────────────────────────── */

function Location() {
  const l = C.location;
  const f = C.footer;
  const lines = [
    { label: "주소", value: f.addr },
    { label: "운영시간", value: f.hours },
    { label: "주차", value: f.parking },
    { label: "전화", value: f.phone },
  ];
  return (
    <section id="location" className="bg-ivory py-16 sm:py-24">
      <div className="mx-auto max-w-8xl px-5 sm:px-8">
        <SectionHead eyebrow={l.eyebrow} title={l.title} mark />

        <div className="mt-12 grid lg:grid-cols-[1.25fr_1fr] gap-5">
          {/* 26-09-30 업체 요청: 가시성 좋은 직접 그린 3D 지도 (tools/build-map.py 로 생성, OSM·Overture 데이터) */}
          <a
            href={"https://map.naver.com/p/search/" + encodeURIComponent(C.branches[0].mapQuery)}
            target="_blank" rel="noreferrer noopener"
            aria-label="랩스휘트니스 독산2호점 위치 — 네이버 지도에서 열기"
            className="reveal block rounded-2xl overflow-hidden bg-[#EDF0F5] min-h-[380px] lg:min-h-[440px] relative border border-accent/10"
          >
            <img
              src="img/map_doksan2.svg"
              alt="랩스휘트니스 독산2호점은 독산로 205, 해나온 마트 건너편입니다"
              loading="lazy"
              className="absolute inset-0 h-full w-full object-cover object-[48%_50%]"
            />
          </a>

          <div className="reveal rounded-2xl bg-white p-7 sm:p-9 flex flex-col">
            <p className="font-label text-[13px] tracking-mega uppercase text-accent/70">RAPS FITNESS · 독산2호점</p>
            <dl className="mt-6 space-y-4">
              {lines.map((x, i) => (
                <div key={i} className="grid grid-cols-[72px_1fr] gap-3 text-[16px] leading-[1.6]">
                  <dt className="text-accent/60">{x.label}</dt>
                  <dd className="font-semibold text-accent break-keep">{fmt(x.value)}</dd>
                </div>
              ))}
            </dl>
            <div className="mt-8 lg:mt-auto pt-2 grid grid-cols-2 gap-3">
              <a
                href={"https://map.naver.com/p/search/" + encodeURIComponent(C.branches[0].mapQuery)}
                target="_blank" rel="noreferrer noopener"
                className="rounded-xl bg-accent px-4 py-3.5 text-center text-[15.5px] font-semibold text-white hover:bg-accentDark transition-colors whitespace-nowrap"
              >
                네이버 지도
              </a>
              <a
                href={"tel:" + f.phone.replace(/[^0-9+]/g, "")}
                className="rounded-xl border border-accent/30 px-4 py-3.5 text-center text-[15.5px] font-semibold text-accent hover:border-accent transition-colors whitespace-nowrap"
              >
                전화 문의
              </a>
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}


/* ── 11. 푸터 ──────────────────────────────────────────────────────────── */

function Footer() {
  const f = C.footer;
  const lines = [
    { label: "주소", value: f.addr },
    { label: "전화", value: f.phone },
    { label: "운영시간", value: f.hours },
    { label: "주차", value: f.parking },
  ];

  return (
    <footer className="bg-accent text-cream/80 py-16 sm:py-20">
      <div className="mx-auto max-w-8xl px-5 sm:px-8">
        <div className="grid md:grid-cols-2 gap-10">
          <div>
            <img src={C.brand.logoDark} alt={C.brand.name} style={{ height: 30 }} className="w-auto" />
            <p className="mt-3 font-label text-[12px] tracking-mega text-tint/60">{f.company}</p>
            <p className="mt-6 text-[16px] leading-[1.8] text-cream/65 max-w-md">
              문화비 소득공제 대상 시설입니다.
            </p>
          </div>

          <div className="md:justify-self-end">
            <dl className="space-y-2.5">
              {lines.map((l, i) => (
                <div key={i} className="flex gap-4 text-[16.5px]">
                  <dt className="w-20 shrink-0 text-cream/55">{l.label}</dt>
                  <dd className="text-cream/85">{fmt(l.value)}</dd>
                </div>
              ))}
            </dl>
          </div>
        </div>

        <div className="mt-14 pt-7 border-t border-cream/10 flex flex-col sm:flex-row justify-between gap-3">
          <p className="font-label text-[13px] tracking-wider2 text-cream/55">© 2026 RAPS FITNESS 독산2호점</p>
          <button
            onClick={() => window.scrollTo({ top: 0, behavior: "smooth" })}
            className="text-left font-label text-[13px] tracking-wider2 text-cream/55 hover:text-cream/80 transition-colors"
          >
            맨 위로 ↑
          </button>
        </div>
      </div>
    </footer>
  );
}

/* ── 12. 칼럼(저널) ───────────────────────────────────────────────────────
   데이터는 src/columns.js 에 있다. 칼럼봇(~/automation/raps-doksan2-column)이 매일 갱신한다.
   ────────────────────────────────────────────────────────────────────────── */

const J = window.COLUMNS || { meta: {}, categories: [], photoPools: {}, posts: [] };

// "2026.09.17" → 오늘 날짜와 비교할 수 있는 숫자
function dateNum(str) {
  const m = String(str || "").match(/(\d{4})\D(\d{1,2})\D(\d{1,2})/);
  if (!m) return 0;
  return Number(m[1]) * 10000 + Number(m[2]) * 100 + Number(m[3]);
}
function todayNum() {
  const d = new Date();
  return d.getFullYear() * 10000 + (d.getMonth() + 1) * 100 + d.getDate();
}

// 화면에 내보낼 글: 초안 제외, 발행일이 오늘 이후인 글 제외(예약 발행), 최신순
function publishedPosts() {
  const t = todayNum();
  return (J.posts || [])
    .filter((p) => !p.draft && dateNum(p.date) <= t)
    .sort((a, b) => dateNum(b.date) - dateNum(a.date));
}

function findPost(id) {
  return (J.posts || []).find((p) => p.id === id);
}

// image 가 "auto" 면 카테고리 사진 중에서 고른다. id 를 기준으로 고르므로 항상 같은 사진이 나온다.
function postImage(p) {
  if (p.image && p.image !== "auto") return p.image;
  const pool = (J.photoPools || {})[p.cat] || (J.photoPools || {})._default || [];
  if (!pool.length) return "img/천국의 계단.jpg";
  // id 해시로 바로 고르면 같은 분류 글끼리 우연히 같은 사진을 뽑는 경우가 잦다.
  // 같은 분류 안에서 몇 번째 글인지로 순서를 매겨 풀을 돌아가며 쓰게 한다.
  const sameCat = (J.posts || []).filter((x) => x.cat === p.cat);
  const idx = sameCat.findIndex((x) => x.id === p.id);
  return pool[(idx < 0 ? 0 : idx) % pool.length];
}

/* 본문 중간에 자동으로 들어가는 사진.
   카테고리 사진 묶음이 세 장씩이라, 대표 사진 1장 + 본문 2장 = 한 편에 3장이 된다. */
function bodyPhotos(p, count = 2) {
  const pool = (J.photoPools || {})[p.cat] || (J.photoPools || {})._default || [];
  const hero = postImage(p);
  const rest = pool.filter((src) => src !== hero);
  const out = [];
  for (let i = 0; i < count && rest.length; i++) out.push(rest[i % rest.length]);
  return out;
}

/* 사진 자리 고르기 — 소제목 바로 앞에 넣는다. 소제목이 모자라면 그냥 그 자리 문단 사이에 넣는다. */
function photoSlots(body, count) {
  const n = body.length;
  const heads = [];
  body.forEach((b, i) => {
    if (i > 0 && b && typeof b === "object" && b.h2) heads.push(i);
  });
  const slots = [];
  for (let k = 1; k <= count; k++) {
    const target = Math.round((n * k) / (count + 1));
    let at = heads.find((h) => h >= target && slots.indexOf(h) < 0);
    if (at === undefined) at = heads.filter((h) => slots.indexOf(h) < 0).pop();
    if (at === undefined) at = slots.indexOf(target) < 0 ? target : target + 1;
    if (at > 0 && at < n) slots.push(at);
  }
  return slots;
}

// 흑백 사진을 누르면 컬러, 다시 누르면 흑백 (26-09-30 업체 요청)
function ColorImg({ src, alt, className, loading }) {
  const [on, setOn] = useState(false);
  return (
    <img
      src={src} alt={alt} loading={loading}
      onClick={() => setOn(!on)}
      className={className + " cursor-pointer transition-[filter] duration-500 " + (on ? "" : "grayscale")}
    />
  );
}

function BodyPhoto({ src }) {
  return (
    <figure className="mt-14 mb-2 -mx-5 sm:mx-0">
      <ColorImg src={src} alt="랩스휘트니스 독산2호점 내부" loading={LAZY} className="w-full h-[240px] sm:h-[420px] object-cover sm:rounded-sm" />
    </figure>
  );
}

/* noPhoto: 사진 없이 글자만 — 「함께 읽으면 좋은 글」 자리에서 쓴다(사진이 너무 많아 보이지 않게). */
function ColumnCard({ p, noPhoto = false }) {
  return (
    <a
      href={"#/column/" + p.id}
      className="reveal group block overflow-hidden rounded-2xl bg-cream border border-ink/10 transition-transform duration-500 hover:-translate-y-1"
    >
      {noPhoto ? null : (
        <div className="overflow-hidden h-[200px]">
          <img src={postImage(p)} alt={p.title} loading={LAZY} className="h-full w-full object-cover grayscale transition duration-[1200ms] group-hover:scale-[1.06] group-hover:grayscale-0 group-active:grayscale-0" />
        </div>
      )}
      <div className="p-7">
        <p className="font-label text-[11.5px] tracking-[0.2em] uppercase text-accent">{p.cat}</p>
        <h3 className="mt-3 text-[19px] sm:text-[20px] font-bold text-ink leading-snug">{p.title}</h3>
        <p className="mt-3 text-[15.5px] leading-[1.8] text-ink/82">{p.excerpt}</p>
        <p className="mt-5 text-[14px] text-ink/60">{p.date}</p>
      </div>
    </a>
  );
}

/* 홈에 붙는 최신 3개 미리보기 — 글이 없으면 통째로 숨긴다 */
function JournalPreview() {
  const list = publishedPosts().slice(0, 3);
  if (!list.length) return null;
  return (
    <section className="bg-ivory bg-techgrid py-16 sm:py-24">
      <div className="mx-auto max-w-8xl px-5 sm:px-8">
        <SectionHead eyebrow={J.meta.eyebrow} title={J.meta.title} desc={J.meta.subtitle} hl="칼럼" />
        <div className="mt-14 grid sm:grid-cols-3 gap-6">
          {list.map((p) => <ColumnCard key={p.id} p={p} />)}
        </div>
        <div className="mt-10 text-center">
          <a href="#/column" className="inline-block rounded-full border border-ink/25 px-8 py-4 text-[16px] text-ink/92 hover:border-accent hover:text-accent transition-colors">
            칼럼 전체 보기 →
          </a>
        </div>
      </div>
    </section>
  );
}

/* 목록 페이지 */
function ColumnList() {
  const [cat, setCat] = useState("전체");
  const all = publishedPosts();
  const cats = ["전체"].concat((J.categories || []).filter((c) => all.some((p) => p.cat === c)));
  const list = cat === "전체" ? all : all.filter((p) => p.cat === cat);

  return (
    <main className="bg-cream pt-32 sm:pt-40 pb-24 sm:pb-32 min-h-[70svh]">
      <div className="mx-auto max-w-8xl px-5 sm:px-8">
        <SectionHead eyebrow={J.meta.eyebrow} title={J.meta.title} desc={J.meta.subtitle} hl="칼럼" />

        {all.length === 0 ? (
          <p className="mt-16 text-center text-[16px] leading-[1.9] text-ink/75">
            아직 발행된 글이 없습니다.
            <br />첫 글이 올라오면 이곳에 표시됩니다.
          </p>
        ) : (
          <React.Fragment>
            {cats.length > 2 ? (
              <div className="mt-12 flex flex-wrap justify-center gap-2.5">
                {cats.map((c) => (
                  <button
                    key={c}
                    onClick={() => setCat(c)}
                    className={"rounded-full px-5 py-2.5 text-[15px] transition-colors " + (c === cat ? "bg-ink text-cream" : "border border-ink/20 text-ink/82 hover:border-accent hover:text-accent")}
                  >
                    {c}
                  </button>
                ))}
              </div>
            ) : null}

            <div className="mt-12 sm:mt-16 grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
              {list.map((p) => <ColumnCard key={p.id} p={p} />)}
            </div>
          </React.Fragment>
        )}
      </div>
    </main>
  );
}

/* 글 한 편 */
function ColumnPost({ id }) {
  const p = findPost(id);

  if (!p || p.draft) {
    return (
      <main className="bg-cream pt-36 sm:pt-44 pb-32 min-h-[70svh]">
        <div className="mx-auto max-w-2xl px-5 text-center">
          <h1 className="text-[32px] font-bold text-ink">글을 찾을 수 없습니다</h1>
          <p className="mt-5 text-[16px] text-ink/78">주소가 바뀌었거나 아직 공개되지 않은 글입니다.</p>
          <a href="#/column" className="mt-9 inline-block rounded-full bg-ink px-7 py-3.5 text-[16px] text-cream hover:brightness-110 transition-colors">
            칼럼 목록으로
          </a>
        </div>
      </main>
    );
  }

  const related = (p.related || []).map(findPost).filter((x) => x && !x.draft);
  const photos = bodyPhotos(p);
  const slots = photoSlots(p.body || [], photos.length);

  return (
    <main className="bg-cream pb-24 sm:pb-32 pt-[68px] sm:pt-[76px]">
      <div className="relative h-[46svh] min-h-[300px] overflow-hidden">
        <ColorImg src={postImage(p)} alt={p.title} className="h-full w-full object-cover" />
        <div className="absolute inset-0 pointer-events-none" style={{ background: "linear-gradient(to top, rgba(14,14,16,.92) 0%, rgba(14,14,16,.55) 40%, rgba(14,14,16,.22) 100%)" }} />
        <div className="absolute inset-x-0 bottom-0">
          <div className="mx-auto max-w-3xl px-5 sm:px-8 pb-10 sm:pb-14">
            <p className="font-label text-[11.5px] tracking-[0.24em] uppercase text-tint/80">{p.cat}</p>
            <h1 className="mt-4 text-[27px] sm:text-[38px] font-bold text-cream leading-[1.3] break-keep">{p.title}</h1>
            <p className="mt-4 text-[14.5px] text-cream/70">{p.date}{p.updated ? " · " + p.updated + " 수정" : ""}</p>
          </div>
        </div>
      </div>

      <article className="mx-auto max-w-3xl px-5 sm:px-8">
        <p className="mt-12 text-[17px] sm:text-[18.5px] leading-[1.9] text-ink/92 break-keep">{p.excerpt}</p>
        <div className="mt-8 h-px w-14 bg-accent/50" />

        <div className="mt-10">
          {(p.body || []).map((b, i) => {
            const slot = slots.indexOf(i);
            return (
              <React.Fragment key={i}>
                {slot >= 0 && photos[slot] ? <BodyPhoto src={photos[slot]} /> : null}
                {typeof b === "string" ? (
                  <p className="mt-6 text-[16px] sm:text-[17px] leading-[2.0] text-ink/90 break-keep">{b}</p>
                ) : (
                  <h2 className="mt-14 mb-1 text-[21px] sm:text-[24px] font-bold text-ink leading-snug break-keep">{b.h2}</h2>
                )}
              </React.Fragment>
            );
          })}
        </div>

        <div className="mt-20 rounded-2xl bg-ink px-8 py-10 sm:px-12 sm:py-12 text-center">
          <p className="font-label text-[11px] tracking-[0.2em] uppercase text-cream/60">RAPS FITNESS 독산2호점</p>
          <p className="mt-5 text-[19px] sm:text-[22px] leading-[1.6] text-cream break-keep">
            궁금한 점은 편하게 문의해 주세요.
            <br />전화 상담 · 방문 상담 모두 가능합니다.
          </p>
          <a
            href={"tel:" + C.footer.phone.replace(/[^0-9+]/g, "")}
            className="mt-8 inline-block rounded-full bg-accent px-8 py-4 text-[16px] font-semibold text-white hover:bg-accentDark transition-colors"
          >
            전화 문의 {C.footer.phone}
          </a>
        </div>

        {related.length ? (
          <div className="mt-20">
            <p className="font-label text-[11.5px] tracking-[0.2em] uppercase text-accent">RELATED</p>
            <h2 className="mt-3 text-[22px] font-bold text-ink">함께 읽으면 좋은 글</h2>
            <div className="mt-7 grid sm:grid-cols-2 gap-5">
              {related.map((r) => <ColumnCard key={r.id} p={r} noPhoto />)}
            </div>
          </div>
        ) : null}

        <div className="mt-16 text-center">
          <a href="#/column" className="inline-block rounded-full border border-ink/25 px-8 py-4 text-[16px] text-ink/92 hover:border-accent hover:text-accent transition-colors">
            ← 칼럼 목록으로
          </a>
        </div>
      </article>
    </main>
  );
}

/* ── 화면 전환 ─────────────────────────────────────────────────────────────
   주소의 # 뒤만 보고 화면을 고른다. 서버 설정이 필요 없어 정적 호스팅에서 그대로 돈다.
     (없음)              홈
     #/column            칼럼 목록
     #/column/<글id>     글 한 편
   ────────────────────────────────────────────────────────────────────────── */

function parseHash() {
  const h = (window.location.hash || "").replace(/^#\/?/, "");
  if (h === "column") return { name: "list" };
  if (h.indexOf("column/") === 0) return { name: "post", id: h.slice("column/".length) };
  return { name: "home" };
}

/* ── 떠 있는 전화 버튼 (모바일) ────────────────────────────────────────── */

function FloatingCta() {
  return (
    <a
      href={"tel:" + C.footer.phone.replace(/[^0-9+]/g, "")}
      className="lg:hidden fixed bottom-5 right-5 z-40 flex items-center gap-2 rounded-full bg-accent text-white px-5 py-3.5 shadow-xl shadow-accent/30"
    >
      <svg width="18" height="18"><use href="#icon-phone" /></svg>
      <span className="text-[15px] font-semibold">전화 문의</span>
    </a>
  );
}

/* ── 앱 루트 ───────────────────────────────────────────────────────────── */

function App() {
  const [route, setRoute] = useState(parseHash());
  useReveal();

  useEffect(() => {
    const onHash = () => {
      setRoute(parseHash());
      window.scrollTo({ top: 0, behavior: "auto" });
    };
    window.addEventListener("hashchange", onHash);
    return () => window.removeEventListener("hashchange", onHash);
  }, []);

  if (route.name === "list") {
    return (
      <React.Fragment>
        <Header alwaysSolid />
        <ColumnList />
        <Footer />
        <FloatingCta />
      </React.Fragment>
    );
  }

  if (route.name === "post") {
    return (
      <React.Fragment>
        <Header alwaysSolid />
        <ColumnPost id={route.id} />
        <Footer />
        <FloatingCta />
      </React.Fragment>
    );
  }

  return (
    <React.Fragment>
      <Header />
      <Hero />
      <Facilities />
      <Trainers />
      <Location />
      <JournalPreview />
      <Footer />
      <FloatingCta />
    </React.Fragment>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
