// Phone.jsx — phone-native UI components.
//
// These are SEPARATE components from the desktop equivalents, not
// responsive variants. App.jsx swaps them in when viewport ≤ 760px so
// every phone visitor gets a UI designed for a 360–414 px screen and
// touch-first interaction.
//
// Shared with the desktop tree:
//   · Hooks (useAccount, useAllTiles, useCollectionsMeta, etc.)
//   · Server endpoints (no duplication)
//   · Tile data + collection metadata
//
// Phone-specific:
//   · Layouts (single-column, sticky bars, bottom-sheet drawers)
//   · Typography (smaller, denser)
//   · Interactions (touch swipe, no hover states, big tap targets)
//
// Components defined:
//   · PhoneHeader     — sticky top bar (logo + cart/search/menu)
//   · PhoneFooter     — minimal footer
//   · PhoneHome       — landing page
//   · PhoneListing    — collection / filter / grid
//   · PhoneProduct    — product detail
//   · PhoneJournal    — journal list + article view
//   · PhoneQuote      — quote form
//   · PhoneAccount    — favourites + profile

const { useState, useEffect, useMemo, useRef } = React;

// Bind dependencies once at module load. index.html guarantees these
// scripts load before Phone.jsx (AccountStore + Collections + Journal
// + Home all run their Object.assign(window, {…}) before this file
// is even parsed).
const useAccount        = window.useAccount;
const useAllTilesHook   = window.useAllTilesHook;
const useCollectionsMetaHook = window.useCollectionsMetaHook;
const HERO_SLIDES_DATA  = window.HERO_SLIDES || [];
const ARTICLES_DATA     = window.ARTICLES    || [];
// Live subscription that's safe to call from any component, regardless
// of script load order. Always calls the same React hooks (useState +
// useEffect) so the hook-order rule is never violated. If
// Journal.jsx's articlesStore is available on the window, subscribes
// to it; otherwise the initial state is whatever ARTICLES_DATA holds
// and never updates — the page still renders.
function usePhoneArticles() {
  const [list, setList] = useState(() => {
    const store = typeof window !== 'undefined' ? window.QW_articlesStore : null;
    return (store && Array.isArray(store.list) && store.list.length) ? store.list : ARTICLES_DATA;
  });
  useEffect(() => {
    const store = typeof window !== 'undefined' ? window.QW_articlesStore : null;
    if (!store) return;
    const sync = () => setList([...store.list]);
    store.listeners.add(sync);
    sync();
    return () => { store.listeners.delete(sync); };
  }, []);
  return list;
}
const COLLECTIONS_DATA  = window.COLLECTIONS || [];
const SIDEBAR_SECTIONS_DATA = window.SIDEBAR_SECTIONS || [];
const AXES_DATA         = window.AXES || {};
const applySidebarFn    = window.applySidebarFn || ((tiles) => tiles);
const filterTilesFn     = window.filterTilesFn  || ((tiles) => tiles);
const tileHasFn         = window.tileHasFn      || (() => false);
const resolveFilterFn   = window.resolveFilter  || ((p) => p || { axis: 'all', value: '', label: 'All Tiles' });

// ─── Helpers ─────────────────────────────────────────────────────────
// Read the first image from a tile that's safe to show as a hero/thumb.
// Mirrors the storefront's imagesOf() but stays local so this file is
// self-contained.
function pSightImg(tile)   { return (Array.isArray(tile?.sightImages)   && tile.sightImages[0])   || tile?.img || ''; }
function pGalleryImgs(tile){ return Array.isArray(tile?.galleryImages) ? tile.galleryImages : []; }
function pColourImg(tile)  {
  if (tile?.colourImage) return tile.colourImage;
  const g = pGalleryImgs(tile);
  return g[0] || pSightImg(tile);
}
function pFirst(t, key)    { const a = t?.[key]; return Array.isArray(a) ? a[0] : null; }

// ─── PhoneHeader ─────────────────────────────────────────────────────
// Sticky top bar. Logo on the left, action icons on the right
// (search + menu). Shrinks to ~56px tall. Background is white with
// a slight backdrop blur so content scrolls underneath.
function PhoneHeader({ navigate, onOpenSearch, onOpenCart, onOpenMenu }) {
  // Reads favourites count direct from the store so the badge stays
  // live without having to thread it down from App.jsx.
  const { favourites } = useAccount();
  const favCount = favourites?.length || 0;
  return (
    <header style={{
      position: 'sticky', top: 0, zIndex: 50,
      display: 'flex', alignItems: 'center', justifyContent: 'space-between',
      padding: '10px 16px',
      background: 'rgba(255,255,255,0.96)',
      backdropFilter: 'saturate(140%) blur(12px)',
      WebkitBackdropFilter: 'saturate(140%) blur(12px)',
      borderBottom: '1px solid var(--cream-deep)',
      height: '56px',
    }}>
      <button
        type="button"
        onClick={() => navigate('home')}
        aria-label="Venoraa home"
        style={{
          background: 'none', border: 'none', padding: 0, cursor: 'pointer',
          display: 'flex', alignItems: 'center', height: '36px',
        }}
      >
        <img src="/assets/brand/venoraa-logo.svg" alt="Venoraa" style={{ height: '32px', width: 'auto', display: 'block' }}/>
      </button>
      <div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
        <button type="button" onClick={onOpenSearch} aria-label="Search" style={pIconBtn}>
          <svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.6"><circle cx="9" cy="9" r="6"/><path d="M14 14l4 4" strokeLinecap="round"/></svg>
        </button>
        {/* Favourites / Selection — opens the same drawer used on
            desktop. Badge surfaces fav count so users always know
            how many tiles are sitting in their selection. */}
        <button type="button" onClick={onOpenCart} aria-label={`Selection (${favCount})`} style={{ ...pIconBtn, position: 'relative' }}>
          <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><path d="M12 21s-7-4.35-7-10a4 4 0 0 1 7-2.65A4 4 0 0 1 19 11c0 5.65-7 10-7 10z"/></svg>
          {favCount > 0 && (
            <span style={{
              position: 'absolute', top: '4px', right: '4px',
              background: 'var(--terracotta)', color: 'white',
              minWidth: '16px', height: '16px', borderRadius: '8px',
              display: 'flex', alignItems: 'center', justifyContent: 'center',
              fontFamily: 'var(--sans)', fontSize: '10px', fontWeight: 500, padding: '0 4px',
            }}>{favCount}</span>
          )}
        </button>
        <button type="button" onClick={onOpenMenu} aria-label="Menu" style={pIconBtn}>
          <svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.6"><path d="M3 6h14M3 10h14M3 14h14" strokeLinecap="round"/></svg>
        </button>
      </div>
    </header>
  );
}
const pIconBtn = {
  width: '40px', height: '40px',
  background: 'none', border: 'none', padding: 0, cursor: 'pointer',
  color: 'var(--dark)',
  display: 'flex', alignItems: 'center', justifyContent: 'center',
};

