// Home.jsx — editorial, award-winning homepage. Full-bleed hero uses generated
// cinematic imagery + layered typography + parallax + GSAP-style reveals.
const { useEffect, useRef, useState } = React;

const COLLECTIONS = [
  {
    id: 'bath',
    label: 'Bath',
    subtitle: 'Mosaic · Subway · Large Format',
    img: '/assets/generated/hero-main.jpg',
    color: 'oklch(90% 0.008 220)',
    desc: 'From sleek large-format porcelain to handmade subway glazes — tiles built for moisture, beauty and longevity.',
  },
  {
    id: 'wall',
    label: 'Wall',
    subtitle: 'Glazed · Textured · Handmade',
    img: '/assets/generated/lifestyle-wall-texture.jpg',
    color: 'oklch(88% 0.01 75)',
    desc: 'Expressive glazes, rich textures and tactile surfaces that transform kitchens, hallways and living spaces.',
  },
  {
    id: 'floor',
    label: 'Floor',
    subtitle: 'Porcelain · Stone · Anti-slip',
    img: '/assets/generated/lifestyle-floor-wide.jpg',
    color: 'oklch(85% 0.012 80)',
    desc: 'Hardwearing surfaces in natural stone looks and smooth porcelain. Timeless underfoot.',
  },
  {
    id: 'outdoor',
    label: 'Outdoor',
    subtitle: 'Frost-resistant · Non-slip · Natural',
    img: '/assets/generated/hero-outdoor.jpg',
    color: 'oklch(82% 0.02 100)',
    desc: 'Weatherproof tiles for terraces, paths and pool surrounds — crafted for the elements.',
  },
  {
    id: 'decor',
    label: 'Decor',
    subtitle: 'Patterned · Feature · Handcrafted',
    img: '/assets/generated/lifestyle-decor-feature.jpg',
    color: 'oklch(88% 0.03 50)',
    desc: 'Statement tiles for feature walls, niches and splashbacks. Zellige, encaustic and artisan glazes.',
  },
];

// Journal entries — rewritten using the user's new lifestyle imagery
// from `redesignultra/jounral/` (now copied into /assets/journal/).
// One entry — the "Outdoor Tiles: A Buyer's Guide" — deliberately keeps
// the original /assets/generated/lifestyle-outdoor-patio.jpg per
// request ("keep only one old image which is on the home page where
// journal is shown for the Outdoor Tiles: A Buyer's Guide"). The other
// five are new editorial pieces written to fit the imagery the user
// supplied — modern living, wood, marble, kitchen, and a behind-the-
// scenes piece for the black-and-white atelier shot.
const JOURNAL = [
  {
    title: 'Outdoor Tiles: A Buyer\'s Guide',
    category: 'Outdoor',
    date: 'May 2026',
    img: '/assets/generated/lifestyle-outdoor-patio.jpg',
    excerpt: 'Frost rating, slip class, format, finish — what actually matters when you\'re specifying a tile that has to live with the London weather.',
  },
  {
    title: 'Living, Layered',
    category: 'Floor · Stone',
    date: 'May 2026',
    img: '/assets/journal/Dakota-ContemporaryStone_1.jpg',
    excerpt: 'How a single concrete-look porcelain floor lets a modern living room flex between calm grey weekdays and emerald-velvet evenings.',
  },
  {
    title: 'Wood-Look Floors: A Field Guide',
    category: 'Floor · Wood',
    date: 'April 2026',
    img: '/assets/journal/DURSTONE_MICHIGAN-OAK-1.webp',
    excerpt: 'Why porcelain in oak grain is the floor that finally lets you put real warmth where real wood was never going to survive.',
  },
  {
    title: 'Marble\'s Modern Vernacular',
    category: 'Wall · Marble',
    date: 'March 2026',
    img: '/assets/journal/Swan-Durstone_1-1.webp',
    excerpt: 'White marble has stopped meaning Renaissance and started meaning mid-century. A short reading on how the new generation are using it.',
  },
  {
    title: 'The Quiet Kitchen',
    category: 'Kitchen',
    date: 'March 2026',
    img: '/assets/journal/Terramar-ContemporaryStone_4.webp',
    excerpt: 'Cream stone-look porcelain, oak cabinetry, exposed beams — a study in the kitchen that disappears so the cooking shows.',
  },
  {
    title: 'From the London Studio, by Hand',
    category: 'Studio',
    date: 'February 2026',
    img: '/assets/journal/Descargar-Serie-Bn.webp',
    excerpt: 'A morning in the warehouse where every surface arrives, gets pulled, gets photographed, and gets sent to one of you.',
  },
];

function useReveal() {
  useEffect(() => {
    const els = document.querySelectorAll('.reveal');
    const io = new IntersectionObserver(entries => {
      entries.forEach(e => { if (e.isIntersecting) e.target.classList.add('visible'); });
    }, { threshold: 0.1 });
    els.forEach(el => io.observe(el));
    return () => io.disconnect();
  }, []);
}

