// 3B Printing homepage — composes design-system components only.
const DS = window.Ds3BPrintingDesignSystem_6b9cdf;
const { HeroFull, TrustStrip, GalleryTile, Lightbox, TierCard, BlogCard } = DS;
const { LINE_HREF, t, Header, SectionTitle, SiteFooter } = window.Shared;

// Hostinger-style scroll reveal: the element starts slightly scaled-down and faded, then
// eases to full size/opacity the first time it scrolls into view (IntersectionObserver, not a
// scroll listener). A callback ref is used so it re-arms when the node actually mounts. Shown
// immediately (no animation) in the builder (EDIT_MODE) or where IntersectionObserver is
// unsupported, so content can never stay stuck invisible.
function Reveal({ children }) {
  const [node, setNode] = React.useState(null);
  const [shown, setShown] = React.useState(false);
  React.useEffect(() => {
    if (!node) return;
    if (window.EDIT_MODE || !('IntersectionObserver' in window)) { setShown(true); return; }
    const io = new IntersectionObserver((entries) => {
      if (entries.some((e) => e.isIntersecting)) { setShown(true); io.disconnect(); }
    }, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' });
    io.observe(node);
    return () => io.disconnect();
  }, [node]);
  return (
    <div ref={setNode} style={{
      opacity: shown ? 1 : 0,
      transform: shown ? 'none' : 'scale(0.95) translateY(24px)',
      transition: 'opacity 0.7s var(--ease-standard), transform 0.7s var(--ease-standard)',
      willChange: 'opacity, transform',
    }}>
      {children}
    </div>
  );
}

function PaperSwatch({ paper, lang }) {
  const [hover, setHover] = React.useState(false);
  // Up to 3 photos per swatch — hover jumps to the secondary image, then cycles every 2s.
  const frames = (paper.images && paper.images.length > 0) ? paper.images : null;
  const [slide, setSlide] = React.useState(0);
  const framesLen = frames ? frames.length : 0;
  React.useEffect(() => {
    if (!hover || framesLen < 2) { setSlide(0); return; }
    setSlide(1 % framesLen);
    const id = setInterval(() => setSlide((s) => (s + 1) % framesLen), 2000);
    return () => clearInterval(id);
  }, [hover, framesLen]);
  // GSM may hold several values (e.g. front/back stock) separated by "|".
  const gsmList = String(paper.gsm == null ? '' : paper.gsm).split('|').map((s) => s.trim()).filter(Boolean);

  return (
    <div
      onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
      style={{
        background: 'var(--surface-card)', border: '1px solid var(--line)',
        borderRadius: 'var(--radius-card)', overflow: 'hidden', fontFamily: 'var(--font-body)',
        boxShadow: hover ? 'var(--shadow-lift)' : 'none',
        transform: hover ? 'translateY(-2px)' : 'none',
        transition: 'all var(--duration-standard) var(--ease-standard)',
        // Fill the (stretched) track slot so a 2-line title doesn't make just that card taller —
        // the photo stays a fixed 200px and the text block absorbs the leftover space instead.
        height: '100%', display: 'flex', flexDirection: 'column',
      }}>
      <div style={{ position: 'relative', height: 200, flexShrink: 0, background: paper.tone, borderBottom: '1px solid var(--line)', overflow: 'hidden' }}>
        {frames && frames.map((f, i) => (
          <img key={i} src={f} alt={lang === 'th' ? paper.nameTh : paper.nameEn} style={{
            position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover',
            objectPosition: paper.imagePosition || '50% 50%',
            transform: `scale(${(paper.imageZoom || 1) * (hover ? 1.08 : 1)})`,
            opacity: i === slide ? 1 : 0,
            transition: 'opacity 0.6s var(--ease-standard), transform 0.5s var(--ease-standard)',
          }} />
        ))}
        {gsmList.length > 0 && (
          <span style={{
            position: 'absolute', top: 'var(--space-3)', right: 'var(--space-3)',
            display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 2,
            fontSize: 11, letterSpacing: 'var(--tracking-label)', textTransform: 'uppercase',
            color: frames ? 'var(--ink)' : (paper.dark ? 'var(--on-dark-soft)' : 'var(--ink-faint)'),
            background: frames ? 'rgba(250,249,247,0.88)' : 'transparent',
            backdropFilter: frames ? 'var(--chrome-blur-soft, blur(8px))' : undefined,
            WebkitBackdropFilter: frames ? 'var(--chrome-blur-soft, blur(8px))' : undefined,
            padding: frames ? '3px 8px' : 0, borderRadius: frames ? 'var(--radius-ui)' : 0,
          }}>
            {gsmList.map((g, i) => <span key={i}>{g} {t[lang].paperWeight}</span>)}
          </span>
        )}
      </div>
      <div style={{ padding: 'var(--space-4)', flex: 1, display: 'flex', flexDirection: 'column', gap: 'var(--space-1)' }}>
        <span style={{
          fontFamily: lang === 'th' ? 'var(--font-heading-thai)' : 'var(--font-heading)',
          fontSize: 20, fontWeight: lang === 'th' ? 'var(--weight-heading-thai)' : 600, color: 'var(--ink)',
        }}>{lang === 'th' ? paper.nameTh : paper.nameEn}</span>
        <span style={{ fontSize: 'var(--text-caption)', lineHeight: 'var(--leading-caption)', color: 'var(--ink-faint)' }}>
          {lang === 'th' ? paper.textureTh : paper.textureEn}
        </span>
      </div>
    </div>
  );
}

function cellText(val, lang) {
  if (val === true || val === false) return val;
  if (val && typeof val === 'object') return lang === 'th' ? val.th : (val.en || val.th);
  return val;
}

function BusinessCardTable({ lang, columns, rows }) {
  const headerH = 68;
  const headerBg = 'var(--bg-base)';
  const headerFg = '#FFFFFF';
  const bodyFg = 'var(--ink)';
  const tick = '✓';

  return (
    <div className="compare-table-scroll" style={{ overflowX: 'auto', borderRadius: 'var(--radius-card)' }}>
      {/* minWidth forces the grid past the viewport on mobile so the wrapper's overflowX actually
          engages. Without it the 1fr columns squeeze to min-content, wrapping every cell to one
          word per line and still clipping the last tier — a scroll, not a squeeze, is the fix. */}
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr 1fr', minWidth: 680 }}>
        {/* Header */}
        <div style={{ height: headerH, background: 'var(--bg-light)' }}></div>
        {columns.map((tier, i) => (
          <div key={i} style={{
            height: 35, background: headerBg, display: 'flex', alignItems: 'center', justifyContent: 'center',
            padding: 'var(--space-3) var(--space-4)', fontSize: 22, fontWeight: 600, color: headerFg,
            fontFamily: 'var(--font-heading)', border: '1px solid #FFFFFF',
          }}>
            {tier}
          </div>
        ))}

        {/* Rows */}
        {rows.map((row, i) => (
          <React.Fragment key={i}>
            <div style={{
              background: 'transparent', textAlign: 'center',
              padding: 'var(--space-4) var(--space-3)',
            }}>
              <div style={{ fontFamily: 'var(--font-heading)', fontSize: 20, fontWeight: 600, lineHeight: 1.3, color: bodyFg }}>{row.en}</div>
              {/* Always Noto Sans, not --font-heading-thai — that spot is a typography-panel
                  override meant for Thai headings, and this is a plain caption, not a heading. */}
              <div style={{ fontFamily: "'Noto Sans Thai', sans-serif", fontSize: 15, lineHeight: 1.3, color: 'var(--ink-faint)', marginTop: 6, opacity: 0.8 }}>({row.th})</div>
            </div>
            {['std', 'prem', 'lux'].map((tier, j) => {
              const val = cellText(row[tier], lang);
              return (
                <div key={j} style={{
                  background: 'transparent', display: 'flex', alignItems: 'center', justifyContent: 'center',
                  padding: 'var(--space-4) var(--space-3)', fontSize: val === false ? 25 : 18,
                  color: val === false ? '#000000' : bodyFg,
                  fontWeight: val === true ? 600 : 400, textAlign: 'center', whiteSpace: 'pre-line',
                  // Value follows the active language: Noto Sans for Thai text, the site's
                  // Latin heading font (matching the tier headers above) for English.
                  fontFamily: lang === 'th' ? "'Noto Sans Thai', sans-serif" : 'var(--font-heading)', lineHeight: 1.4,
                }}>
                  {val === true ? tick : val === false ? '−' : val}
                </div>
              );
            })}
          </React.Fragment>
        ))}
      </div>
    </div>
  );
}

// Shared by TierCarousel/PaperCarousel: lets < > arrows step a continuously CSS-animated
// marquee by one screen-width of cards WITHOUT stopping the drift — clicking doesn't switch to
// a different "mode", it just nudges the same animation forward/back.
//
// Mechanism: on click, read the track's CURRENT on-screen translateX (via getComputedStyle,
// works whether the animation is running live or force-paused in the builder), freeze it there
// with a direct, synchronous DOM mutation + a forced reflow (not requestAnimationFrame — a
// backgrounded tab can pause rAF indefinitely, but a synchronous style read/write always
// commits), then hand off to a plain CSS transition that animates to the target position. When
// that transition ends, the keyframe animation resumes with animation-delay set to the
// equivalent negative offset — so it continues from exactly that point, with no visible seam.
// Works identically in the live site and the builder (EDIT_MODE just keeps it paused at rest;
// the step transition itself never checks EDIT_MODE).
function useMarqueeStep(trackRef, containerRef, duration) {
  const [stepping, setStepping] = React.useState(false);
  const [stepTarget, setStepTarget] = React.useState(0);
  const [resumeDelay, setResumeDelay] = React.useState(0);

  const go = (dir) => {
    const track = trackRef.current, container = containerRef.current;
    if (!track || !container || stepping) return;
    const oneCopyWidth = track.scrollWidth / 2;   // track holds the item list duplicated once
    const stepPx = container.clientWidth;          // "one screen" of cards, responsive
    const cssTransform = getComputedStyle(track).transform;
    const m = new DOMMatrixReadOnly(cssTransform === 'none' ? 'matrix(1,0,0,1,0,0)' : cssTransform);
    const currentX = m.m41;

    // Freeze at the exact live position — synchronous mutation + forced reflow, so the browser
    // commits this frame before any state-driven re-render changes the style again.
    track.style.transition = 'none';
    track.style.animation = 'none';
    track.style.transform = `translateX(${currentX}px)`;
    void track.offsetWidth;

    let targetX = currentX - dir * stepPx;
    while (targetX <= -oneCopyWidth) targetX += oneCopyWidth;
    while (targetX > 0) targetX -= oneCopyWidth;

    setStepTarget(targetX);
    setStepping(true);
  };

  const onTransitionEnd = (e) => {
    if (e.propertyName !== 'transform' || !stepping) return;
    const oneCopyWidth = trackRef.current.scrollWidth / 2;
    const fraction = (-stepTarget) / oneCopyWidth;   // 0..1 progress through one loop cycle
    setResumeDelay(-(fraction * duration));
    setStepping(false);
  };

  return { stepping, stepTarget, resumeDelay, go, onTransitionEnd };
}

function MarqueeArrows({ onPrev, onNext }) {
  const arrowStyle = (side) => ({
    position: 'absolute', top: '50%', [side]: 'var(--space-4)', transform: 'translateY(-50%)',
    width: 44, height: 44, borderRadius: '50%', border: '1px solid var(--line)',
    background: 'var(--paper)', display: 'flex', alignItems: 'center', justifyContent: 'center',
    cursor: 'pointer', boxShadow: 'var(--shadow-lift)', zIndex: 5,
    color: 'var(--ink)', fontSize: 18, fontFamily: 'var(--font-body)',
  });
  return (
    <React.Fragment>
      <button type="button" aria-label="Previous" onClick={onPrev} style={arrowStyle('left')}>‹</button>
      <button type="button" aria-label="Next" onClick={onNext} style={arrowStyle('right')}>›</button>
    </React.Fragment>
  );
}

// "Roulette" marquee — continuous CSS-animated loop, plus < > arrows (see useMarqueeStep above)
// that step by one screen-width of cards without interrupting the drift. The track is the tier
// list duplicated once; translateX(-50%) always lands exactly on the identical second copy
// regardless of card count/width, so the loop point is invisible.
// A single `paused` boolean (hover or touch) drives animationPlayState — same on/off switch
// for mouse and touch, matching the pause-then-resume behavior used elsewhere on the site.
function TierCarousel({ lang, tiers }) {
  const [paused, setPaused] = React.useState(false);
  const loop = React.useMemo(() => [...tiers, ...tiers], [tiers]);
  const duration = Math.max(20, tiers.length * 6);
  const trackRef = React.useRef(null);
  const containerRef = React.useRef(null);
  const { stepping, stepTarget, resumeDelay, go, onTransitionEnd } = useMarqueeStep(trackRef, containerRef, duration);

  return (
    <div>
      {/* Full-bleed break-out: TierCarousel's parent <section> is centered at max-width:1120px
          like every other section, but the marquee itself should extend to the true viewport
          edges. 100vw + a negative margin equal to half the viewport minus half this element's
          own (centered) width re-anchors it to the browser edges regardless of the ancestor's
          width — the classic CSS full-bleed trick. */}
      <div style={{ width: '100vw', marginLeft: 'calc(-50vw + 50%)' }}>
        <div
          ref={containerRef}
          onMouseEnter={() => setPaused(true)} onMouseLeave={() => setPaused(false)}
          onTouchStart={() => setPaused(true)} onTouchEnd={() => setPaused(false)}
          style={{
            overflow: 'hidden', position: 'relative',
            maskImage: 'linear-gradient(to right, transparent, black 12%, black 88%, transparent)',
            WebkitMaskImage: 'linear-gradient(to right, transparent, black 12%, black 88%, transparent)',
          }}>
          <div
            ref={trackRef}
            onTransitionEnd={onTransitionEnd}
            style={stepping ? {
              display: 'flex', gap: 'var(--space-5)', width: 'max-content',
              transform: `translateX(${stepTarget}px)`,
              transition: 'transform 0.6s cubic-bezier(0.4, 0, 0.2, 1)',
            } : {
              display: 'flex', gap: 'var(--space-5)', width: 'max-content',
              animation: `tier-marquee ${duration}s linear infinite`,
              animationDelay: `${resumeDelay}s`,
              animationPlayState: (paused || window.EDIT_MODE) ? 'paused' : 'running',
            }}>
            {loop.map((item, i) => (
              <div key={`${item.id}-${i}`} style={{ width: 'var(--tier-card-w)', flexShrink: 0 }}>
                <window.EditItem table="tiers" id={item.id}>
                  <TierCard lang={lang} name={item.name} nameEn={item.nameEn} fromPrice={lang === 'th' ? item.fromPriceTh : item.fromPriceEn} href={item.href} src={item.src} images={item.images} placeholders={item.placeholders} tone={item.tone} imagePosition={item.imagePosition} imageZoom={item.imageZoom} cta={lang === 'th' ? item.ctaTh : item.ctaEn} />
                </window.EditItem>
              </div>
            ))}
          </div>
          <MarqueeArrows onPrev={() => go(-1)} onNext={() => go(1)} />
        </div>
      </div>
      <div style={{ display: 'flex', justifyContent: 'center', marginTop: 'var(--space-5)' }}>
        <a href={window.Shared.url('/gallery')} className="see-more-link" style={{
          display: 'inline-flex', alignItems: 'center', gap: 8, textDecoration: 'none',
          fontFamily: 'var(--font-body)', fontSize: 14, color: 'var(--ink-faint)',
        }}>
          {lang === 'th' ? 'ดูทั้งหมด' : 'See more'} <span className="see-more-arrow">→</span>
        </a>
      </div>
    </div>
  );
}

function PaperCarousel({ lang, papers: items }) {
  const [paused, setPaused] = React.useState(false);
  const loop = React.useMemo(() => [...items, ...items], [items]);
  const duration = Math.max(20, items.length * 6);
  const trackRef = React.useRef(null);
  const containerRef = React.useRef(null);
  const { stepping, stepTarget, resumeDelay, go, onTransitionEnd } = useMarqueeStep(trackRef, containerRef, duration);

  return (
    <div>
      {/* Full-bleed break-out — see the identical comment on TierCarousel below. */}
      <div style={{ width: '100vw', marginLeft: 'calc(-50vw + 50%)' }}>
        <div
          ref={containerRef}
          onMouseEnter={() => setPaused(true)} onMouseLeave={() => setPaused(false)}
          onTouchStart={() => setPaused(true)} onTouchEnd={() => setPaused(false)}
          style={{
            overflow: 'hidden', position: 'relative',
            maskImage: 'linear-gradient(to right, transparent, black 12%, black 88%, transparent)',
            WebkitMaskImage: 'linear-gradient(to right, transparent, black 12%, black 88%, transparent)',
          }}>
          <div
            ref={trackRef}
            onTransitionEnd={onTransitionEnd}
            style={stepping ? {
              display: 'flex', gap: 'var(--space-5)', width: 'max-content',
              transform: `translateX(${stepTarget}px)`,
              transition: 'transform 0.6s cubic-bezier(0.4, 0, 0.2, 1)',
            } : {
              display: 'flex', gap: 'var(--space-5)', width: 'max-content',
              animation: `tier-marquee ${duration}s linear infinite`,
              animationDelay: `${resumeDelay}s`,
              animationPlayState: (paused || window.EDIT_MODE) ? 'paused' : 'running',
            }}>
            {loop.map((item, i) => (
              <div key={`${item.id}-${i}`} style={{ width: 'var(--tier-card-w)', flexShrink: 0 }}>
                <window.EditItem table="papers" id={item.id}>
                  <PaperSwatch paper={item} lang={lang} />
                </window.EditItem>
              </div>
            ))}
          </div>
          <MarqueeArrows onPrev={() => go(-1)} onNext={() => go(1)} />
        </div>
      </div>
      <div style={{ display: 'flex', justifyContent: 'center', marginTop: 'var(--space-5)' }}>
        <a href={window.Shared.url('/papers')} className="see-more-link" style={{
          display: 'inline-flex', alignItems: 'center', gap: 8, textDecoration: 'none',
          fontFamily: 'var(--font-body)', fontSize: 14, color: 'var(--ink-faint)',
        }}>
          {lang === 'th' ? 'ดูทั้งหมด' : 'See more'} <span className="see-more-arrow">→</span>
        </a>
      </div>
    </div>
  );
}

// Full-bleed gallery — an infinite, center-focused carousel of "sets". Each set is one complete
// 3×2 grid (6 cards). The active set sits centered and fully opaque; every other set (including
// the ones peeking at the edges) is dimmed. Left/right arrows page one whole set at a time and
// LOOP: past the last set the first comes back around, and the last set sits to the left of the
// first. Transform-based (not native scroll) because scroll can't loop or dim by center.
//
// Loop mechanic: the set list is rendered THREE times in a row; `active` lives in the middle
// copy, and after each slide settles we silently re-center by ±one copy if it drifted out — the
// destination set is visually identical, so the seam is invisible (classic infinite-carousel trick).
//
// Each tile is a DISTINCT real gallery item — no duplication. Items (those given a Home position
// in the CMS) are chunked sequentially into one set per screen (3×2 desktop, 2×2 mobile — see
// useIsMobile below), so paging always reveals different work and every tile maps 1:1 to its own
// `gallery_items` row (clicking it in edit mode selects exactly that row in the Inspector). To add
// more work, give more items a Home position in the CMS; the final set may be partial until the
// count is a multiple of the set size — that's expected, not padded.
function buildGallerySets(items, setSize) {
  const base = items || [];
  if (!base.length) return [];
  const sets = [];
  for (let start = 0; start < base.length; start += setSize) {
    const set = [];
    for (let k = 0; k < setSize && start + k < base.length; k++) {
      const realIndex = start + k;
      set.push({ item: base[realIndex], realIndex, key: `cell-${realIndex}` });
    }
    sets.push(set);
  }
  return sets;
}

// Matches styles.css's `@media (max-width: 768px)` breakpoint, which is where the gallery grid
// itself switches to 2 columns (.gallery-set rule) — the JS chunk size has to track the same
// breakpoint or the sets would paginate on a column count the grid isn't actually using.
function useIsMobile() {
  const [isMobile, setIsMobile] = React.useState(() => window.matchMedia('(max-width: 768px)').matches);
  React.useEffect(() => {
    const mq = window.matchMedia('(max-width: 768px)');
    const onChange = () => setIsMobile(mq.matches);
    mq.addEventListener('change', onChange);
    return () => mq.removeEventListener('change', onChange);
  }, []);
  return isMobile;
}

function GalleryStrip({ items, lang, onSelect }) {
  const isMobile = useIsMobile();
  const setSize = isMobile ? 4 : 6;   // 2×2 mobile, 3×2 desktop
  const sets = React.useMemo(() => buildGallerySets(items, setSize), [items, setSize]);
  const N = sets.length;
  const tripled = React.useMemo(() => (N ? [...sets, ...sets, ...sets] : []), [sets, N]);
  const [active, setActive] = React.useState(0);
  const [animate, setAnimate] = React.useState(false);
  const [paused, setPaused] = React.useState(false);
  const touchX = React.useRef(null);

  // Center on the middle copy's first set once sets exist (and whenever their count changes).
  React.useEffect(() => { if (N) { setAnimate(false); setActive(N); } }, [N]);

  const go = (dir) => { setAnimate(true); setActive((a) => a + dir); };

  // Autoplay: advance one set every 5s, looping forever; pause while the user is hovering/touching.
  React.useEffect(() => {
    if (!N || paused) return;
    const id = setInterval(() => go(1), 5000);
    return () => clearInterval(id);
  }, [N, paused]);

  // Seamless loop: after the slide settles, if `active` drifted out of the middle copy, silently
  // re-center by ±one copy (animation off) — the destination set is visually identical.
  // Timeout must exceed the track's transition duration below (0.8s) so it fires post-settle.
  React.useEffect(() => {
    if (!N || (active >= N && active < 2 * N)) return;
    const id = setTimeout(() => { setAnimate(false); setActive((a) => (a < N ? a + N : a - N)); }, 840);
    return () => clearTimeout(id);
  }, [active, N]);

  // Re-arm animation shortly after any silent (animation-off) jump.
  React.useEffect(() => {
    if (animate) return;
    const id = setTimeout(() => setAnimate(true), 60);
    return () => clearTimeout(id);
  }, [animate]);

  const onTouchStart = (e) => { touchX.current = e.touches[0].clientX; setPaused(true); };
  const onTouchEnd = (e) => {
    setPaused(false);
    if (touchX.current == null) return;
    const dx = e.changedTouches[0].clientX - touchX.current;
    touchX.current = null;
    if (Math.abs(dx) > 40) go(dx < 0 ? 1 : -1);
  };

  const arrowStyle = (side) => ({
    position: 'absolute', top: '50%', [side]: 'var(--space-4)', transform: 'translateY(-50%)',
    width: 44, height: 44, borderRadius: '50%', border: '1px solid var(--line)',
    background: 'var(--paper)', display: 'flex', alignItems: 'center', justifyContent: 'center',
    cursor: 'pointer', boxShadow: 'var(--shadow-lift)', zIndex: 5,
    color: 'var(--ink)', fontSize: 18, fontFamily: 'var(--font-body)',
  });

  // Move the track so set `active` is centered: viewport-center − half a set − active whole strides.
  const trackTransform = `translateX(calc(50vw - (var(--gallery-set-w) / 2) - ${active} * (var(--gallery-set-w) + var(--space-6))))`;

  return (
    <div
      onTouchStart={onTouchStart} onTouchEnd={onTouchEnd}
      onMouseEnter={() => setPaused(true)} onMouseLeave={() => setPaused(false)}
      style={{
      width: '100vw', marginLeft: 'calc(-50vw + 50%)', position: 'relative', overflow: 'hidden',
      maskImage: 'linear-gradient(to right, transparent, black 5%, black 95%, transparent)',
      WebkitMaskImage: 'linear-gradient(to right, transparent, black 5%, black 95%, transparent)',
    }}>
      <div style={{
        display: 'flex', gap: 'var(--space-6)', width: 'max-content',
        transform: trackTransform,
        transition: animate ? 'transform 0.8s cubic-bezier(0.22, 1, 0.36, 1)' : 'none',
      }}>
        {tripled.map((set, p) => (
          <div key={p} className="gallery-set" style={{
            flexShrink: 0, width: 'var(--gallery-set-w)',
            display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 'var(--space-4)',
            opacity: p === active ? 1 : 0.35,
            transition: animate ? 'opacity 0.8s cubic-bezier(0.22, 1, 0.36, 1)' : 'none',
          }}>
            {set.map((cell) => (
              <GalleryTile key={cell.key} item={cell.item} lang={lang} onClick={() => onSelect && onSelect(cell.item, cell.realIndex)} />
            ))}
          </div>
        ))}
      </div>
      <button type="button" className="gallery-arrow" aria-label="Previous" onClick={() => go(-1)} style={arrowStyle('left')}>‹</button>
      <button type="button" className="gallery-arrow" aria-label="Next" onClick={() => go(1)} style={arrowStyle('right')}>›</button>
    </div>
  );
}

function HomePage({ lang, setLang, route, cms }) {
  const [lightbox, setLightbox] = React.useState(null);
  const s = t[lang];
  const section = { maxWidth: 1120, margin: '0 auto', padding: 'var(--space-9) var(--space-6) 0' };

  // Inline-editable section heading (click-to-type on the real page in edit mode).
  const EditHeading = ({ type, content }) => (
    <window.Editable
      tag="span"
      id={cms.sections[type] && cms.sections[type].id}
      field={lang === 'th' ? 'heading_th' : 'heading_en'}
      value={lang === 'th' ? content.heading_th : content.heading_en}
    />
  );

  const heroContent = cms.sections.hero && cms.sections.hero.content;
  const trustContent = cms.sections.trust_strip && cms.sections.trust_strip.content;
  const tiersContent = cms.sections.tiers && cms.sections.tiers.content;
  const galleryContent = cms.sections.gallery_teaser && cms.sections.gallery_teaser.content;
  const papersContent = cms.sections.papers && cms.sections.papers.content;
  const compareContent = cms.sections.comparison && cms.sections.comparison.content;
  const blogContent = cms.sections.blog && cms.sections.blog.content;
  const footerContent = cms.sections.footer && cms.sections.footer.content;

  const SECTION_RENDERERS = {
    hero: () => heroContent && (
      <HeroFull
        eyebrow={lang === 'th' ? heroContent.eyebrow_th : heroContent.eyebrow_en}
        headlineTh={lang === 'th' ? heroContent.headline_th : heroContent.headline_en}
        headlineEn={lang === 'th' ? heroContent.headline_en : heroContent.headline_th}
        cta1Label={s.heroCta1} cta1Href={window.Shared.url('/gallery')}
        cta2Label={s.heroCta2} cta2Href={window.Shared.url('/papers')}
        src={heroContent.image_path || undefined}
        height={heroContent.height || undefined}
        imagePosition={heroContent.image_position || undefined}
        imageZoom={heroContent.image_zoom || undefined}
      />
    ),
    trust_strip: () => trustContent && (
      <TrustStrip
        heading={lang === 'th' ? trustContent.heading_th : trustContent.heading_en}
        clients={cms.trustClients}
      />
    ),
    tiers: () => tiersContent && (
      <section style={section}>
        <SectionTitle lang={lang} spot="tiers"><EditHeading type="tiers" content={tiersContent} /></SectionTitle>
        <TierCarousel lang={lang} tiers={cms.tiers} />
      </section>
    ),
    gallery_teaser: () => galleryContent && (
      <section style={section}>
        <SectionTitle lang={lang} spot="gallery_teaser"><EditHeading type="gallery_teaser" content={galleryContent} /></SectionTitle>
        <GalleryStrip items={cms.galleryItemsHome} lang={lang} onSelect={(item, i) => {
          // In edit mode a tile click selects the row for the Inspector instead of opening the lightbox.
          if (window.EDIT_MODE) {
            window.postToHost({
              type: 'select',
              sectionId: cms.sections.gallery_teaser && cms.sections.gallery_teaser.id,
              sectionType: 'gallery_teaser',
              item: { table: 'gallery_items', id: item.id },
            });
          } else setLightbox(i);
        }} />
        <div style={{ display: 'flex', justifyContent: 'center', marginTop: 'var(--space-5)' }}>
          <a href={galleryContent.cta_href || window.Shared.url('/gallery')} className="see-more-link" style={{
            display: 'inline-flex', alignItems: 'center', gap: 8, textDecoration: 'none',
            fontFamily: 'var(--font-body)', fontSize: 14, color: 'var(--ink-faint)',
          }}>
            {lang === 'th' ? galleryContent.cta_label_th : galleryContent.cta_label_en} <span className="see-more-arrow">→</span>
          </a>
        </div>
      </section>
    ),
    papers: () => papersContent && (
      <section style={section}>
        <SectionTitle lang={lang} spot="papers"><EditHeading type="papers" content={papersContent} /></SectionTitle>
        <p style={{ margin: '0 0 var(--space-6)', maxWidth: 820, fontSize: 'var(--text-body-size)', lineHeight: lang === 'th' ? 'var(--leading-body-thai)' : 'var(--leading-body)', color: 'var(--ink-soft)' }}>
          <window.Editable tag="span" id={cms.sections.papers && cms.sections.papers.id}
            field={lang === 'th' ? 'subtitle_th' : 'subtitle_en'}
            value={lang === 'th' ? papersContent.subtitle_th : papersContent.subtitle_en} />
        </p>
        <PaperCarousel lang={lang} papers={cms.papers} />
      </section>
    ),
    comparison: () => compareContent && (
      <section style={section}>
        <SectionTitle lang={lang} spot="comparison"><EditHeading type="comparison" content={compareContent} /></SectionTitle>
        <BusinessCardTable lang={lang} columns={compareContent.columns} rows={cms.comparisonRows} />
      </section>
    ),
    blog: () => blogContent && (
      <section style={{ ...section, paddingBottom: 'var(--space-9)' }}>
        <SectionTitle lang={lang} spot="blog"><EditHeading type="blog" content={blogContent} /></SectionTitle>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: 'var(--space-5)' }}>
          {cms.blogPosts.map((post, i) => (
            <window.EditItem key={i} table="blog_posts" id={post.id}>
              <BlogCard lang={lang} titleTh={post.titleTh} titleEn={post.titleEn} date={lang === 'th' ? post.dateTh : post.dateEn} href={post.href} src={post.src} images={post.images} tone={post.tone} imagePosition={post.imagePosition} imageZoom={post.imageZoom} />
            </window.EditItem>
          ))}
        </div>
      </section>
    ),
    footer: () => <SiteFooter lang={lang} footer={footerContent} />,
  };

  // Plain top-to-bottom section flow (no stacking/curtain cards). Each section just scale-fades
  // in as it scrolls into view; hero and footer render straight (no reveal) so the page opens
  // and closes cleanly.
  return (
    <div className="page-mobile-pad" style={{ background: 'var(--bg-light)', fontFamily: 'var(--font-body)', color: 'var(--ink)' }}>
      <Header lang={lang} setLang={setLang} route={route} footerContent={footerContent} />
      {cms.sectionsOrdered.map((sec) => {
        const renderer = SECTION_RENDERERS[sec.type];
        if (!renderer) return null;
        const plain = sec.type === 'hero' || sec.type === 'footer';
        return (
          <window.EditSection key={sec.id} type={sec.type} id={sec.id}>
            {plain ? renderer() : <Reveal>{renderer()}</Reveal>}
          </window.EditSection>
        );
      })}

      {lightbox !== null && (
        <Lightbox items={cms.galleryItemsHome} lang={lang} index={lightbox} onNavigate={setLightbox} onClose={() => setLightbox(null)} ctaHref={LINE_HREF} ctaLabel={s.cta} />
      )}
    </div>
  );
}

window.HomePage = HomePage;
window.PaperSwatch = PaperSwatch;  // reused by PapersPage.jsx