// ─── PhoneMenu ───────────────────────────────────────────────────────
// Slide-down full-screen menu triggered by the header's menu icon.
function PhoneMenu({ open, onClose, navigate }) {
  const { favourites } = useAccount();
  if (!open) return null;
  const go = (page, data) => { navigate(page, data); onClose(); };
  const openSelection = () => {
    onClose();
    if (typeof window.QW_openCart === 'function') window.QW_openCart();
  };
  return (
    <div
      style={{
        position: 'fixed', inset: 0, zIndex: 100,
        background: 'var(--cream)',
        display: 'flex', flexDirection: 'column',
        padding: '12px 0 0',
        animation: 'pMenuIn 0.25s var(--ease-out)',
      }}
    >
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '4px 16px 12px' }}>
        <span style={{ fontFamily: 'var(--sans)', fontSize: '11px', fontWeight: 500, letterSpacing: '0.22em', textTransform: 'uppercase', color: 'var(--dark-mid)' }}>Menu</span>
        <button type="button" onClick={onClose} aria-label="Close" style={pIconBtn}>
          <svg width="22" height="22" viewBox="0 0 22 22" fill="none" stroke="currentColor" strokeWidth="1.6"><path d="M5 5l12 12M17 5L5 17" strokeLinecap="round"/></svg>
        </button>
      </div>
      <nav style={{ display: 'flex', flexDirection: 'column', padding: '8px 24px 32px', gap: 0 }}>
        {[
          { page: 'collections', label: 'Collections' },
          { page: 'journal',     label: 'Journal' },
          { page: 'quote',       label: 'Request a quote' },
          { page: 'account',     label: 'Account' },
        ].map(item => (
          <button
            key={item.page}
            type="button"
            onClick={() => go(item.page)}
            style={{
              padding: '22px 0', background: 'none', border: 'none',
              borderBottom: '1px solid var(--cream-deep)', cursor: 'pointer',
              textAlign: 'left',
              fontFamily: 'var(--serif)', fontSize: '30px', fontWeight: 300, fontStyle: 'italic',
              color: 'var(--dark)',
            }}
          >{item.label}</button>
        ))}
        {/* Selection — opens the favourites drawer rather than a
            page. Shown alongside the page links so it's reachable
            from anywhere on phone. */}
        <button
          type="button"
          onClick={openSelection}
          style={{
            padding: '22px 0', background: 'none', border: 'none',
            borderBottom: '1px solid var(--cream-deep)', cursor: 'pointer',
            textAlign: 'left',
            fontFamily: 'var(--serif)', fontSize: '30px', fontWeight: 300, fontStyle: 'italic',
            color: 'var(--dark)',
          }}
        >Selection ({favourites?.length || 0})</button>
      </nav>
      <style>{`@keyframes pMenuIn { from { opacity: 0; transform: translateY(-12px); } to { opacity: 1; transform: translateY(0); } }`}</style>
    </div>
  );
}

// ─── PhoneFooter ─────────────────────────────────────────────────────
// Single-column minimal footer below every phone page.
function PhoneFooter({ navigate }) {
  return (
    <footer style={{
      borderTop: '1px solid var(--cream-deep)',
      padding: '32px 20px 28px',
      background: 'white',
      textAlign: 'center',
    }}>
      <img src="/assets/brand/venoraa-logo.svg" alt="Venoraa" style={{ height: '26px', width: 'auto', display: 'inline-block', marginBottom: '20px' }}/>
      <div style={{ display: 'flex', flexDirection: 'column', gap: '12px', alignItems: 'center' }}>
        {[['collections', 'Collections'], ['journal', 'Journal'], ['quote', 'Request a quote']].map(([p, l]) => (
          <button key={p} type="button" onClick={() => navigate(p)} style={{
            background: 'none', border: 'none', padding: '4px 0', cursor: 'pointer',
            fontFamily: 'var(--sans)', fontSize: '13px', color: 'var(--dark)',
          }}>{l}</button>
        ))}
      </div>
      <p style={{ fontFamily: 'var(--sans)', fontSize: '11px', color: 'var(--dark-mid)', marginTop: '24px' }}>
        © {new Date().getFullYear()} Venoraa
      </p>
    </footer>
  );
}

// ─── PhonePageShell ──────────────────────────────────────────────────
// Wraps every phone page with the header, the menu drawer, and the
// footer. Keeps each PhoneXxx component focused on its own content.
function PhonePageShell({ navigate, children, hideFooter, hideHeader }) {
  const [menuOpen, setMenuOpen] = useState(false);
  return (
    <div style={{ minHeight: '100vh', background: 'white', display: 'flex', flexDirection: 'column' }}>
      {!hideHeader && (
        <PhoneHeader
          navigate={navigate}
          onOpenSearch={() => { if (typeof window.QW_openSearch === 'function') window.QW_openSearch(); }}
          onOpenCart={() => { if (typeof window.QW_openCart === 'function') window.QW_openCart(); }}
          onOpenMenu={() => setMenuOpen(true)}
        />
      )}
      <PhoneMenu open={menuOpen} onClose={() => setMenuOpen(false)} navigate={navigate}/>
      <main style={{ flex: 1 }}>{children}</main>
      {!hideFooter && <PhoneFooter navigate={navigate}/>}
    </div>
  );
}