// Six "vibe" categories — replaces the old tile-type slides
// (Bath/Wall/Floor/Outdoor/Decor). Two of the six use real reference
// photography we have permission to use:
//   · LUXURY  → Versace Meteorite hero shot (versace-ceramics.com)
//   · STYLE   → Geotiles Frozen hero shot   (geotiles.com)
// The other four (Room / Outdoor / Heritage / Statement) are
// OpenRouter-generated bright + vibrant interiors, deliberately
// shifted off the muted-cream tone earlier images had.
//
// Click target on each slide → the existing /collections list page
// (the "vibe" categories aren't a real catalogue axis yet — when the
// catalogue gets a vibe filter, swap each entry's `target` to that
// filter's URL).
// Each slide carries everything the hero needs to rewrite its copy
// when the slideshow rotates: a two-part headline (line1 plain + line2
// in italic), a subline body paragraph, and a CTA label that swaps
// "Browse tiles" for the active category. The image, eyebrow tagline
// and click target stay alongside.
//
// The first entry (id: 'welcome') is the OPENING slide — brand-level
// rather than category-level. It's the slide the page lands on, the
// one that sets the tone before the slideshow rotates into specific
// categories. The side-by-side row below the hero deliberately skips
// this slide (uses HERO_SLIDES.slice(1)) because Welcome is a
// "browse all" entry, not a category.
const HERO_SLIDES = [
  {
    id: 'welcome', label: 'Showroom',
    tagline: 'London Tile Craft — Since 1978',
    line1: 'Every surface', line2: 'deserves a story.',
    body: 'Handcrafted tiles for bathrooms, kitchens, floors and outdoor spaces. Curated and dispatched from our London studio — six worlds of porcelain, one studio.',
    ctaLabel: 'Browse all collections',
    img: '/assets/generated/hero-welcome.jpg',
    target: 'collections',
  },
  {
    id: 'luxury', label: 'Luxury',
    tagline: 'Featuring Versace · Meteorite Miele',
    line1: 'Versace', line2: 'Meteorite.',
    body: 'Meteorite Miele in honeyed cream porcelain, paired with the house\'s Megabarocco wallpaper and a Mosaico Intreccio floor bordered by the signature Greek-key Fascia Greca. Maximvs formats, lapped or natural, available in Bianco, Miele, Avorio, Moka and Nero.',
    ctaLabel: 'Explore Luxury collection',
    img: '/assets/categories/luxury-meteorite.jpg',
    target: 'collections',
  },
  {
    id: 'style', label: 'Style',
    tagline: 'Ice-blue marble · gold veining',
    line1: 'Frozen', line2: 'marble.',
    body: 'Pale ice-blue marble-look porcelain with rich golden veining — a clear, crystalline aesthetic for bathrooms, feature walls and large-format floors.',
    ctaLabel: 'Explore Style collection',
    img: '/assets/categories/style-frozen-up.jpg',
    target: 'collections',
  },
  {
    id: 'room', label: 'Room',
    tagline: 'Living, kitchen, hallway',
    line1: 'Daylit', line2: 'interior worlds.',
    body: 'Soft terrazzo and large-format pale stone-look porcelain for living rooms, kitchens and hallways — surfaces that disappear into the architecture.',
    ctaLabel: 'Explore Room collection',
    img: '/assets/categories/room.jpg',
    target: 'collections',
  },
  {
    id: 'outdoor', label: 'Outdoor',
    tagline: 'Terraces, paths, pool surrounds',
    line1: 'Mediterranean', line2: 'exteriors.',
    body: 'Sandstone-look porcelain paving, frost-resistant and non-slip, made for terraces, pool surrounds and the unforgiving London weather.',
    ctaLabel: 'Explore Outdoor collection',
    img: '/assets/categories/outdoor.jpg',
    target: 'collections',
  },
  {
    id: 'heritage', label: 'Heritage',
    tagline: 'Travertine, marble, classical',
    line1: 'Classical', line2: 'warmth.',
    body: 'Honey-warm travertine and marble-look porcelain — for entrance halls, classical interiors and projects that earn their gravity from the floor up.',
    ctaLabel: 'Explore Heritage collection',
    img: '/assets/categories/heritage.jpg',
    target: 'collections',
  },
  {
    id: 'statement', label: 'Statement',
    tagline: 'Zellige, encaustic, feature',
    line1: 'Vivid', line2: 'feature surfaces.',
    body: 'Hand-glazed zellige, encaustic patterns, deep saturated pigments — built for feature walls, splashbacks and rooms that refuse to whisper.',
    ctaLabel: 'Explore Statement collection',
    img: '/assets/categories/statement.jpg',
    target: 'collections',
  },
];

function Home({ navigate }) {
  const heroRef = useRef(null);
  const [parallax, setParallax] = useState(0);
  const [heroLoaded, setHeroLoaded] = useState(false);
  // Active hero slide index. Auto-advances every 6.5s, hovers pause it,
  // clicking a tab jumps to that index (and resets the timer naturally
  // because the effect re-runs on `paused` change).
  const [activeSlide, setActiveSlide] = useState(0);
  const [paused, setPaused] = useState(false);

  // Journal teasers — subscribe directly to Journal.jsx's articles
  // store via a top-level useState + useEffect (NOT a conditional
  // hook inside JSX, which caused intermittent blank renders when
  // script load order varied). Initial state pulls whatever is in
  // the store at mount; if Journal.jsx hasn't loaded yet, the
  // hard-coded JOURNAL list below is used as a paint-buffer until
  // the first /api/journal response arrives and the listener fires.
  const [journalArticles, setJournalArticles] = useState(() => {
    const store = typeof window !== 'undefined' ? window.QW_articlesStore : null;
    return (store && Array.isArray(store.list) && store.list.length) ? store.list : JOURNAL;
  });
  useEffect(() => {
    const store = typeof window !== 'undefined' ? window.QW_articlesStore : null;
    if (!store) return;
    const sync = () => setJournalArticles([...store.list]);
    store.listeners.add(sync);
    sync(); // pick up any update that landed between initial state + effect
    return () => { store.listeners.delete(sync); };
  }, []);

  // Re-attach the reveal IntersectionObserver to the journal cards
  // every time the articles list changes. The global useReveal() hook
  // below only runs ONCE on mount, so if the articles store updates
  // AFTER mount (server fetch landed late), the newly-rendered cards
  // never get the `.visible` class and stay at opacity:0 — the
  // "sometimes blank journal section" bug. This effect re-observes
  // any `.reveal` element inside `.journal-grid` whenever the list
  // changes, and as a safety net forces `.visible` after a 600 ms
  // grace period in case the observer ever misses an element.
  useEffect(() => {
    const els = document.querySelectorAll('.journal-grid .reveal');
    if (!els.length) return;
    const io = new IntersectionObserver(entries => {
      entries.forEach(e => { if (e.isIntersecting) e.target.classList.add('visible'); });
    }, { threshold: 0.1 });
    els.forEach(el => io.observe(el));
    const safety = setTimeout(() => {
      els.forEach(el => el.classList.add('visible'));
    }, 600);
    return () => { io.disconnect(); clearTimeout(safety); };
  }, [journalArticles]);

  useReveal();

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

  // Trigger entrance animation
  useEffect(() => {
    const t = setTimeout(() => setHeroLoaded(true), 80);
    return () => clearTimeout(t);
  }, []);

  // Auto-advance the slideshow. Cleared & restarted whenever `paused`
  // flips so a hover-and-leave doesn't give a half-tick stutter.
  useEffect(() => {
    if (paused) return;
    const id = setInterval(() => {
      setActiveSlide(s => (s + 1) % HERO_SLIDES.length);
    }, 6500);
    return () => clearInterval(id);
  }, [paused]);

  // Click handler shared by the slide click-area and the "Enter" chip.
  // All six vibe categories currently route to the full collections
  // list (the catalogue doesn't have vibe-axis filtering yet). When
  // it does, switch each slide's `target` to a filter URL.
  const enterSlide = (slide) => {
    if (slide.target === 'collections') return navigate('collections');
    if (typeof slide.target === 'number' && COLLECTIONS[slide.target]) {
      const col = COLLECTIONS[slide.target];
      return navigate('collection', { id: col.id, label: col.label + ' Tiles', ...col });
    }
    navigate('collections');
  };

  // Hero left/right zone navigation — wraps in both directions so the
  // user can keep clicking past the ends. Resets the auto-rotate
  // pause flag so the slideshow keeps rolling after a manual click.
  const prevSlide = () => setActiveSlide(s => (s - 1 + HERO_SLIDES.length) % HERO_SLIDES.length);
  const nextSlide = () => setActiveSlide(s => (s + 1) % HERO_SLIDES.length);

  return (
    <div>
      {/* ══════════════ HERO ══════════════ */}
      <section ref={heroRef} style={{
        position: 'relative', height: '100svh', minHeight: '720px',
        overflow: 'hidden', background: 'var(--dark)',
      }}>
        {/* Rotating cinematic slideshow with parallax. Layered <img>s
            stacked at full-bleed; only the active slide has opacity:1,
            the rest fade. Click handler removed — clicks now go to the
            two left/right nav zones (defined further down) for slide
            navigation, and to the bottom CTA for "enter collection". */}
        <div
          style={{
            position: 'absolute', inset: 0,
            transform: `translateY(${parallax * 0.35}px) scale(${1 + parallax * 0.0002})`,
            transition: 'opacity 1.4s var(--ease-out)',
            opacity: heroLoaded ? 1 : 0,
          }}
          onMouseEnter={() => setPaused(true)}
          onMouseLeave={() => setPaused(false)}
          aria-hidden
        >
          {HERO_SLIDES.map((s, i) => (
            <img
              key={s.id}
              src={s.img}
              alt=""
              style={{
                position: 'absolute', inset: 0,
                width: '100%', height: '120%', objectFit: 'cover', display: 'block',
                /* Lighter filter than the original — the photographs are
                   meant to read bright and airy. The bottom-darkening is
                   handled by the gradient layer below, not by burning the
                   image itself. */
                filter: 'brightness(0.96) saturate(1.04)',
                opacity: i === activeSlide ? 1 : 0,
                transition: 'opacity 1.4s var(--ease-out)',
                pointerEvents: 'none',
              }}
            />
          ))}
        </div>
        {/* Editorial gradient overlay. Adjusted for the lighter hero
            slides — top is now barely-tinted (was 55% dark) so the
            airy daylight in the new photography reads through, while
            the bottom remains heavily darkened to preserve white-text
            legibility for the headline copy. The gradient layer is
            pointer-events:none so clicks pass through to the slide
            click-area below it. */}
        <div style={{
          position: 'absolute', inset: 0, pointerEvents: 'none',
          background: 'linear-gradient(180deg, rgba(20,16,12,0.18) 0%, rgba(20,16,12,0.04) 30%, rgba(20,16,12,0.42) 70%, rgba(20,16,12,0.85) 100%)',
        }}/>
        {/* Film grain micro-texture */}
        <div style={{
          position: 'absolute', inset: 0, pointerEvents: 'none',
          backgroundImage: 'radial-gradient(rgba(255,255,255,0.04) 1px, transparent 1px)',
          backgroundSize: '3px 3px', mixBlendMode: 'overlay', opacity: 0.6,
        }}/>

        {/* Hero NAV ZONES — almost-full-hero left and right click
            zones that act as previous/next buttons. Cursor.jsx watches
            for [data-hero-nav] and renders the chevron arrow when
            the pointer is over one.

            Layout per the brief — "anywhere on the hero stays in that
            arrow format apart from the nav bar and one 10% strip in
            the middle":
              top:  var(--nav-h)  → nav bar excluded
              bottom: 0           → all the way down (no bottom mask)
              left zone:  width 45%  (left edge → 45%)
              right zone: width 45%  (55% → right edge)
              middle 10%  → uncovered "safe gap" so the cursor reverts
                            to its standard square in that band.

            z-index 2 keeps them above the image + gradient but below
            the content (which sits at z-index 3) so the bottom
            headline + CTA remain perfectly clickable. */}
        <button
          type="button"
          data-hero-nav="left"
          onClick={prevSlide}
          aria-label="Previous slide"
          style={{
            position: 'absolute', left: 0, top: 'var(--nav-h)', bottom: 0, width: '45%',
            zIndex: 2, background: 'transparent', border: 'none', padding: 0,
          }}
        />
        <button
          type="button"
          data-hero-nav="right"
          onClick={nextSlide}
          aria-label="Next slide"
          style={{
            position: 'absolute', right: 0, top: 'var(--nav-h)', bottom: 0, width: '45%',
            zIndex: 2, background: 'transparent', border: 'none', padding: 0,
          }}
        />

        {/* Side label — rotated */}
        <div style={{
          position: 'absolute', left: '34px', top: '50%',
          transform: 'translateY(-50%) rotate(-90deg)', transformOrigin: 'left top',
          fontFamily: 'var(--sans)', fontSize: '10px', letterSpacing: '0.3em',
          textTransform: 'uppercase', color: 'rgba(255,255,255,0.55)',
          opacity: heroLoaded ? 1 : 0, transition: 'opacity 1.2s 0.4s var(--ease-out)',
        }}>
          N° 01 &nbsp;—&nbsp; Spring / Summer 2026
        </div>

        {/* Right editorial meta */}
        <div style={{
          position: 'absolute', right: '34px', top: '112px',
          fontFamily: 'var(--sans)', fontSize: '10px', letterSpacing: '0.2em',
          textTransform: 'uppercase', color: 'rgba(255,255,255,0.55)',
          textAlign: 'right', lineHeight: 2,
          opacity: heroLoaded ? 1 : 0, transition: 'opacity 1.2s 0.5s var(--ease-out)',
        }} className="hero-meta">
          London &nbsp;·&nbsp; 51° 30′ N
        </div>

        {/* Top-right floating "Enter [Label]" chip removed per request
            — its function (per-slide CTA) has moved into the bottom
            CTA position, replacing the static "Browse tiles" button. */}

        {/* Main copy */}
        <div style={{
          position: 'absolute', bottom: '10vh', left: '80px', right: '80px',
          display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', gap: '60px',
          flexWrap: 'wrap',
          zIndex: 3,
          /* Critical: wrapper itself doesn't intercept mouse events
             — otherwise the empty space inside this 80px-inset
             container would block the underlying nav zones (which is
             why the boomerang arrow was only showing in the very
             outer ~10% earlier). pointer-events:auto is restored on
             the actual button below so the CTA still clicks. */
          pointerEvents: 'none',
        }} className="hero-main">
          <div style={{ maxWidth: '860px' }}>
            <p style={{
              fontFamily: 'var(--sans)', fontSize: '11px', letterSpacing: '0.3em',
              textTransform: 'uppercase', color: 'rgba(255,255,255,0.7)', marginBottom: '28px',
              opacity: heroLoaded ? 1 : 0, transform: heroLoaded ? 'translateY(0)' : 'translateY(16px)',
              transition: 'all 1s 0.2s var(--ease-out)',
            }}>
              {/* Hero copy reverted to its original wording per request
                  — only the collection NAMES on the rotating slides /
                  side-by-side row are the new vibe categories. The
                  brand statement copy stays as it was. */}
              <span style={{ display: 'inline-block', width: '36px', height: '1px', background: 'white', verticalAlign: 'middle', marginRight: '14px' }}/>
              London Tile Craft — Since 1978
            </p>
            <h1 style={{
              fontFamily: 'var(--serif)', fontWeight: 300,
              fontSize: 'clamp(60px, 9.2vw, 148px)', lineHeight: 0.92,
              color: 'white', letterSpacing: '-0.02em', marginBottom: '34px',
            }}>
              {/* Headline reads from the active slide — line1 plain
                  + line2 italic. Both lines re-render when activeSlide
                  changes (React state); the opacity-fade keys the
                  transition off the slide id so each new headline
                  fades in cleanly. */}
              <span style={{ display: 'block', overflow: 'hidden' }}>
                <span key={'h1-' + HERO_SLIDES[activeSlide].id} style={{
                  display: 'inline-block',
                  transform: heroLoaded ? 'translateY(0)' : 'translateY(110%)',
                  transition: 'transform 1.2s 0.3s cubic-bezier(0.2, 0.9, 0.25, 1), opacity 0.7s var(--ease-out)',
                  animation: 'heroCopyIn 0.8s var(--ease-out) both',
                }}>{HERO_SLIDES[activeSlide].line1}</span>
              </span>
              <span style={{ display: 'block', overflow: 'hidden' }}>
                <em key={'h2-' + HERO_SLIDES[activeSlide].id} style={{
                  display: 'inline-block', fontStyle: 'italic', color: 'white', fontWeight: 300,
                  transform: heroLoaded ? 'translateY(0)' : 'translateY(110%)',
                  transition: 'transform 1.2s 0.45s cubic-bezier(0.2, 0.9, 0.25, 1), opacity 0.7s var(--ease-out)',
                  animation: 'heroCopyIn 0.8s 0.1s var(--ease-out) both',
                }}>{HERO_SLIDES[activeSlide].line2}</em>
              </span>
            </h1>
            {/* Subline reads from the active slide — re-renders on
                slide change with the same fade-in keying as the
                headline. */}
            <p key={'sub-' + HERO_SLIDES[activeSlide].id} style={{
              fontFamily: 'var(--sans)', fontWeight: 300, fontSize: '17px',
              color: 'rgba(255,255,255,0.78)', maxWidth: '520px', lineHeight: 1.7, marginBottom: '40px',
              opacity: heroLoaded ? 1 : 0, transform: heroLoaded ? 'translateY(0)' : 'translateY(16px)',
              transition: 'all 1s 0.7s var(--ease-out)',
              animation: 'heroCopyIn 0.8s 0.18s var(--ease-out) both',
            }}>
              {HERO_SLIDES[activeSlide].body}
            </p>
            {/* CTA is now the per-slide "Explore [Label] collection"
                button — replaces the previous static "Browse tiles"
                button AND the top-right floating "Enter [Label]" chip
                (which has been removed entirely). One CTA, dynamic,
                always tied to the slide on screen. */}
            <div style={{
              display: 'flex', gap: '16px', flexWrap: 'wrap',
              opacity: heroLoaded ? 1 : 0, transform: heroLoaded ? 'translateY(0)' : 'translateY(16px)',
              transition: 'all 1s 0.85s var(--ease-out)',
            }}>
              <button key={'cta-' + HERO_SLIDES[activeSlide].id}
                onClick={() => enterSlide(HERO_SLIDES[activeSlide])} className="btn"
                style={{
                  background: 'white', color: 'var(--dark)', padding: '16px 32px',
                  border: 'none', cursor: 'pointer', fontFamily: 'var(--sans)',
                  fontSize: '11px', letterSpacing: '0.16em', textTransform: 'uppercase',
                  transition: 'all 0.3s',
                  animation: 'heroCopyIn 0.8s 0.26s var(--ease-out) both',
                  pointerEvents: 'auto',  /* re-enable inside the pointer-events:none wrapper */
                }}
                onMouseEnter={e => { e.currentTarget.style.background = 'var(--terracotta)'; e.currentTarget.style.color = 'white'; }}
                onMouseLeave={e => { e.currentTarget.style.background = 'white'; e.currentTarget.style.color = 'var(--dark)'; }}
              >{HERO_SLIDES[activeSlide].ctaLabel} &nbsp;→</button>
            </div>
          </div>

          {/* Featured tile swatch card removed per request — hero is
              now just the rotating slideshow + headline + single CTA,
              no floating product card. */}
        </div>

        {/* The numbered 01-07 tab row was removed per request — slide
            navigation now happens by clicking the left/right halves
            of the hero (the data-hero-nav buttons above), which
            advance forward/back with wrap-around. The auto-rotate
            still drives passive cycling. */}

        {/* Scroll cue */}
        <div style={{
          position: 'absolute', bottom: '24px', left: '50%', transform: 'translateX(-50%)',
          display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '10px',
          opacity: heroLoaded ? 0.72 : 0, transition: 'opacity 1.2s 1.4s var(--ease-out)',
        }}>
          <span style={{ fontFamily: 'var(--sans)', fontSize: '9px', letterSpacing: '0.26em', textTransform: 'uppercase', color: 'white' }}>Scroll</span>
          <div style={{ width: '1px', height: '48px', background: 'rgba(255,255,255,0.3)', overflow: 'hidden', position: 'relative' }}>
            <div style={{ position: 'absolute', top: 0, width: '100%', height: '50%', background: 'white', animation: 'scrollLine 2.4s ease-in-out infinite' }}/>
          </div>
        </div>

        <style>{`
          @keyframes scrollLine { 0%{top:-50%;} 100%{top:150%;} }
          /* Used by the per-slide hero copy (line1 / line2 / body /
             CTA). Each element is keyed off the slide id so React
             re-mounts it when activeSlide changes — that re-mount
             restarts the animation from 0, giving each slide's copy
             a clean fade + lift in instead of a hard text swap. */
          @keyframes heroCopyIn {
            from { opacity: 0; transform: translateY(14px); }
            to   { opacity: 1; transform: translateY(0); }
          }
          @media (max-width: 960px) {
            .hero-swatch { display: none !important; }
            .hero-meta { display: none !important; }
            .hero-main { left: 24px !important; right: 24px !important; bottom: 8vh !important; }
            /* Six tabs in one row don't fit on phones; collapse them
               into compact 2-letter codes (BA / WA / FL / OU / DE)
               using the data-attribute trick so we keep the same
               markup. */
            .hero-tabs { gap: 0 !important; bottom: 96px !important; }
            .hero-tabs button { padding: 10px 8px !important; font-size: 9px !important; letter-spacing: 0.16em !important; }
            .hero-tabs button > span:first-child { display: none !important; }
          }
        `}</style>
      </section>

      {/* ══════════════ MARQUEE STRIP ══════════════ */}
      <div style={{
        overflow: 'hidden', background: 'var(--cream)', borderBottom: '1px solid var(--cream-deep)',
        padding: '22px 0', whiteSpace: 'nowrap',
      }}>
        <div style={{ display: 'inline-block', animation: 'marq 40s linear infinite' }}>
          {[
            'Light, shadow, surface', '—', 'A floor is a quiet foundation',
            '—', 'Stone holds light differently', '—', 'Where pattern meets patience',
            '—', 'Touch, then choose', '—', 'Rooms begin underfoot', '—',
          ].concat([
            'Light, shadow, surface', '—', 'A floor is a quiet foundation',
            '—', 'Stone holds light differently', '—', 'Where pattern meets patience',
            '—', 'Touch, then choose', '—', 'Rooms begin underfoot', '—',
          ]).map((t, i) => (
            <span key={i} style={{
              fontFamily: 'var(--serif)', fontSize: '20px', fontWeight: 300,
              fontStyle: t === '—' ? 'normal' : 'italic',
              color: t === '—' ? 'var(--terracotta)' : 'var(--dark)',
              padding: '0 22px',
            }}>{t}</span>
          ))}
        </div>
        <style>{`@keyframes marq { 0%{transform:translateX(0)} 100%{transform:translateX(-50%)} }`}</style>
      </div>

      {/* ══════════════ EDITORIAL INTRO ══════════════
           This is the first content section after the hero + marquee.
           The user's "Blue Roma" tile webp slides in from the right
           as the section scrolls into view, settling fully when the
           section is 70% visible. Image sits BEHIND every text element
           and IS the section background once settled.

           Mechanics: tracks the section's position relative to the
           viewport via a scroll listener; computes a 0-1 progress
           value from "section bottom enters viewport bottom" → "section
           top reaches viewport top". Maps progress 0 → 0.7 to
           translateX 100% → 0%. Above 0.7 progress, image stays
           pinned. Above 0.85 it starts to ease back out so it doesn't
           cling forever (optional — keeps the next section feeling
           clean). */}
      <Page2WithSlideBackground/>

      {/* ══════════════ CATEGORIES — SIDE-BY-SIDE 6-UP ══════════════
           Replaces the old masonry CollectionCard grid. Renders the
           same six "vibe" categories the hero rotates through, but
           laid out as a single equal-width row so the user sees
           every category at a glance and can pick by visual mood.
           Tall portrait cards (3:5) with the category label
           overlaid bottom-left. Click anywhere on a card → enter
           that category's page. */}
      <section style={{ padding: '40px 24px 40px' }} className="categories-section">
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', flexWrap: 'wrap', gap: '20px', marginBottom: '56px', padding: '0 16px', maxWidth: '1640px', margin: '0 auto 56px' }}>
          <div>
            <p className="t-label reveal" style={{ color: 'var(--terracotta)', marginBottom: '14px', letterSpacing: '0.18em' }}>N° 02 — Categories</p>
            <h2 className="reveal reveal-delay-1" style={{
              fontFamily: 'var(--serif)', fontWeight: 300, fontSize: 'clamp(38px, 5vw, 72px)',
              lineHeight: 1, letterSpacing: '-0.02em',
            }}>
              Six vibes,<br/><em>one studio.</em>
            </h2>
          </div>
          <button onClick={() => navigate('collections')} className="btn btn-ghost reveal reveal-delay-2">Explore all &nbsp;→</button>
        </div>
        <div className="categories-row" style={{
          display: 'grid',
          gridTemplateColumns: 'repeat(6, 1fr)',
          gap: '6px',
          maxWidth: '1640px',
          margin: '0 auto',
        }}>
          {/* slice(1) skips HERO_SLIDES[0] (the welcome / showroom
              slide) — the row is for the six "vibe" categories only;
              welcome is a hero-only opener. */}
          {HERO_SLIDES.slice(1).map((c, i) => (
            <button
              key={c.id}
              type="button"
              className="reveal"
              style={{
                position: 'relative', display: 'block',
                background: 'var(--cream-mid)',
                border: 'none', padding: 0, cursor: 'pointer',
                aspectRatio: '3 / 5',
                overflow: 'hidden',
                transitionDelay: (i * 0.06) + 's',
              }}
              onClick={() => enterSlide(c)}
            >
              <img src={c.img} alt={c.label} style={{
                position: 'absolute', inset: 0, width: '100%', height: '100%',
                objectFit: 'cover', display: 'block',
                transition: 'transform 0.9s var(--ease-out), filter 0.5s',
              }}
              onMouseEnter={e => e.currentTarget.style.transform = 'scale(1.05)'}
              onMouseLeave={e => e.currentTarget.style.transform = 'scale(1)'}
              />
              {/* Bottom gradient + label overlay */}
              <div aria-hidden style={{
                position: 'absolute', inset: 0, pointerEvents: 'none',
                background: 'linear-gradient(180deg, transparent 50%, rgba(15,12,10,0.78) 100%)',
              }}/>
              <div style={{
                position: 'absolute', left: '20px', right: '20px', bottom: '20px',
                color: 'white', textAlign: 'left',
              }}>
                <p style={{
                  fontFamily: 'var(--serif)', fontWeight: 300,
                  fontSize: 'clamp(22px, 1.8vw, 30px)',
                  lineHeight: 1, letterSpacing: '-0.005em',
                  marginBottom: '8px',
                }}>{c.label}</p>
                <p style={{
                  fontFamily: 'var(--sans)', fontSize: '10px',
                  letterSpacing: '0.18em', textTransform: 'uppercase',
                  color: 'rgba(255,255,255,0.7)', lineHeight: 1.4,
                }}>{c.tagline}</p>
              </div>
            </button>
          ))}
        </div>
        <style>{`
          @media (max-width: 1100px) { .categories-row { grid-template-columns: repeat(3, 1fr) !important; } }
          @media (max-width: 640px)  { .categories-row { grid-template-columns: repeat(2, 1fr) !important; } }
        `}</style>
      </section>

      {/* Trust Strip section removed on 2026-04-27. Full code preserved
          at _removed/TrustStrip.md so the four stats (480+ / 48 /
          Free / 24h) can be put back if needed. */}

      {/* ══════════════ JOURNAL ══════════════ */}
      <section style={{ padding: 'clamp(60px, 8vw, 80px) clamp(24px, 4vw, 40px) clamp(60px, 8vw, 80px)' }} className="journal-section">
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', flexWrap: 'wrap', gap: '20px', marginBottom: 'clamp(40px, 6vw, 64px)' }}>
          <div>
            <p className="t-label reveal" style={{ color: 'var(--terracotta)', marginBottom: '14px' }}>N° 03 — Journal</p>
            <h2 className="reveal reveal-delay-1" style={{
              fontFamily: 'var(--serif)', fontWeight: 300, fontSize: 'clamp(36px, 5vw, 64px)',
              lineHeight: 1, letterSpacing: '-0.02em',
            }}>
              Ideas &amp; <em>inspiration</em>
            </h2>
          </div>
          <button onClick={() => navigate('journal')} className="btn btn-ghost reveal reveal-delay-2">Read the journal &nbsp;→</button>
        </div>
        {/* 6 entries flow naturally as 3-cols × 2-rows on desktop;
            grid-auto-rows handles the second row without any extra
            markup. Each card shows category + date, headline, a short
            excerpt (per the rewritten data), then the read-link. */}
        <div style={{
          display: 'grid',
          gridTemplateColumns: 'repeat(3, 1fr)',
          gap: '40px 32px',
        }} className="journal-grid">
          {/* Home teasers — pulled from the live articlesStore via the
              `journalArticles` state at the top of this component. */}
          {journalArticles.slice(0, 6).map((a, i) => {
            const img = a.img || a.thumb || a.hero || '';
            const teaser = a.excerpt || a.intro || '';
            return (
              <article
                key={a.id || i}
                className="reveal"
                style={{ transitionDelay: `${(i % 3) * 0.1}s`, cursor: 'pointer' }}
                onClick={() => navigate('journal', a.id ? { articleId: a.id } : null)}
              >
                <div style={{ aspectRatio: '4/5', overflow: 'hidden', background: 'var(--cream-deep)', marginBottom: '22px' }}>
                  <img src={img} alt={a.title} style={{ width: '100%', height: '100%', objectFit: 'cover', transition: 'transform 0.9s var(--ease-out)' }}
                    onMouseEnter={e => e.target.style.transform = 'scale(1.05)'}
                    onMouseLeave={e => e.target.style.transform = 'scale(1)'}
                    onError={e => e.target.style.display='none'}
                  />
                </div>
                <p className="t-label" style={{ color: 'var(--terracotta)', marginBottom: '10px' }}>{a.category} · {a.date}</p>
                <h3 style={{ fontFamily: 'var(--serif)', fontSize: '24px', fontWeight: 300, lineHeight: 1.18, marginBottom: '10px', letterSpacing: '-0.01em' }}>{a.title}</h3>
                {teaser && (
                  <p style={{
                    fontFamily: 'var(--sans)', fontSize: '13px', fontWeight: 300,
                    lineHeight: 1.65, color: 'var(--dark-mid)',
                    marginBottom: '12px',
                  }}>{teaser}</p>
                )}
                <p style={{ fontFamily: 'var(--sans)', fontSize: '12px', color: 'var(--dark)', textDecoration: 'underline', textUnderlineOffset: '4px', letterSpacing: '0.04em' }}>Read article</p>
              </article>
            );
          })}
        </div>
        <style>{`
          @media (max-width: 1100px) {
            .journal-grid { grid-template-columns: repeat(2, 1fr) !important; }
          }
          @media (max-width: 700px) {
            .journal-grid { grid-template-columns: 1fr !important; gap: 32px !important; }
            .journal-section { padding: 80px 24px 40px !important; }
          }
        `}</style>
      </section>

      {/* Closing CTA section ("Quoted per project / One reply, within
          a day") removed per request. Quote flow remains accessible
          from the cart drawer, mega-menu, footer, and tile detail
          pages. */}
    </div>
  );
}