// ─── PhoneHome ───────────────────────────────────────────────────────
// Single full-height hero with auto-rotating swipeable slides; then
// editorial sections below. Optimised for portrait phone scrolling.
function PhoneHome({ navigate }) {
  const slides = HERO_SLIDES_DATA;
  const [active, setActive] = useState(0);
  const [paused, setPaused] = useState(false);
  useEffect(() => {
    if (paused || slides.length <= 1) return;
    const id = setInterval(() => setActive(s => (s + 1) % slides.length), 5800);
    return () => clearInterval(id);
  }, [paused, slides.length]);
  // Touch swipe between slides
  const touchRef = useRef({ x: 0, t: 0 });
  function onTouchStart(e) { touchRef.current = { x: e.touches[0].clientX, t: Date.now() }; }
  function onTouchEnd(e) {
    const dx = e.changedTouches[0].clientX - touchRef.current.x;
    if (Math.abs(dx) < 50) return;
    setActive(s => (dx < 0 ? (s + 1) : (s - 1 + slides.length)) % slides.length);
  }

  const slide = slides[active] || {};
  const enterSlide = () => {
    if (slide.target === 'collections') return navigate('collections');
    if (typeof slide.target === 'number' && COLLECTIONS_DATA[slide.target]) {
      const col = COLLECTIONS_DATA[slide.target];
      return navigate('collection', { id: col.id, label: col.label + ' Tiles', ...col });
    }
    navigate('collections');
  };

  // Categories: same 6 vibe slides used in the hero, rendered as a
  // 2-col grid below.
  const categories = slides.slice(1);

  return (
    <PhonePageShell navigate={navigate}>
      {/* HERO */}
      <section
        style={{ position: 'relative', height: '70vh', minHeight: '440px', overflow: 'hidden', background: 'var(--dark)' }}
        onMouseEnter={() => setPaused(true)}
        onMouseLeave={() => setPaused(false)}
        onTouchStart={onTouchStart}
        onTouchEnd={onTouchEnd}
      >
        {slides.map((s, i) => (
          <img
            key={s.id || i}
            src={s.img}
            alt=""
            style={{
              position: 'absolute', inset: 0,
              width: '100%', height: '100%', objectFit: 'cover',
              opacity: i === active ? 1 : 0,
              transition: 'opacity 0.9s var(--ease-out)',
              filter: 'brightness(0.78) saturate(1.04)',
            }}
          />
        ))}
        <div aria-hidden style={{
          position: 'absolute', inset: 0,
          background: 'linear-gradient(180deg, rgba(20,16,12,0.12) 0%, rgba(20,16,12,0.08) 40%, rgba(20,16,12,0.7) 100%)',
        }}/>
        <div style={{ position: 'absolute', left: '20px', right: '20px', bottom: '32px', color: 'white' }}>
          <p style={{ fontFamily: 'var(--sans)', fontSize: '10px', letterSpacing: '0.28em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.8)', marginBottom: '14px' }}>
            London Tile Craft
          </p>
          <h1 key={'h-' + active} style={{
            fontFamily: 'var(--serif)', fontWeight: 300,
            fontSize: 'clamp(38px, 10vw, 56px)', lineHeight: 1.0,
            letterSpacing: '-0.02em', marginBottom: '14px',
            animation: 'pHeroIn 0.6s var(--ease-out) both',
          }}>
            <span style={{ display: 'block' }}>{slide.line1}</span>
            <em style={{ fontStyle: 'italic' }}>{slide.line2}</em>
          </h1>
          <p key={'b-' + active} style={{
            fontFamily: 'var(--sans)', fontSize: '14px', lineHeight: 1.55,
            color: 'rgba(255,255,255,0.85)', maxWidth: '320px', marginBottom: '22px',
            animation: 'pHeroIn 0.6s 0.08s var(--ease-out) both',
          }}>{slide.body}</p>
          <button
            type="button"
            onClick={enterSlide}
            style={{
              padding: '14px 22px', background: 'white', color: 'var(--dark)',
              border: 'none', cursor: 'pointer',
              fontFamily: 'var(--sans)', fontSize: '11px', fontWeight: 500,
              letterSpacing: '0.18em', textTransform: 'uppercase',
            }}
          >{slide.ctaLabel || 'Explore'} →</button>
        </div>
        {/* Slide pips */}
        <div style={{ position: 'absolute', left: '50%', bottom: '14px', transform: 'translateX(-50%)', display: 'flex', gap: '6px' }} aria-hidden>
          {slides.map((_, i) => (
            <span key={i} style={{
              width: i === active ? '18px' : '5px', height: '5px',
              background: i === active ? 'white' : 'rgba(255,255,255,0.45)',
              borderRadius: '999px',
              transition: 'all 0.4s var(--ease-out)',
            }}/>
          ))}
        </div>
        <style>{`@keyframes pHeroIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }`}</style>
      </section>

      {/* Categories grid (2-col) */}
      <section style={{ padding: '36px 16px 12px' }}>
        <p className="t-label" style={{ color: 'var(--terracotta)', marginBottom: '10px' }}>Categories</p>
        <h2 style={{ fontFamily: 'var(--serif)', fontWeight: 300, fontSize: 'clamp(28px, 7vw, 40px)', lineHeight: 1, letterSpacing: '-0.02em', marginBottom: '20px' }}>
          Six vibes,<br/><em>one studio.</em>
        </h2>
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px' }}>
          {categories.map(c => (
            <button
              key={c.id}
              type="button"
              onClick={() => {
                if (c.target === 'collections') return navigate('collections');
                if (typeof c.target === 'number' && COLLECTIONS_DATA[c.target]) {
                  const col = COLLECTIONS_DATA[c.target];
                  return navigate('collection', { id: col.id, label: col.label + ' Tiles', ...col });
                }
                navigate('collections');
              }}
              style={{
                position: 'relative', background: 'var(--cream-mid)',
                border: 'none', padding: 0, cursor: 'pointer',
                aspectRatio: '3 / 4', overflow: 'hidden',
              }}
            >
              <img src={c.img} alt={c.label} style={{
                position: 'absolute', inset: 0, width: '100%', height: '100%',
                objectFit: 'cover',
              }}/>
              <div aria-hidden style={{
                position: 'absolute', inset: 0,
                background: 'linear-gradient(180deg, transparent 50%, rgba(15,12,10,0.78) 100%)',
              }}/>
              <div style={{ position: 'absolute', left: '14px', right: '14px', bottom: '14px', color: 'white', textAlign: 'left' }}>
                <p style={{ fontFamily: 'var(--serif)', fontWeight: 300, fontSize: '20px', lineHeight: 1, margin: 0 }}>{c.label}</p>
                <p style={{ fontFamily: 'var(--sans)', fontSize: '9px', letterSpacing: '0.16em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.72)', marginTop: '6px' }}>{c.tagline}</p>
              </div>
            </button>
          ))}
        </div>
      </section>

      {/* Philosophy band */}
      <section style={{ padding: '40px 20px', background: 'var(--cream)' }}>
        <p className="t-label" style={{ color: 'var(--terracotta)', marginBottom: '12px' }}>Our philosophy</p>
        <h2 style={{ fontFamily: 'var(--serif)', fontWeight: 300, fontSize: 'clamp(28px, 7vw, 40px)', lineHeight: 1.1, letterSpacing: '-0.015em', color: 'var(--dark)', marginBottom: '20px' }}>
          A tile is not a product.<br/>
          <em style={{ fontStyle: 'italic' }}>It's a decision you live with.</em>
        </h2>
        <p style={{ fontFamily: 'var(--sans)', fontSize: '14px', lineHeight: 1.7, color: 'var(--dark-mid)' }}>
          A London tile studio with almost five decades of curation behind it. We don't make tiles — we choose them. Every surface in our catalogue is seen, touched and approved by us, sourced direct from the makers we trust, and dispatched from our Wimbledon warehouse.
        </p>
      </section>

      {/* Journal teasers */}
      <section style={{ padding: '40px 16px 56px' }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: '20px' }}>
          <h2 style={{ fontFamily: 'var(--serif)', fontWeight: 300, fontSize: 'clamp(24px, 6vw, 32px)', lineHeight: 1, letterSpacing: '-0.02em', margin: 0 }}>
            Journal
          </h2>
          <button type="button" onClick={() => navigate('journal')} style={{
            background: 'none', border: 'none', cursor: 'pointer',
            fontFamily: 'var(--sans)', fontSize: '11px', letterSpacing: '0.14em', textTransform: 'uppercase', color: 'var(--dark-mid)',
            padding: 0,
          }}>Read all →</button>
        </div>
        <PhoneJournalTeasers navigate={navigate}/>
      </section>

      {/* Bottom CTA */}
      <section style={{ padding: '0 16px 48px' }}>
        <button
          type="button"
          onClick={() => navigate('quote')}
          style={{
            width: '100%', padding: '18px',
            background: 'var(--dark)', color: 'var(--cream)',
            border: 'none', cursor: 'pointer',
            fontFamily: 'var(--sans)', fontSize: '12px', fontWeight: 500,
            letterSpacing: '0.2em', textTransform: 'uppercase',
          }}
        >Request a quote →</button>
      </section>
    </PhonePageShell>
  );
}

// Pulls the journal articles from the global ARTICLES const exposed by
// Journal.jsx, falls back to an empty list. Renders the first 3 as
// stacked cards.
function PhoneJournalTeasers({ navigate }) {
  const all = usePhoneArticles();
  const articles = (all.length ? all : ARTICLES_DATA).slice(0, 3);
  return (
    <div style={{ display: 'grid', gap: '20px' }}>
      {articles.map(a => (
        <button
          key={a.id}
          type="button"
          onClick={() => navigate('journal', { articleId: a.id })}
          style={{ background: 'none', border: 'none', padding: 0, cursor: 'pointer', textAlign: 'left' }}
        >
          <div style={{ aspectRatio: '16 / 10', overflow: 'hidden', background: 'var(--cream-deep)' }}>
            <img src={a.thumb} alt={a.title} style={{ width: '100%', height: '100%', objectFit: 'cover' }}/>
          </div>
          <p className="t-label" style={{ color: 'var(--terracotta)', margin: '12px 0 6px' }}>{a.category} · {a.read}</p>
          <h3 style={{ fontFamily: 'var(--serif)', fontSize: '20px', fontWeight: 400, lineHeight: 1.2, margin: 0 }}>{a.title}</h3>
        </button>
      ))}
    </div>
  );
}

// ─── PhoneListing ────────────────────────────────────────────────────
// Sticky title + filter/sort bar. 2-column tile grid. Bottom-sheet
// filter drawer.
function PhoneListing({ filter, navigate }) {
  const tiles = useAllTilesHook ? useAllTilesHook() : usePhoneTiles();
  const collectionsMeta = useCollectionsMetaHook ? useCollectionsMetaHook() : usePhoneCollectionsMeta();

  // Filter spec from URL/nav. Reuse the desktop resolveFilter +
  // applySidebar via the bound module-level functions.
  const spec = useMemo(() => resolveFilterFn(filter), [filter]);

  // Per-axis selection state — mirrors CollectionDetail's selected.
  const [selected, setSelected] = useState(() => ({
    room: new Set(), colour: new Set(), style: new Set(), shape: new Set(), finish: new Set(),
  }));
  useEffect(() => {
    if (['room', 'colour', 'style', 'shape'].includes(spec.axis)) {
      setSelected(prev => ({ ...prev, [spec.axis]: new Set([spec.value]) }));
    }
  }, [spec.axis, spec.value]);

  const [sort, setSort] = useState('featured');
  const [drawerOpen, setDrawerOpen] = useState(false);
  useEffect(() => {
    if (drawerOpen) { document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = ''; }; }
  }, [drawerOpen]);

  const baseTiles = useMemo(() => {
    if (spec.axis === 'cat') return filterTilesFn(tiles, spec);
    return tiles;
  }, [tiles, spec]);

  const filtered = useMemo(() => applySidebarFn(baseTiles, selected), [baseTiles, selected]);

  const products = useMemo(() => {
    const list = filtered.slice();
    if (sort === 'name-asc')  list.sort((a, b) => (a.name||'').localeCompare(b.name||''));
    if (sort === 'name-desc') list.sort((a, b) => (b.name||'').localeCompare(a.name||''));
    return list;
  }, [filtered, sort]);

  const PAGE_SIZE = 24;
  const [visibleCount, setVisibleCount] = useState(PAGE_SIZE);
  useEffect(() => { setVisibleCount(PAGE_SIZE); }, [spec.axis, spec.value, selected, sort]);
  const visible = products.slice(0, visibleCount);
  const hasMore = visibleCount < products.length;

  const activeCount = ['room', 'colour', 'style', 'shape', 'finish']
    .reduce((n, k) => n + (selected[k]?.size || 0), 0);
  const resetAll = () => setSelected({
    room: new Set(), colour: new Set(), style: new Set(), shape: new Set(), finish: new Set(),
  });
  const toggle = (axis, id) => setSelected(prev => {
    const next = { ...prev, [axis]: new Set(prev[axis]) };
    if (next[axis].has(id)) next[axis].delete(id); else next[axis].add(id);
    return next;
  });

  return (
    <PhonePageShell navigate={navigate}>
      {/* Title + count */}
      <div style={{ padding: '20px 16px 8px' }}>
        <p style={{ fontFamily: 'var(--sans)', fontSize: '10px', letterSpacing: '0.22em', textTransform: 'uppercase', color: 'var(--dark-mid)', margin: '0 0 6px' }}>
          {spec.axis === 'all' ? 'All tiles' : (AXES_DATA[spec.axis]?.label || '').replace('By ', '')}
        </p>
        <h1 style={{ fontFamily: 'var(--serif)', fontWeight: 300, fontSize: 'clamp(28px, 7vw, 40px)', lineHeight: 1.05, letterSpacing: '-0.02em', margin: 0 }}>
          {spec.label || 'All Tiles'}
        </h1>
      </div>

      {/* Sticky filter + sort bar (sticks under the header) */}
      <div style={{
        position: 'sticky', top: '56px', zIndex: 30,
        display: 'flex', gap: '8px', padding: '10px 16px',
        background: 'rgba(255,255,255,0.96)',
        backdropFilter: 'saturate(140%) blur(12px)',
        WebkitBackdropFilter: 'saturate(140%) blur(12px)',
        borderBottom: '1px solid var(--cream-deep)',
      }}>
        <button
          type="button"
          onClick={() => setDrawerOpen(true)}
          style={{
            flex: 1, padding: '12px 16px',
            background: activeCount > 0 ? 'var(--dark)' : 'white',
            color:      activeCount > 0 ? 'white' : 'var(--dark)',
            border: `1px solid ${activeCount > 0 ? 'var(--dark)' : 'var(--cream-deep)'}`,
            fontFamily: 'var(--sans)', fontSize: '12px',
            letterSpacing: '0.14em', textTransform: 'uppercase',
            cursor: 'pointer', minHeight: '44px',
          }}
        >Filter{activeCount > 0 ? ` · ${activeCount}` : ''}</button>
        <select
          value={sort}
          onChange={e => setSort(e.target.value)}
          style={{
            flex: 1, padding: '12px 14px',
            background: 'white', color: 'var(--dark)',
            border: '1px solid var(--cream-deep)',
            fontFamily: 'var(--sans)', fontSize: '12px',
            letterSpacing: '0.06em', textTransform: 'uppercase',
            cursor: 'pointer', minHeight: '44px', outline: 'none',
          }}
        >
          <option value="featured">Sort: Featured</option>
          <option value="name-asc">Sort: Name A–Z</option>
          <option value="name-desc">Sort: Name Z–A</option>
        </select>
      </div>

      {/* Tile grid */}
      <div style={{ padding: '12px 12px 16px' }}>
        <p style={{ fontFamily: 'var(--sans)', fontSize: '11px', color: 'var(--dark-mid)', letterSpacing: '0.06em', margin: '4px 4px 12px' }}>
          {products.length} {products.length === 1 ? 'tile' : 'tiles'}
        </p>
        {products.length === 0 ? (
          <div style={{ textAlign: 'center', padding: '64px 20px' }}>
            <p style={{ fontFamily: 'var(--serif)', fontSize: '20px', fontWeight: 300, color: 'var(--dark-mid)', marginBottom: '16px' }}>
              No tiles match every filter.
            </p>
            <button type="button" onClick={resetAll} style={{
              background: 'none', border: '1px solid var(--dark)', cursor: 'pointer',
              padding: '12px 22px', fontFamily: 'var(--sans)', fontSize: '11px',
              letterSpacing: '0.16em', textTransform: 'uppercase', color: 'var(--dark)',
            }}>Clear all</button>
          </div>
        ) : (
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px 8px' }}>
            {visible.map(t => <PhoneTileCard key={t.id} tile={t} navigate={navigate}/>)}
          </div>
        )}
        {hasMore && (
          <div style={{ textAlign: 'center', padding: '24px 16px 8px' }}>
            <p style={{ fontFamily: 'var(--sans)', fontSize: '10px', letterSpacing: '0.16em', textTransform: 'uppercase', color: 'var(--dark-mid)', marginBottom: '10px' }}>
              Showing {visible.length} of {products.length}
            </p>
            <button
              type="button"
              onClick={() => setVisibleCount(c => c + PAGE_SIZE)}
              style={{
                padding: '14px 28px',
                background: 'var(--dark)', color: 'var(--cream)',
                border: 'none', cursor: 'pointer',
                fontFamily: 'var(--sans)', fontSize: '11px', fontWeight: 500,
                letterSpacing: '0.2em', textTransform: 'uppercase',
              }}
            >Load {Math.min(PAGE_SIZE, products.length - visibleCount)} more</button>
          </div>
        )}
      </div>

      {/* Filter drawer */}
      {drawerOpen && (
        <PhoneFilterDrawer
          tiles={tiles}
          baseTiles={baseTiles}
          selected={selected}
          toggle={toggle}
          resetAll={resetAll}
          onClose={() => setDrawerOpen(false)}
          productCount={products.length}
        />
      )}
    </PhonePageShell>
  );
}

// Tile card for the phone listing — image + name + collection.
function PhoneTileCard({ tile, navigate }) {
  const { favourites, toggleFavourite } = useAccount();
  const faved = favourites?.includes(tile.id);
  return (
    <div
      onClick={() => navigate('product', tile)}
      style={{ cursor: 'pointer' }}
    >
      <div className="tile-frame" style={{ position: 'relative', aspectRatio: '1', overflow: 'hidden' }}>
        <img
          src={pSightImg(tile)} alt={tile.name}
          loading="lazy"
          style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
          onError={e => { e.target.style.display = 'none'; }}
        />
        <button
          type="button"
          onClick={(e) => { e.stopPropagation(); toggleFavourite(tile.id); }}
          aria-label={faved ? 'Remove from favourites' : 'Add to favourites'}
          style={{
            position: 'absolute', top: '8px', right: '8px',
            width: '32px', height: '32px',
            background: 'rgba(255,255,255,0.92)',
            border: 'none', cursor: 'pointer',
            color: faved ? 'var(--terracotta)' : 'var(--dark)',
            fontSize: '15px', lineHeight: 1,
            display: 'flex', alignItems: 'center', justifyContent: 'center',
          }}
        >{faved ? '♥' : '♡'}</button>
      </div>
      <div style={{ padding: '10px 2px 0' }}>
        {tile.collection && (
          <p style={{ fontFamily: 'var(--sans)', fontSize: '9px', letterSpacing: '0.18em', textTransform: 'uppercase', color: 'var(--terracotta)', margin: '0 0 4px' }}>
            {tile.collection}
          </p>
        )}
        <p style={{
          fontFamily: 'var(--serif)', fontStyle: 'italic', fontWeight: 400,
          fontSize: '15px', lineHeight: 1.2, color: 'var(--dark)',
          margin: 0, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
        }}>{tile.name}</p>
      </div>
    </div>
  );
}