// ─── Editorial collection card with hover reveal ────────────────────────
function CollectionCard({ col, navigate, spanCol, spanRow, tall, wide }) {
  const [hover, setHover] = useState(false);
  return (
    <div
      className="reveal"
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
      onClick={() => navigate('collection', { id: col.id, label: `${col.label} Tiles`, ...col })}
      style={{
        gridColumn: `span ${spanCol}`, gridRow: `span ${spanRow}`,
        position: 'relative', overflow: 'hidden', cursor: 'pointer',
        aspectRatio: wide ? '21/6' : tall ? '5/7' : '4/3',
        background: col.color,
      }}
    >
      <img src={col.img} alt={col.label} style={{
        width: '100%', height: '100%', objectFit: 'cover', display: 'block',
        transform: hover ? 'scale(1.06)' : 'scale(1)', transition: 'transform 1.2s var(--ease-out)',
      }} onError={e => e.target.style.display='none'}/>
      <div style={{
        position: 'absolute', inset: 0,
        background: hover
          ? 'linear-gradient(to top, rgba(20,16,12,0.85) 0%, rgba(20,16,12,0.15) 55%)'
          : 'linear-gradient(to top, rgba(20,16,12,0.55) 0%, transparent 50%)',
        transition: 'background 0.5s',
      }}/>
      <div style={{ position: 'absolute', top: '22px', left: '22px' }}>
        <p style={{ fontFamily: 'var(--sans)', fontSize: '10px', letterSpacing: '0.22em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.78)' }}>
          N° {String(COLLECTIONS.findIndex(c => c.id === col.id) + 1).padStart(2, '0')}
        </p>
      </div>
      <div style={{ position: 'absolute', bottom: '24px', left: '24px', right: '24px' }}>
        <h3 style={{
          fontFamily: 'var(--serif)', fontSize: tall ? 'clamp(32px, 3vw, 48px)' : wide ? 'clamp(34px, 3.6vw, 56px)' : 'clamp(26px, 2.4vw, 38px)',
          fontWeight: 300, color: 'white', lineHeight: 1, letterSpacing: '-0.015em', marginBottom: '8px',
        }}>{col.label} Tiles</h3>
        <p style={{ fontFamily: 'var(--sans)', fontSize: '11px', color: 'rgba(255,255,255,0.7)', letterSpacing: '0.14em', textTransform: 'uppercase', marginBottom: '14px' }}>{col.subtitle}</p>
        <div style={{
          overflow: 'hidden',
          maxHeight: hover ? '80px' : '0',
          opacity: hover ? 1 : 0,
          transition: 'max-height 0.5s var(--ease-out), opacity 0.4s var(--ease-out)',
        }}>
          <p style={{
            fontFamily: 'var(--sans)', fontWeight: 300, fontSize: '13px',
            color: 'rgba(255,255,255,0.85)', maxWidth: '440px', lineHeight: 1.6, marginBottom: '12px',
          }}>{col.desc}</p>
          <p style={{ fontFamily: 'var(--sans)', fontSize: '11px', color: 'white', letterSpacing: '0.14em', textTransform: 'uppercase' }}>
            Explore collection &nbsp;→
          </p>
        </div>
      </div>
    </div>
  );
}