// Bottom-sheet filter drawer.
function PhoneFilterDrawer({ tiles, baseTiles, selected, toggle, resetAll, onClose, productCount }) {
  const sections = SIDEBAR_SECTIONS_DATA;
  const [openSet, setOpenSet] = useState(() => new Set(sections.length ? [sections[0].key] : []));

  return (
    <div
      role="dialog" aria-modal="true"
      style={{ position: 'fixed', inset: 0, zIndex: 200, background: 'rgba(0,0,0,0.45)', display: 'flex', alignItems: 'flex-end' }}
      onClick={onClose}
    >
      <div
        onClick={e => e.stopPropagation()}
        style={{
          background: 'white', width: '100%',
          maxHeight: '88vh',
          borderRadius: '16px 16px 0 0',
          display: 'flex', flexDirection: 'column',
          overflow: 'hidden',
          animation: 'pSheetUp 0.28s var(--ease-out)',
        }}
      >
        <div style={{
          position: 'relative',
          display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          padding: '18px 20px 14px', borderBottom: '1px solid var(--cream-deep)',
        }}>
          <div style={{ width: '36px', height: '4px', background: 'var(--cream-deep)', borderRadius: '999px', position: 'absolute', top: '8px', left: '50%', transform: 'translateX(-50%)' }}/>
          <p style={{ fontFamily: 'var(--sans)', fontSize: '11px', fontWeight: 600, letterSpacing: '0.22em', textTransform: 'uppercase', color: 'var(--dark)', margin: 0 }}>Filter</p>
          <button type="button" onClick={onClose} aria-label="Close" style={pIconBtn}>
            <svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.6"><path d="M5 5l10 10M15 5L5 15" strokeLinecap="round"/></svg>
          </button>
        </div>

        <div style={{ flex: 1, overflowY: 'auto', padding: '4px 20px 16px' }}>
          {sections.map(sec => {
            const opts = sec.getOptions(tiles);
            const sel  = selected[sec.key];
            const open = openSet.has(sec.key);
            return (
              <div key={sec.key} style={{ borderBottom: '1px solid var(--cream-deep)' }}>
                <button
                  type="button"
                  onClick={() => setOpenSet(prev => { const n = new Set(prev); n.has(sec.key) ? n.delete(sec.key) : n.add(sec.key); return n; })}
                  style={{
                    width: '100%', display: 'flex', justifyContent: 'space-between', alignItems: 'center',
                    background: 'none', border: 'none', padding: '18px 0', cursor: 'pointer',
                    fontFamily: 'var(--sans)', fontSize: '12px', fontWeight: 500,
                    letterSpacing: '0.18em', textTransform: 'uppercase', color: 'var(--dark)',
                  }}
                >
                  <span>{sec.title}{sel?.size > 0 ? ` · ${sel.size}` : ''}</span>
                  <span aria-hidden style={{ fontSize: '14px', color: 'var(--dark-mid)' }}>{open ? '−' : '+'}</span>
                </button>
                {open && (
                  <div style={{ paddingBottom: '14px' }}>
                    {opts.map(opt => {
                      const checked = sel?.has(opt.id);
                      const count = baseTiles.filter(t => tileHasFn(t, sec.key, opt.id)).length;
                      const dim = count === 0 && !checked;
                      return (
                        <label key={opt.id} style={{
                          display: 'flex', alignItems: 'center', gap: '12px',
                          padding: '10px 0', cursor: dim ? 'default' : 'pointer',
                          opacity: dim ? 0.4 : 1,
                          fontFamily: 'var(--sans)', fontSize: '14px', color: 'var(--dark)',
                        }}>
                          <input
                            type="checkbox"
                            checked={!!checked}
                            disabled={dim}
                            onChange={() => toggle(sec.key, opt.id)}
                            style={{ width: '20px', height: '20px', accentColor: 'var(--dark)' }}
                          />
                          {opt.swatch && (
                            <span aria-hidden style={{
                              display: 'inline-block', width: '18px', height: '18px',
                              background: opt.swatch, border: '1px solid var(--cream-deep)', flex: '0 0 auto',
                            }}/>
                          )}
                          <span style={{ flex: 1 }}>{opt.label}</span>
                          <span style={{ fontSize: '11px', color: 'var(--dark-mid)' }}>{count}</span>
                        </label>
                      );
                    })}
                  </div>
                )}
              </div>
            );
          })}
        </div>

        {/* Sticky footer */}
        <div style={{
          display: 'flex', gap: '10px',
          padding: '14px 20px calc(14px + env(safe-area-inset-bottom, 0px))',
          borderTop: '1px solid var(--cream-deep)', background: 'white',
        }}>
          <button type="button" onClick={resetAll} style={{
            flex: '0 0 auto', padding: '14px 20px',
            background: 'white', color: 'var(--dark)',
            border: '1px solid var(--cream-deep)',
            fontFamily: 'var(--sans)', fontSize: '11px',
            letterSpacing: '0.16em', textTransform: 'uppercase', cursor: 'pointer',
          }}>Reset</button>
          <button type="button" onClick={onClose} style={{
            flex: 1, padding: '14px 20px',
            background: 'var(--dark)', color: 'white',
            border: '1px solid var(--dark)',
            fontFamily: 'var(--sans)', fontSize: '11px', fontWeight: 500,
            letterSpacing: '0.18em', textTransform: 'uppercase', cursor: 'pointer',
          }}>Show {productCount} tile{productCount === 1 ? '' : 's'}</button>
        </div>
        <style>{`@keyframes pSheetUp { from { transform: translateY(100%); } to { transform: translateY(0); } }`}</style>
      </div>
    </div>
  );
}