// ─── Scroll-driven fade hook (shared by sections that have a fade-in
// background image). Returns a CSS opacity value 0..1 based on how
// much of the target section has scrolled into the viewport. The
// section is "fully appeared" once `settleAt` (default 0.20) of its
// height has scrolled past the viewport bottom — earlier brief asked
// for 70% slide settle; user revised to 20% appearance for a faster,
// snappier reveal with no horizontal motion at all.
//
// Returns: { ref, opacity } — attach `ref` to the section element,
// apply `opacity` to whatever you want to fade in.

function useFadeOnScroll(settleAt = 0.20) {
  const ref = React.useRef(null);
  const [opacity, setOpacity] = React.useState(0);

  React.useEffect(() => {
    const onScroll = () => {
      const sec = ref.current;
      if (!sec) return;
      const r = sec.getBoundingClientRect();
      const vh = window.innerHeight;
      // Share of section currently scrolled past the viewport bottom.
      // Normalised against section height so the timing is
      // consistent regardless of how tall the section is.
      const appearance = Math.max(0, Math.min(1, (vh - r.top) / Math.max(r.height, 1)));
      // Linear ease 0 → 1 across [0, settleAt]; pinned at 1 above.
      const v = appearance >= settleAt ? 1 : appearance / settleAt;
      setOpacity(v);
    };
    onScroll();
    window.addEventListener('scroll', onScroll, { passive: true });
    window.addEventListener('resize', onScroll, { passive: true });
    return () => {
      window.removeEventListener('scroll', onScroll);
      window.removeEventListener('resize', onScroll);
    };
  }, [settleAt]);

  return { ref, opacity };
}

// ─── Editorial intro section with the Blue Roma background.
// User-supplied "Blue Roma" porcelain image (portrait native, displayed
// landscape via object-fit:cover) sits absolutely behind the text. No
// horizontal slide — pure opacity fade per the latest brief: appears
// fully once the section is 20% scrolled-in. White wash on top keeps
// the dark copy readable.

function Page2WithSlideBackground() {
  // Scroll-driven fade-in removed per request — image is now permanent
  // (always at full opacity), simpler and more reliable than chasing
  // a 20% scroll trigger.
  return (
    <section
      style={{
        position: 'relative',
        overflow: 'hidden',
        padding: '140px 80px 120px',
        margin: '0 auto',
        maxWidth: 'none',
      }}
      className="editorial-intro"
    >
      {/* Background image layer — permanently visible (opacity:1).
          Was a scroll-driven fade earlier; the user asked for it to
          just be there from the start. */}
      <div
        aria-hidden
        style={{
          position: 'absolute',
          inset: 0,
          zIndex: 0,
          pointerEvents: 'none',
        }}
      >
        <img
          src="/assets/page2/blue-roma.webp"
          alt=""
          style={{
            width: '100%',
            height: '100%',
            objectFit: 'cover',
            objectPosition: 'center',
            display: 'block',
          }}
        />
        {/* White wash for text legibility (image is the BACKGROUND, the
            charcoal copy reads on top). Gradient skews lighter toward
            the left where the bulk of the text sits. */}
        <div style={{
          position: 'absolute', inset: 0,
          background: 'linear-gradient(90deg, rgba(255,255,255,0.92) 0%, rgba(255,255,255,0.72) 50%, rgba(255,255,255,0.55) 100%)',
        }}/>
      </div>

      {/* Foreground content — same copy as before, z-indexed so it
          always sits on top of the fading image. */}
      <div style={{ position: 'relative', zIndex: 1, maxWidth: '1400px', margin: '0 auto' }}>
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1.4fr', gap: '80px', alignItems: 'flex-start' }} className="intro-grid">
          <p className="t-label reveal" style={{ color: 'var(--dark)' }}>Our philosophy</p>
          <div>
            <h2 className="reveal reveal-delay-1" style={{
              fontFamily: 'var(--serif)', fontWeight: 300, fontSize: 'clamp(34px, 4vw, 62px)',
              lineHeight: 1.1, letterSpacing: '-0.015em', color: 'var(--dark)', marginBottom: '34px',
            }}>
              A tile is not a product.<br/>
              <em style={{ color: 'var(--dark)', fontStyle: 'italic' }}>It's a decision you live with.</em>
            </h2>
            <p className="reveal reveal-delay-2" style={{
              fontFamily: 'var(--sans)', fontWeight: 400, fontSize: '17px', lineHeight: 1.8,
              color: 'var(--dark)', maxWidth: '640px', marginBottom: '20px',
            }}>
              A London tile studio with almost five decades of curation behind it. We don't make tiles — we choose them, carefully, working direct with the makers whose craft we trust. Every surface in our catalogue has been seen, touched and approved by us before it earns its place, and described in plain language — size, finish, material, origin — so what arrives at your door is exactly what you saw on the screen.
            </p>
            <p className="reveal reveal-delay-3" style={{
              fontFamily: 'var(--sans)', fontWeight: 400, fontSize: '17px', lineHeight: 1.8,
              color: 'var(--dark)', maxWidth: '640px',
            }}>
              We hold the catalogue at our Wimbledon warehouse and dispatch from there. Tell us about your project — the room, the surface, the square metreage, the look you're after — and we'll come back with pricing, realistic lead times, and a sample if you'd like one.
            </p>
          </div>
        </div>
      </div>

      <style>{`
        @media (max-width: 900px) {
          .intro-grid { grid-template-columns: 1fr !important; gap: 20px !important; }
          .editorial-intro { padding: 80px 24px !important; }
        }
      `}</style>
    </section>
  );
}

// TrustStripWithBackground component removed on 2026-04-27.
// Full source preserved in _removed/TrustStrip.md for restoration.


Object.assign(window, { Home, COLLECTIONS, JOURNAL, HERO_SLIDES, useReveal, Page2WithSlideBackground, useFadeOnScroll });