// ─── PhoneProduct ────────────────────────────────────────────────────
// Full-bleed image carousel, name, swatches, accordion specs, sticky
// CTA at the bottom.
function PhoneProduct({ product, navigate }) {
  const allTiles = useAllTilesHook ? useAllTilesHook() : usePhoneTiles();
  const collectionsMeta = useCollectionsMetaHook ? useCollectionsMetaHook() : usePhoneCollectionsMeta();
  const { favourites, toggleFavourite } = useAccount();
  const faved = favourites?.includes(product.id);

  const sights = useMemo(() => {
    if (Array.isArray(product.sightImages) && product.sightImages.length) return product.sightImages;
    return product.img ? [product.img] : [];
  }, [product.sightImages, product.img]);
  const gallery = pGalleryImgs(product);

  const [active, setActive] = useState(0);
  useEffect(() => { setActive(0); }, [product.id]);
  // Touch swipe through hero images
  const touchRef = useRef({ x: 0 });
  function onTouchStart(e) { touchRef.current.x = e.touches[0].clientX; }
  function onTouchEnd(e) {
    const dx = e.changedTouches[0].clientX - touchRef.current.x;
    if (Math.abs(dx) < 50) return;
    setActive(s => (dx < 0 ? (s + 1) : (s - 1 + sights.length)) % sights.length);
  }

  // Sizes / finishes derived
  const sizes = useMemo(() => Array.isArray(product.sizes) ? product.sizes : (product.size ? [product.size] : []), [product.sizes, product.size]);
  const finishes = useMemo(() => Array.isArray(product.finishes) ? product.finishes : (product.finish ? [product.finish] : []), [product.finishes, product.finish]);
  const [activeSize, setActiveSize] = useState(sizes[0] || '');
  const [activeFinish, setActiveFinish] = useState(finishes[0] || '');
  useEffect(() => { setActiveSize(sizes[0] || ''); setActiveFinish(finishes[0] || ''); }, [product.id]);

  // Colour variants (same collection, admin-ordered)
  const colourSwatches = useMemo(() => {
    if (!product.collection || !allTiles.length) return [];
    const meta = collectionsMeta[product.collection] || {};
    const hidden = new Set(Array.isArray(meta.hidden) ? meta.hidden : []);
    const order  = Array.isArray(meta.order) ? meta.order : [];
    const seen = new Set();
    const pool = [];
    for (const t of allTiles) {
      if (t.id === product.id) continue;
      if (t.collection !== product.collection) continue;
      if (hidden.has(t.id)) continue;
      if (seen.has(t.name)) continue;
      seen.add(t.name);
      pool.push(t);
    }
    const idx = (id) => { const i = order.indexOf(id); return i < 0 ? Infinity : i; };
    pool.sort((a, b) => idx(a.id) - idx(b.id));
    return pool;
  }, [allTiles, collectionsMeta, product.collection, product.id]);

  // Accordion: description + spec
  const [openSpec, setOpenSpec] = useState(false);

  // Suggestions (capped at 6 for the horizontal scroll)
  const suggestions = useMemo(() => {
    if (!allTiles.length) return [];
    const others = allTiles.filter(t => t.id !== product.id);
    const seen = new Set();
    const buckets = [[], [], [], []];
    const arr = v => Array.isArray(v) ? v : (v ? [v] : []);
    const pColours = new Set(arr(product.colours).concat(arr(product.colour)).filter(Boolean));
    const pStyles  = new Set(arr(product.styles ).concat(arr(product.style )).filter(Boolean));
    for (const t of others) {
      if (seen.has(t.name)) continue;
      const tColours = new Set(arr(t.colours).concat(arr(t.colour)).filter(Boolean));
      const tStyles  = new Set(arr(t.styles ).concat(arr(t.style )).filter(Boolean));
      const cMatch = [...pColours].some(c => tColours.has(c));
      const sMatch = [...pStyles ].some(s => tStyles.has(s));
      let b;
      if (sMatch && cMatch) b = 0;
      else if (cMatch)      b = 1;
      else if (sMatch)      b = 2;
      else                  b = 3;
      buckets[b].push(t);
      seen.add(t.name);
    }
    return buckets.flat().slice(0, 8);
  }, [allTiles, product.id, product.colours, product.styles]);

  const submitQuote = () => navigate('quote', { tiles: [{ name: product.name, size: activeSize, finish: activeFinish, sqm: 10 }] });

  return (
    <PhonePageShell navigate={navigate} hideFooter>
      <div style={{ paddingBottom: '88px' }}>
        {/* HERO carousel */}
        <section
          className="tile-frame"
          style={{ position: 'relative', width: '100%', aspectRatio: '4 / 5', overflow: 'hidden', borderRadius: 0 }}
          onTouchStart={onTouchStart}
          onTouchEnd={onTouchEnd}
        >
          {sights.map((src, i) => (
            <img
              key={src + ':' + i}
              src={src}
              alt={i === 0 ? product.name : ''}
              loading={i === 0 ? 'eager' : 'lazy'}
              style={{
                position: 'absolute', inset: 0,
                width: '100%', height: '100%', objectFit: 'cover',
                opacity: i === active ? 1 : 0,
                transition: 'opacity 0.7s var(--ease-out)',
              }}
              onError={e => { e.target.style.display = 'none'; }}
            />
          ))}
          {/* Back button */}
          <button
            type="button"
            onClick={() => navigate('back')}
            aria-label="Back"
            style={{
              position: 'absolute', top: '12px', left: '12px', zIndex: 2,
              width: '40px', height: '40px',
              background: 'rgba(255,255,255,0.94)', border: 'none', cursor: 'pointer',
              display: 'flex', alignItems: 'center', justifyContent: 'center',
              color: 'var(--dark)',
            }}
          >
            <svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.6"><path d="M12 4L6 10l6 6" strokeLinecap="round" strokeLinejoin="round"/></svg>
          </button>
          {/* Heart */}
          <button
            type="button"
            onClick={() => toggleFavourite(product.id)}
            aria-label={faved ? 'Remove from favourites' : 'Add to favourites'}
            style={{
              position: 'absolute', top: '12px', right: '12px', zIndex: 2,
              width: '40px', height: '40px',
              background: 'rgba(255,255,255,0.94)', border: 'none', cursor: 'pointer',
              display: 'flex', alignItems: 'center', justifyContent: 'center',
              color: faved ? 'var(--terracotta)' : 'var(--dark)',
              fontSize: '18px',
            }}
          >{faved ? '♥' : '♡'}</button>
          {/* Dot pips */}
          {sights.length > 1 && (
            <div style={{ position: 'absolute', left: '50%', bottom: '14px', transform: 'translateX(-50%)', display: 'flex', gap: '6px', zIndex: 2 }} aria-hidden>
              {sights.map((_, i) => (
                <span key={i} style={{
                  width: i === active ? '22px' : '6px', height: '6px',
                  background: i === active ? 'white' : 'rgba(255,255,255,0.5)',
                  borderRadius: '999px',
                  transition: 'all 0.35s var(--ease-out)',
                }}/>
              ))}
            </div>
          )}
        </section>

        {/* Name + collection */}
        <section style={{ padding: '24px 20px 0' }}>
          {product.collection && (
            <p style={{ fontFamily: 'var(--sans)', fontSize: '11px', letterSpacing: '0.22em', textTransform: 'uppercase', color: 'var(--terracotta)', margin: '0 0 10px' }}>
              {product.collection}
            </p>
          )}
          <h1 style={{ fontFamily: 'var(--serif)', fontWeight: 300, fontStyle: 'italic', fontSize: 'clamp(32px, 8vw, 44px)', lineHeight: 1.05, letterSpacing: '-0.015em', color: 'var(--dark)', margin: 0 }}>
            {product.name}
          </h1>
          {product.description && (
            <p style={{ fontFamily: 'var(--serif)', fontWeight: 300, fontStyle: 'italic', fontSize: '15px', lineHeight: 1.55, color: 'var(--dark-mid)', margin: '14px 0 0' }}>
              {product.description}
            </p>
          )}
        </section>

        {/* Size selector */}
        {sizes.length > 0 && (
          <section style={{ padding: '28px 20px 0' }}>
            <p className="t-label" style={{ color: 'var(--dark-mid)', margin: '0 0 10px' }}>Size</p>
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
              {sizes.map(s => {
                const on = s === activeSize;
                return (
                  <button
                    key={s}
                    type="button"
                    onClick={() => setActiveSize(s)}
                    style={{
                      padding: '12px 18px', minHeight: '44px',
                      background: on ? 'var(--dark)' : 'white',
                      color:      on ? 'white' : 'var(--dark)',
                      border: `1px solid ${on ? 'var(--dark)' : 'var(--cream-deep)'}`,
                      fontFamily: 'var(--serif)', fontStyle: 'italic',
                      fontSize: '14px', cursor: 'pointer',
                    }}
                  >{s}</button>
                );
              })}
            </div>
          </section>
        )}

        {/* Finish selector */}
        {finishes.length > 0 && (
          <section style={{ padding: '24px 20px 0' }}>
            <p className="t-label" style={{ color: 'var(--dark-mid)', margin: '0 0 10px' }}>Finish</p>
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
              {finishes.map(f => {
                const on = f === activeFinish;
                return (
                  <button
                    key={f}
                    type="button"
                    onClick={() => setActiveFinish(f)}
                    style={{
                      padding: '12px 16px', minHeight: '44px',
                      background: on ? 'var(--dark)' : 'white',
                      color:      on ? 'white' : 'var(--dark)',
                      border: `1px solid ${on ? 'var(--dark)' : 'var(--cream-deep)'}`,
                      fontFamily: 'var(--sans)', fontSize: '12px', letterSpacing: '0.06em',
                      cursor: 'pointer',
                    }}
                  >{f}</button>
                );
              })}
            </div>
          </section>
        )}

        {/* Colour swatches — horizontal scroll strip */}
        {colourSwatches.length > 0 && (
          <section style={{ padding: '28px 0 0' }}>
            <p className="t-label" style={{ color: 'var(--dark-mid)', margin: '0 20px 12px' }}>
              The colours · {colourSwatches.length + 1} variants
            </p>
            <div style={{
              display: 'flex', gap: '10px', overflowX: 'auto',
              padding: '0 20px 4px',
              scrollSnapType: 'x mandatory',
              WebkitOverflowScrolling: 'touch',
            }}>
              {/* Current tile first */}
              <PhoneColourCell tile={product} active onClick={() => {}}/>
              {colourSwatches.map(t => (
                <PhoneColourCell
                  key={t.id}
                  tile={t}
                  onClick={() => navigate('product', t)}
                />
              ))}
            </div>
          </section>
        )}

        {/* Gallery */}
        {gallery.length > 0 && (
          <section style={{ padding: '32px 0 0' }}>
            <p className="t-label" style={{ color: 'var(--dark-mid)', margin: '0 20px 12px' }}>In sight</p>
            <div style={{ display: 'grid', gap: '4px' }}>
              {gallery.map((src, i) => (
                <img key={src+':'+i} src={src} alt="" loading="lazy" style={{ width: '100%', display: 'block' }}/>
              ))}
            </div>
          </section>
        )}

        {/* Spec accordion */}
        <section style={{ padding: '32px 20px 0' }}>
          <button
            type="button"
            onClick={() => setOpenSpec(s => !s)}
            style={{
              width: '100%', display: 'flex', justifyContent: 'space-between', alignItems: 'center',
              background: 'none', border: 'none', padding: '16px 0',
              borderTop: '1px solid var(--cream-deep)',
              borderBottom: openSpec ? 'none' : '1px solid var(--cream-deep)',
              cursor: 'pointer',
              fontFamily: 'var(--sans)', fontSize: '13px', fontWeight: 500,
              letterSpacing: '0.18em', textTransform: 'uppercase', color: 'var(--dark)',
            }}
          >
            <span>Specification</span>
            <span aria-hidden style={{ fontSize: '18px', color: 'var(--dark-mid)' }}>{openSpec ? '−' : '+'}</span>
          </button>
          {openSpec && (
            <div style={{ paddingBottom: '20px', borderBottom: '1px solid var(--cream-deep)' }}>
              <SpecRow label="Collection" value={product.collection || '—'}/>
              <SpecRow label="Variant"    value={product.variantLabel || product.name}/>
              <SpecRow label="Sizes"      value={(product.sizes || [product.size]).filter(Boolean).join(' · ') || '—'}/>
              <SpecRow label="Finishes"   value={(product.finishes || [product.finish]).filter(Boolean).join(' · ') || '—'}/>
              <SpecRow label="Styles"     value={(product.styles || [product.style]).filter(Boolean).join(' · ') || '—'}/>
              <SpecRow label="Rooms"      value={(product.rooms || []).join(' · ') || '—'}/>
              <SpecRow label="Price"      value={product.price || 'POA'}/>
            </div>
          )}
        </section>

        {/* Suggestions */}
        {suggestions.length > 0 && (
          <section style={{ padding: '32px 0 32px' }}>
            <p className="t-label" style={{ color: 'var(--dark-mid)', margin: '0 20px 12px' }}>You may also like</p>
            <div style={{
              display: 'flex', gap: '12px', overflowX: 'auto',
              padding: '0 20px 8px',
              scrollSnapType: 'x mandatory',
              WebkitOverflowScrolling: 'touch',
            }}>
              {suggestions.map(t => (
                <button
                  key={t.id}
                  type="button"
                  onClick={() => navigate('product', t)}
                  style={{
                    flex: '0 0 64%', maxWidth: '240px',
                    background: 'none', border: 'none', padding: 0, cursor: 'pointer',
                    textAlign: 'left', scrollSnapAlign: 'start',
                  }}
                >
                  <div className="tile-frame" style={{ aspectRatio: '1', overflow: 'hidden' }}>
                    <img src={pSightImg(t)} alt={t.name} loading="lazy" style={{ width: '100%', height: '100%', objectFit: 'cover' }}/>
                  </div>
                  <p style={{ fontFamily: 'var(--sans)', fontSize: '10px', letterSpacing: '0.16em', textTransform: 'uppercase', color: 'var(--terracotta)', margin: '10px 0 4px' }}>{t.collection}</p>
                  <p style={{ fontFamily: 'var(--serif)', fontStyle: 'italic', fontSize: '15px', color: 'var(--dark)', margin: 0 }}>{t.name}</p>
                </button>
              ))}
            </div>
          </section>
        )}
      </div>

      {/* Sticky bottom CTA bar */}
      <div style={{
        position: 'fixed', left: 0, right: 0, bottom: 0, zIndex: 90,
        display: 'flex', alignItems: 'center', gap: '10px',
        padding: '10px 16px calc(10px + env(safe-area-inset-bottom, 0px))',
        background: 'rgba(255,255,255,0.96)',
        backdropFilter: 'saturate(140%) blur(14px)',
        WebkitBackdropFilter: 'saturate(140%) blur(14px)',
        borderTop: '1px solid var(--cream-deep)',
      }}>
        <button
          type="button"
          onClick={() => toggleFavourite(product.id)}
          aria-label={faved ? 'Remove from favourites' : 'Add to favourites'}
          style={{
            width: '52px', height: '52px',
            background: 'white',
            border: `1px solid ${faved ? 'var(--terracotta)' : 'var(--cream-deep)'}`,
            color: faved ? 'var(--terracotta)' : 'var(--dark)',
            fontSize: '20px', cursor: 'pointer',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            flexShrink: 0,
          }}
        >{faved ? '♥' : '♡'}</button>
        <button
          type="button"
          onClick={submitQuote}
          style={{
            flex: 1, height: '52px',
            background: 'var(--dark)', color: 'var(--cream)',
            border: 'none', cursor: 'pointer',
            fontFamily: 'var(--sans)', fontSize: '12px', fontWeight: 500,
            letterSpacing: '0.2em', textTransform: 'uppercase',
          }}
        >Request a quote</button>
      </div>
    </PhonePageShell>
  );
}

function SpecRow({ label, value }) {
  return (
    <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: '14px', padding: '10px 0', borderBottom: '1px dashed var(--cream-deep)' }}>
      <span style={{ fontFamily: 'var(--sans)', fontSize: '10px', letterSpacing: '0.18em', textTransform: 'uppercase', color: 'var(--dark-mid)' }}>{label}</span>
      <span style={{ fontFamily: 'var(--serif)', fontStyle: 'italic', fontSize: '14px', color: 'var(--dark)', textAlign: 'right' }}>{value}</span>
    </div>
  );
}

function PhoneColourCell({ tile, active, onClick }) {
  return (
    <button
      type="button"
      onClick={onClick}
      style={{
        flex: '0 0 auto',
        width: '120px',
        background: 'none', border: 'none', padding: 0,
        cursor: active ? 'default' : 'pointer',
        textAlign: 'left',
        scrollSnapAlign: 'start',
      }}
    >
      <div style={{
        position: 'relative',
        width: '120px', height: '120px',
        background: '#ffffff',
        overflow: 'hidden',
        boxShadow: active ? 'inset 0 0 0 2px var(--dark)' : 'none',
      }}>
        <img
          src={pColourImg(tile)}
          alt={tile.name}
          loading="lazy"
          style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
        />
      </div>
      <p style={{ fontFamily: 'var(--serif)', fontStyle: 'italic', fontSize: '13px', color: 'var(--dark)', margin: '8px 2px 0', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', width: '120px' }}>
        {tile.name}
      </p>
      {active && (
        <p style={{ fontFamily: 'var(--sans)', fontSize: '9px', letterSpacing: '0.22em', textTransform: 'uppercase', color: 'var(--terracotta)', margin: '2px 2px 0' }}>Current</p>
      )}
    </button>
  );
}

// ─── PhoneJournal ────────────────────────────────────────────────────
function PhoneJournal({ navigate, pageData }) {
  const live = usePhoneArticles();
  const articles = live.length ? live : ARTICLES_DATA;
  const activeArticle = (pageData && pageData.articleId)
    ? articles.find(a => a.id === pageData.articleId) : null;

  if (activeArticle) {
    return (
      <PhonePageShell navigate={navigate}>
        <article style={{ paddingBottom: '40px' }}>
          <div style={{ aspectRatio: '3 / 2', overflow: 'hidden', background: 'var(--cream-deep)' }}>
            <img src={activeArticle.hero} alt={activeArticle.title} style={{ width: '100%', height: '100%', objectFit: 'cover' }}/>
          </div>
          <div style={{ padding: '24px 20px' }}>
            <button type="button" onClick={() => navigate('back')} style={{
              background: 'none', border: 'none', padding: 0, cursor: 'pointer',
              fontFamily: 'var(--sans)', fontSize: '11px', letterSpacing: '0.16em', textTransform: 'uppercase', color: 'var(--dark-mid)',
              marginBottom: '24px',
            }}>← Journal</button>
            <p className="t-label" style={{ color: 'var(--terracotta)', marginBottom: '12px' }}>{activeArticle.category} · {activeArticle.date} · {activeArticle.read}</p>
            <h1 style={{ fontFamily: 'var(--serif)', fontWeight: 300, fontSize: 'clamp(28px, 8vw, 42px)', lineHeight: 1.1, letterSpacing: '-0.015em', margin: '0 0 20px' }}>{activeArticle.title}</h1>
            <p style={{ fontFamily: 'var(--serif)', fontStyle: 'italic', fontSize: '17px', lineHeight: 1.6, color: 'var(--dark-mid)', margin: '0 0 24px' }}>{activeArticle.intro}</p>
            {(activeArticle.body || '').split('\n\n').map((p, i) => (
              <p key={i} style={{ fontFamily: 'var(--sans)', fontWeight: 300, fontSize: '15px', lineHeight: 1.75, color: 'var(--dark-mid)', margin: '0 0 18px' }}>{p}</p>
            ))}
          </div>
        </article>
      </PhonePageShell>
    );
  }

  const [featured, ...rest] = articles;
  return (
    <PhonePageShell navigate={navigate}>
      <div style={{ padding: '28px 20px 12px' }}>
        <p className="t-label" style={{ color: 'var(--terracotta)', marginBottom: '12px' }}>Journal</p>
        <h1 style={{ fontFamily: 'var(--serif)', fontWeight: 300, fontSize: 'clamp(32px, 9vw, 52px)', lineHeight: 1.0, letterSpacing: '-0.02em', margin: 0 }}>
          Ideas, rooms<br/><em>&amp; material stories</em>
        </h1>
      </div>
      {featured && (
        <button
          type="button"
          onClick={() => navigate('journal', { articleId: featured.id })}
          style={{ display: 'block', width: '100%', background: 'none', border: 'none', padding: 0, cursor: 'pointer', textAlign: 'left', marginTop: '20px' }}
        >
          <div style={{ aspectRatio: '4 / 3', overflow: 'hidden', background: 'var(--cream-deep)' }}>
            <img src={featured.hero} alt={featured.title} style={{ width: '100%', height: '100%', objectFit: 'cover' }}/>
          </div>
          <div style={{ padding: '18px 20px 28px', background: 'var(--cream-mid)' }}>
            <p className="t-label" style={{ color: 'var(--terracotta)', marginBottom: '10px' }}>Featured · {featured.category}</p>
            <h2 style={{ fontFamily: 'var(--serif)', fontWeight: 300, fontSize: '26px', lineHeight: 1.1, margin: '0 0 10px' }}>{featured.title}</h2>
            <p style={{ fontFamily: 'var(--sans)', fontWeight: 300, fontSize: '14px', color: 'var(--dark-mid)', lineHeight: 1.6 }}>{featured.intro}</p>
          </div>
        </button>
      )}
      <div style={{ padding: '32px 20px', display: 'grid', gap: '28px' }}>
        {rest.map(a => (
          <button
            key={a.id}
            type="button"
            onClick={() => navigate('journal', { articleId: a.id })}
            style={{ background: 'none', border: 'none', padding: 0, cursor: 'pointer', textAlign: 'left' }}
          >
            <div style={{ aspectRatio: '4 / 3', overflow: 'hidden', background: 'var(--cream-deep)', marginBottom: '14px' }}>
              <img src={a.thumb} alt={a.title} style={{ width: '100%', height: '100%', objectFit: 'cover' }}/>
            </div>
            <p className="t-label" style={{ color: 'var(--terracotta)', marginBottom: '8px' }}>{a.category} · {a.read}</p>
            <h3 style={{ fontFamily: 'var(--serif)', fontSize: '22px', fontWeight: 400, lineHeight: 1.2, margin: '0 0 6px' }}>{a.title}</h3>
            <p style={{ fontFamily: 'var(--sans)', fontSize: '13px', fontWeight: 300, color: 'var(--dark-mid)', lineHeight: 1.6 }}>{(a.intro || '').slice(0, 90)}…</p>
          </button>
        ))}
      </div>
    </PhonePageShell>
  );
}

// ─── PhoneQuote ──────────────────────────────────────────────────────
// Single-column form on mobile. Reuses the desktop Quote underneath
// when possible — but renders a tighter mobile layout.
function PhoneQuote({ navigate, prefill }) {
  // For simplicity we delegate to the desktop Quote component which
  // is already responsive enough at narrow widths (the 720px breakpoint
  // collapses the form to one column). We just wrap it in the phone
  // shell so the global Nav doesn't double up.
  const Inner = window.Quote;
  return (
    <PhonePageShell navigate={navigate}>
      {Inner ? <Inner navigate={navigate} prefill={prefill}/> : <p style={{ padding: 20 }}>Quote form unavailable.</p>}
    </PhonePageShell>
  );
}

// ─── PhoneAccount ────────────────────────────────────────────────────
function PhoneAccount({ navigate, tiles }) {
  const { profile, favourites } = useAccount();
  const favTiles = (favourites || []).map(id => tiles.find(t => t.id === id)).filter(Boolean);
  return (
    <PhonePageShell navigate={navigate}>
      <div style={{ padding: '28px 20px 8px' }}>
        <p className="t-label" style={{ color: 'var(--terracotta)', marginBottom: '12px' }}>Your account</p>
        <h1 style={{ fontFamily: 'var(--serif)', fontWeight: 300, fontSize: 'clamp(34px, 9vw, 50px)', lineHeight: 1, letterSpacing: '-0.02em', margin: 0 }}>
          Hi, {profile?.name?.split(' ')[0] || 'there'}
        </h1>
      </div>
      <div style={{ padding: '24px 16px 40px' }}>
        <p className="t-label" style={{ color: 'var(--dark-mid)', margin: '0 4px 14px' }}>Favourites ({favTiles.length})</p>
        {favTiles.length === 0 ? (
          <div style={{ textAlign: 'center', padding: '40px 16px' }}>
            <p style={{ fontSize: '48px', color: 'var(--terracotta)', margin: '0 0 8px' }}>♡</p>
            <p style={{ fontFamily: 'var(--serif)', fontSize: '22px', fontWeight: 300, color: 'var(--dark)', margin: '0 0 8px' }}>No favourites yet</p>
            <p style={{ fontFamily: 'var(--sans)', fontSize: '13px', color: 'var(--dark-mid)', marginBottom: '20px' }}>Tap the heart on any tile to save it here.</p>
            <button type="button" onClick={() => navigate('collections')} style={{
              padding: '14px 24px', background: 'var(--dark)', color: 'var(--cream)',
              border: 'none', cursor: 'pointer',
              fontFamily: 'var(--sans)', fontSize: '11px', fontWeight: 500,
              letterSpacing: '0.2em', textTransform: 'uppercase',
            }}>Browse tiles</button>
          </div>
        ) : (
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px 8px' }}>
            {favTiles.map(t => <PhoneTileCard key={t.id} tile={t} navigate={navigate}/>)}
          </div>
        )}
      </div>
    </PhonePageShell>
  );
}

// ─── Hooks (local copies) ────────────────────────────────────────────
function usePhoneTiles() {
  const [tiles, setTiles] = useState([]);
  useEffect(() => {
    let cancelled = false;
    fetch('/api/tiles').then(r => r.json()).then(d => { if (!cancelled) setTiles((d?.tiles) || []); }).catch(() => {});
    return () => { cancelled = true; };
  }, []);
  return tiles;
}
function usePhoneCollectionsMeta() {
  const [meta, setMeta] = useState({});
  useEffect(() => {
    let cancelled = false;
    fetch('/api/collections').then(r => r.json()).then(d => { if (!cancelled) setMeta(d?.collections || {}); }).catch(() => {});
    return () => { cancelled = true; };
  }, []);
  return meta;
}

Object.assign(window, {
  PhoneHome, PhoneListing, PhoneProduct, PhoneJournal, PhoneQuote, PhoneAccount,
});

